【全世界待望】Public AccessのRDSにIAM認証でLambda Pythonから接続する
こんにちは。てるい@さっぽろです。
先日、Serverlessおじさん担当なるものに任命されました。かねてからトイレIoTの元祖として名を馳せ、イケてるIoT事例を連発しているIoTお兄さん担当に負けないよう頑張っていきたいと思います。
さて、先日(2017.04.25)に全世界待望のRDSへのIAM認証(IAM Database Authentication)がリリースされました。リリース時にはSDKのIAM認証を行うための署名を作る機能のリリースが後追いとなっており試すに試せない状況でしたが、今朝(2017.04.27)についにリリースされたので、さっそく検証してみようと思います。
全世界はかなり言い過ぎですが、ある程度の規模の開発をServerless(API Gateway + Lambda)で行ったことがある人ならば、多くの人が思ったことがあるはずです。ですが、これには今まで大きな壁がありました。
それは、「VPC Lambda 10秒の壁」と呼ばれています(私の中で)
もう一つの選択肢として、RDSをPublic Accessで起動することが挙げられます。ですが、これにはセキュリティ上のリスクが発生します。VPC内でNAT GatewayへRoutingされたLambdaを除いて、接続元のIPアドレスは不定になります。そのため、接続元を公開されているAWSのIPアドレスのみに絞る程度のことしかできません。SSL接続を行うことで盗聴のリスクは回避できますが、認証がMySQLのID/PasswordではSSHをIP制限せずに公開鍵ではなく、Password認証で運用するようなものでした。
重要なオプションは以下の通りです。
ポイントは以下の通りです。
ここでのポイントは以下の通りです。
このように、virtualenv内で
次に、以下のような設定を設定ファイル
SSL接続する際には証明書が必要になるのでダウンロードして同梱します。
そして、Functionを書きます。
デプロイして実行します。
キタ━━━━(゚∀゚)━━━━!!
そうです。Masterユーザです。
Masterユーザは固定Passwordのまま残ってしまっています。MasterユーザはRDSの基本となるユーザで、削除することができません。なので、例えばMasterユーザはVPC内部からしか接続できないようにしてしまうのが良いのではないかと思います。そうすれば、VPC内部にクライアントのEC2を置けばコマンドラインからのオペレーションはIAM認証を意識せずに今まで通り行うことができます。以下のようなmysqlコマンドラインから以下のようなコマンドを実行します。
これで、 VPCのCIDRが
また、リモートのETLツールなどから接続するためにVPNを毎回設定していたような場面でも代替手段として有効です。
全世界待望の機能を是非、皆様もお役立てください。
先日、Serverless
さて、先日(2017.04.25)に全世界待望のRDSへのIAM認証(IAM Database Authentication)がリリースされました。リリース時にはSDKのIAM認証を行うための署名を作る機能のリリースが後追いとなっており試すに試せない状況でしたが、今朝(2017.04.27)についにリリースされたので、さっそく検証してみようと思います。
全世界待望?
「LambdaからRDSに繋ぎたいと思ったことはありませんか?」全世界はかなり言い過ぎですが、ある程度の規模の開発をServerless(API Gateway + Lambda)で行ったことがある人ならば、多くの人が思ったことがあるはずです。ですが、これには今まで大きな壁がありました。
それは、「VPC Lambda 10秒の壁」と呼ばれています(私の中で)
VPC Lambda 10秒の壁
LambdaからRDSへセキュアに接続しようとすると、Private接続を行うためにRDSと同じVPC内でLambdaを起動することがまず最初の選択肢となります。この時、VPC外でコールドスタートしたLambdaには起動する際にVPC内で通信するための「ENI生成」という処理が始めに実行されます。この処理は10秒程度(あるいはそれ以上)かかるものであり、これはオンライン処理では到底許容できるものではありません。もう一つの選択肢として、RDSをPublic Accessで起動することが挙げられます。ですが、これにはセキュリティ上のリスクが発生します。VPC内でNAT GatewayへRoutingされたLambdaを除いて、接続元のIPアドレスは不定になります。そのため、接続元を公開されているAWSのIPアドレスのみに絞る程度のことしかできません。SSL接続を行うことで盗聴のリスクは回避できますが、認証がMySQLのID/PasswordではSSHをIP制限せずに公開鍵ではなく、Password認証で運用するようなものでした。
IAM認証によってどうなるか
後者のPublic Access時の認証強度の問題が解決できます。つまり、セキュリティを保ったまま10秒の壁が無いLambda FunctionからRDSへ接続することが可能となるということです。試してみる
さて、前置きはこれくらいにして早速試してみましょう。今回はAWS CLIから操作してみます。途中、ホスト名やパスワードが出てきますが適宜置き換えてください(該当のリソースは既に削除しているのでここに書かれているホスト名やパスワードではどこにも繋がりません)RDSを起動する
1 2 3 4 5 6 7 8 9 10 11 |
$ aws rds create-db-instance \ --db-instance-identifier iam-auth-test \ --db-instance-class db.t2.micro \ --engine MySQL \ --engine-version 5.7.16 \ --allocated-storage 20 \ --master-username masteruser \ --master-user-password ADKL996xKkjDCXMPXyPH \ --enable-iam-database-authentication \ --publicly-accessible |
- --engine
現状はMySQL(とAurora)のみ対応です - --engine-version
対応バージョンは現時点で最新のMySQL5.6(5.6.27)、5.7(5.7.16)、Aurora(1.10a)以降のバージョンとなります - --enable-iam-database-authentication
IAM認証を有効にします - --master-user-password
詳しくは後述しますがIAM認証を有効としても最初はここで決めたMasterユーザのID/パスワードでログインする必要があるため念のため複雑なものにしましょう - --publicly-accessible
Public Accessを有効にします
modify-db-(instance|cluster)
コマンドにも --enable-iam-database-authentication
オプションがあり変更可能です。IAM認証用のユーザを作成する
MySQLのユーザ作成
IAM認証を利用する場合、専用のユーザを作成する必要があります。先ほどのMasterユーザでの接続はここで必要となります。
1 2 3 4 5 |
$ mysql -h iam-auth-test.c8oumabfehzg.ap-northeast-1.rds.amazonaws.com -u masteruser -pADKL996xKkjDCXMPXyPH mysql> CREATE USER 'takagisan'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'; mysql> GRANT select ON *.* TO 'takagisan'@'%' REQUIRE SSL; |
- CREATE USER
- ユーザ作成時に 'ユーザ名'@'%' で作成し、Host部を
%
(あらゆるIP/Hostからアクセス可能)とする IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'
でIAM認証プラグインでの認証とする
- ユーザ作成時に 'ユーザ名'@'%' で作成し、Host部を
- GRANT
REQUIRE SSL
= SSLによる接続を必須とする
IAM Roleを作成
以下のようなPolicyを持つロールを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" }, { "Effect": "Allow", "Action": [ "rds-db:connect" ], "Resource": [ "arn:aws:rds-db:ap-northeast-1:123456789:dbuser:db-JXKLCDAI6GU32IM3TUWHDHUYNQ/takagisan" ] } ] } |
rds-db:connect
がRDSへのIAM認証を許可するためのAction- Resource部の形式は
arn:aws:rds-db:リージョン:アカウントID:dbuser:DBリソースID/ユーザ名
DBリソースIDはaws rds describe-db-instances
を実行するとDbiResourceId
の項目で確認可能です logs:
でCloudWatchLogsの権限を与えているのは、Lambda Functionはログ出力先であるCloudWatchLogsの権限の権限が必須であるためで本件と直接的な関係はありません
Lambda Functionのデプロイ
いよいよ、Lambda Functionをデプロイして接続してみます。MySQLへの接続ライブラリはMySQL公式のmysql-connector-pythonを使用します。こういった単独で動くLambda Functionを依存毎まとめてデプロイするのには手前味噌ですがlamveryというツールが楽なのでこちらを使用します。ツールを使用せずにライブラリを同梱してデプロイする方法は公式ドキュメントをご確認ください。
1 2 3 4 5 6 7 8 |
$ mkdir ~/iam-auth-test $ cd ~/iam-auth-test $ pip install lamvery $ pip install virtualenv $ virtualenv .venv $ source .venv/bin/activate (.venv) $ pip install https://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-2.1.6.tar.gz |
mysql-connector-python
をインストールします。次に、以下のような設定を設定ファイル
.lamvery.yml
に書き込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
profile: null region: ap-northeast-1 versioning: false default_alias: null clean_build: false configuration: name: iam-db-auth-py runtime: python2.7 role: arn:aws:iam::123456789:role/lambda_basic_execution # 今回作成したIAM Role handler: lambda_function.lambda_handler description: This is a sample lambda function. timeout: 10 memory_size: 128 |
1 2 |
$ wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import mysql.connector import boto3 import os RDS_HOST = 'iam-auth-test.c8oumabfehzg.ap-northeast-1.rds.amazonaws.com' RDS_USER = 'takagisan' RDS_REGION = 'ap-northeast-1' rds = boto3.client('rds') def lambda_handler(event, context): password = rds.generate_db_auth_token( DBHostname=RDS_HOST, Port=3306, DBUsername=RDS_USER ) conn = mysql.connector.connect( user=RDS_USER, password=password, host=RDS_HOST, database='mysql', charset='utf8', ssl_verify_cert=True, ssl_ca='{}/rds-combined-ca-bundle.pem'.format(os.environ['LAMBDA_TASK_ROOT']) ) cursor = conn.cursor() cursor.execute('SELECT * FROM user') rows = cursor.fetchall() for row in rows: print(row) |
1 2 3 4 5 6 7 8 9 10 |
$ lamvery deploy $ lamvery invoke {} START RequestId: da5d870c-2b06-11e7-bc23-f341078587e8 Version: $LATEST (bytearray(b'localhost'), bytearray(b'rdsadmin'), u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'', bytearray(b''), bytearray(b''), bytearray(b''), 0, 0, 0, 0, bytearray(b'mysql_native_password'), bytearray(b'*32E01955FAD89470CB8B6C92292F808851DDBD20'), u'N', datetime.datetime(2017, 4, 27, 2, 48, 10), None, u'N') (bytearray(b'localhost'), bytearray(b'mysql.sys'), u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'', bytearray(b''), bytearray(b''), bytearray(b''), 0, 0, 0, 0, bytearray(b'mysql_native_password'), bytearray(b'*THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE'), u'N', datetime.datetime(2017, 4, 27, 2, 47, 11), None, u'Y') (bytearray(b'%'), bytearray(b'masteruser'), u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'N', u'Y', u'N', u'Y', u'Y', u'Y', u'Y', u'Y', u'N', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'Y', u'N', u'', bytearray(b''), bytearray(b''), bytearray(b''), 0, 0, 0, 0, bytearray(b'mysql_native_password'), bytearray(b'*FEE5A5DB645CAD2E4594A656BBB2CF5FCA6F1E8E'), u'N', datetime.datetime(2017, 4, 27, 2, 47, 7), None, u'N') (bytearray(b'%'), bytearray(b'takagisan'), u'Y', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'N', u'ANY', bytearray(b''), bytearray(b''), bytearray(b''), 0, 0, 0, 0, bytearray(b'AWSAuthenticationPlugin'), bytearray(b'RDS'), u'N', None, None, u'N') END RequestId: da5d870c-2b06-11e7-bc23-f341078587e8 REPORT RequestId: da5d870c-2b06-11e7-bc23-f341078587e8 Duration: 85.93 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 34 MB |
Appendix
さて、これでSSLとIAM認証を利用したセキュアな接続が可能となりましたが、何か一つ穴があると思いませんか?そうです。Masterユーザです。
Masterユーザは固定Passwordのまま残ってしまっています。MasterユーザはRDSの基本となるユーザで、削除することができません。なので、例えばMasterユーザはVPC内部からしか接続できないようにしてしまうのが良いのではないかと思います。そうすれば、VPC内部にクライアントのEC2を置けばコマンドラインからのオペレーションはIAM認証を意識せずに今まで通り行うことができます。以下のようなmysqlコマンドラインから以下のようなコマンドを実行します。
1 2 |
rename user 'masteruser'@'%' to 'masteruser'@'10.0.%'; |
10.0.0.0/16
であれば内部からしか接続できないということになります。
まとめ
実質的にServerless(API GW + Lambda)のオンライン処理ではRDSが使えない状況で、これまでは採用を見送ったり、DynamoDBで頑張って苦労してきた方も多いかと思います。今回のIAM認証によって、全てがクリアされたわけではないものの、LambdaからRDSを使う上での一番大きな問題がクリアされました。これからServerless開発がさらに盛り上がるのではないかと思います。また、リモートのETLツールなどから接続するためにVPNを毎回設定していたような場面でも代替手段として有効です。
全世界待望の機能を是非、皆様もお役立てください。
COMMENT ON FACEBOOK