こんにちは。てるい@さっぽろです。
先日、Serverlessおじさん担当なるものに任命されました。かねてからトイレIoTの元祖として名を馳せ、イケてるIoT事例を連発しているIoTお兄さん担当に負けないよう頑張っていきたいと思います。
さて、先日(2017.04.25)に全世界待望のRDSへのIAM認証(IAM Database Authentication)がリリースされました。リリース時にはSDKのIAM認証を行うための署名を作る機能が後追いとなっており試すに試せない状況でしたが、今朝(2017.04.27)ついにIAM認証に対応した新バージョンのSDKがリリースされたので、さっそく検証してみようと思います。
全世界待望?
「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へ接続することが可能となるということです。
IAMによる認証はDynamoDBを始め、AWSのAPIで利用されているものです。AWSのAPIは当然IP制限などされていないですが、それでも信頼されて使われていますので、それがどの程度の強度なのかはお分かりいただけるかと思います。
試してみる
さて、前置きはこれくらいにして早速試してみましょう。今回はAWS CLIから操作してみます。途中、ホスト名やパスワードが出てきますが適宜置き換えてください(該当のリソースは既に削除しているのでここに書かれているホスト名やパスワードではどこにも繋がりません)
参考にした公式のドキュメントはこちらです。
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
RDSを起動する
$ 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ユーザでの接続はここで必要となります。
$ 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認証プラグインでの認証とする
- ユーザ作成時に
- GRANT
REQUIRE SSL
= SSLによる接続を必須とする
IAM Roleを作成
以下のようなPolicyを持つロールを作成します。
{
"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というツールが楽なのでこちらを使用します。ツールを使用せずにライブラリを同梱してデプロイする方法は公式ドキュメントをご確認ください。
$ 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
このように、virtualenv内で mysql-connector-python
をインストールします。
次に、以下のような設定を設定ファイル .lamvery.yml
に書き込みます。
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
SSL接続する際には証明書が必要になるのでダウンロードして同梱します。
$ wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem
そして、Functionを書きます。
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)
ポイントをかいつまんで説明すると、 boto3.client('rds').generate_db_auth_token
が認証用のトークンを生成するメソッドで、これは対象のRDSのホスト名とポート・ユーザ名を引数として与えます。Lambdaが起動するリージョンとRDSのリージョンが異なる場合はリージョンも必要です。そして、ここで生成したトークンをMySQLのパスワードとして利用することで認証が行われます。
デプロイして実行します。
$ 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のroot権限が使用できないRDSでrootの代わりとなるユーザで仕様上、削除することができません。なので、例えばMasterユーザはVPC内部からしか接続できないようにしてしまうのが良いのではないかと思います。そうすれば、VPC内部にクライアントのEC2を置けばコマンドラインからのオペレーションはIAM認証を意識せずに今まで通り行うことができます。mysqlコマンドラインから以下のようなコマンドを実行します。
mysql> RENAME USER 'masteruser'@'%' TO 'masteruser'@'10.0.%';
これで、 VPCのCIDRが 10.0.0.0/16
であれば内部からしか接続できないということになります。
まとめ
実質的にServerless(API GW + Lambda)のオンライン処理ではRDSが使えない状況で、これまでは採用を見送ったり、DynamoDBで頑張って苦労してきた方も多いかと思います。今回のIAM認証によって、全てがクリアされたわけではないものの、LambdaからRDSを使う上での一番大きな問題がクリアされました。これからServerless開発がさらに盛り上がるのではないかと思います。
また、リモートのETLツールなどから接続するためにVPNを毎回設定していたような場面でも代替手段として有効です。
全世界待望の機能を是非、皆様もお役立てください。