【全世界待望】Public AccessのRDSへIAM認証(+ SSL)で安全にLambda Pythonから接続する

記事タイトルとURLをコピーする

こんにちは。てるい@さっぽろです。

先日、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を毎回設定していたような場面でも代替手段として有効です。

全世界待望の機能を是非、皆様もお役立てください。