Node.js
AWS
RDS
lambda
serverless

AWS Lambda(Node.js)からAmazon RDSへIAM認証で接続する

概要

現在開発しているburariというおでかけアプリでは、APIGatewayからLambdaを実行し、RDSのデータを取得している。
以前まで、LambdaからRDSへの安全な接続にはVPCが必須となっており、AWS Lambdaの定期実行をSAMを使って実装するにも書いたとおり、コールドスタート問題が懸念になっていたが、IAM認証でLambdaからRDSに接続できるようになったため、ついにVPCから解放される日がきた。VPCから解放されることにより、LambdaからSNSやKMSへの接続にNATが不要になるため、お財布にも優しいサービス運営が待ち受けている。
今回は接続まわりを実装してみる 💪💪

環境

  • Amazon RDS (MySQL v5.7.17, t2.micro)
  • AWS Lambda (Node.js v8.10)
  • Sequelize (ORM for Node.js)

制限

以下のような同時接続数への制限が発生するため、t2.microの場合は接続数を10以下に制限する必要がある。

IAM データベース認証を使用する場合は、db.t2.micro インスタンスクラスを使用している場合を除き、1 秒あたりの接続数を 20 以下に制限する必要があります。この場合、1 秒あたりの接続数を 10 以下に制限する必要があります。

RDSの「IAM DB Authentication Enabled」をオンにする

今回は既存のDBの設定をAWS Console上から変更する。

Screen Shot 2017-08-23 at 10.15.24 PM.png

変更するInstanceを選択し、Instance ActionsのModifyから、上記の「IAM DB Authentication Enabled」をYESにすればOK。

DB Userの作成

Masterユーザーでログインし、IAM認証用のユーザーを作成する。このときHostには「%」を指定。

CREATE USER 'iam_user'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS';

そしてSSL経由での使用を許可するよう変更。

GRANT SELECT,INSERT,UPDATE,DELETE ON dbname.* to 'iam_user'@'%' REQUIRE SSL;

IAM Policyの作成と、IAM Roleへの付与

以下のようなPolicyを作成する。dbi-resource-idはConsole上の「Resource ID」に記載されている。

AmazonRDSIAMAccess
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "rds-db:connect"
            ],
            "Resource": [
                "arn:aws:rds-db:ap-northeast-1:1234567890:dbuser:<dbi-resource-id>/iam_user"
            ]
        }
    ]
}

Policy作成後、Lambdaを実行するRoleに上記PolicyをAttachする。

コードの実装

Tokenを取得した後、Sequelizeを利用してRDSからデータを取得する。

index.js
import AWS from 'aws-sdk';
import Sequelize from 'sequelize';

export const handler = async (event, context, callback) => {
    const connect = async () => {
        const signer = new AWS.RDS.Signer({
            'region': 'ap-northeast-1',
            'username': 'iam_user',
            'hostname': 'dbname.xxxxxxxx.ap-northeast-1.rds.amazonaws.com',
            'port': XXXX
        });

        let token;
        await signer.getAuthToken((error, result) => {
            if(error) {
                throw error;
            }

            token = result;
        });

        return token;
    };

    const fetch = async (token) => {
        const options = {
            'host': 'dbname.xxxxxxxx.ap-northeast-1.rds.amazonaws.com',
            'port': XXXX,
            'ssl': true,
            'dialect': 'mysql',
            'dialectOptions': {
            'ssl': 'Amazon RDS',
            'authSwitchHandler': (data, callback) => {
                if (data.pluginName === 'mysql_clear_password') {
                    const password = token + '\0';
                    const buffer = Buffer.from(password);
                    callback(null, buffer);
                }
            }
        };

        const sequelize = new Sequelize('dbname', 'iam_user', token, options);
        const model = sequelize.define('table_name', {
            'tableId': { 'type': Sequelize.STRING, 'primaryKey': true },
            'name': Sequelize.STRING
        });

        return model.findAll();
    };

    try {
        const token = await connect();
        const data = await fetch(token);
        context.succeed(data);

    } catch(error) {
        context.fail(error);
    }
}

実行結果

RDSからデータを取得することに成功 🙆🙆

Screen Shot 2017-08-24 at 1.28.28 AM.png

参考