⌨️

今から始めるLambda⑦「LambdaからRDS(Aurora)に接続する 後編」

2021/11/01に公開

はじめに

前回の記事ではVPC内にAuroraDBインスタンスを立ち上げるところまでを行いました。

https://zenn.dev/nekoniki/articles/5da3016346b4b0

今回は同一VPC内にあるLambdaからDBに接続をしたいと思います。
そのために使用するRDS Proxyのセットアップなども含めて紹介していきます。

前提としてRDSのインスタンスやVPCの作成は済んでいるものとします。

RDS Proxyとは

https://aws.amazon.com/jp/rds/proxy/

Amazon RDS Proxy は、Amazon Relational Database Service (RDS) 向けの高可用性フルマネージド型データベースプロキシで、アプリケーションのスケーラビリティやデータベース障害に対する回復力と安全性を高めます。

文字通りAWS RDSのプロキシです。
なぜ今回これが必要かというと、LambdaからRDSへの接続を考えた際に、Lambdaはリクエスト毎に起動し、都度コネクションを貼ってしまうためです。

いわゆる同時接続数問題というやつで、RDS Proxyが間に入って接続プールを管理してくれることで、これらが解消されます。

RDS Proxyが登場する以前はLambdaRDSの組み合わせはアンチパターンに近い扱いをされていたようです(以下リンク参照)。

https://www.keisuke69.net/entry/2017/06/21/121501

RDS Proxyの設定

さっそくRDS Proxyの設定を行っていきます。

シークレットの作成

RDS ProxyではDBの認証情報をシークレットで管理します。
そのためAWSSecrets Managerを使用します。

https://aws.amazon.com/jp/secrets-manager/

前回作成したRDSのインスタンスの認証情報(デフォではpostgres)をシークレットに含めます。
名前はrds_proxy_secretとします。

内容のうちKVS_RESOURCEの値についてはAWSのコンソールから「Key Management Service (KMS) > AWS マネージド型キー > aws/secretsmanager」の値を使います。

aws secretsmanager create-secret \
  --name "rds_proxy_secret" \
  --description "rds_proxy_secret" \
  --region us-east-2 \
  --secret-string '{"username":"postgres","password":"postgres"}'
  --kms-key-id 【KVS_RESOURCE】

レスポンスとしてJSONが返ってくれば成功です。

ロールの作成

今回使用するロールを作成します。
RDSアクセスするため、以下のようなJSONを用意しておきます。
名前はrds-proxy-policy.jsonとします。

rds-proxy-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "rds.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

上記を用いてロールを作成します。
ロール名はrds_proxy_roleとします。

aws iam create-role --role-name rds_proxy_role \
  --assume-role-policy-document file://rds-proxy-policy.json

シークレットを利用するためのポリシー追加

作成したロールにさらにポリシーを追加します。
このポリシーは先ほど使用したシークレットの値を利用するためのものです。

以下のようなsecret-reader-policy.jsonを作成します。
KVS_RESOURCEの値についてはcreate-secretで使用したのと同じ値です。

secret-reader-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": [
                "【SECRET_ARN】"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "kms:Decrypt",
            "Resource": "arn:aws:kms:【REGION】:【USER_ID】:key/【KVS_RESOURCE】",
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "secretsmanager.【REGION】.amazonaws.com"
                }
            }
        }
    ]
}

このJSONを使ってポリシーをロールに追加します。

aws iam put-role-policy --role-name rds_proxy_role \
  --policy-name secret-reader-policy --policy-document file://secret-reader-policy.json

CMKを使う

シークレットを暗号化、福号して使用するためCMKを使います。
CMKAWS Key Management Service(以下KMS)を用いて生成します。

以下のようなrds-key.jsonを用意します。

rds-key.json
{
  "Id":"rds-kms",
  "Version":"2012-10-17",
  "Statement":
    [
      {
        "Sid":"Enable IAM User Permissions",
        "Effect":"Allow",
        "Principal":{"AWS":"arn:aws:iam::【USER_ID】:root"},
        "Action":"kms:*","Resource":"*"
      },
      {
        "Sid":"Allow access for Key Administrators",
        "Effect":"Allow",
        "Principal":
          {
            "AWS":
              ["arn:aws:iam::【USER_ID】:user/【USER_NAME】","arn:aws:iam::【USER_ID】:role/rds_proxy_role"]
          },
        "Action":
          [
            "kms:Create*",
            "kms:Describe*",
            "kms:Enable*",
            "kms:List*",
            "kms:Put*",
            "kms:Update*",
            "kms:Revoke*",
            "kms:Disable*",
            "kms:Get*",
            "kms:Delete*",
            "kms:TagResource",
            "kms:UntagResource",
            "kms:ScheduleKeyDeletion",
            "kms:CancelKeyDeletion"
          ],
        "Resource":"*"
      },
      {
        "Sid":"Allow use of the key",
        "Effect":"Allow",
        "Principal":{"AWS":"arn:aws:iam::【USER_ID】:role/rds_proxy_role"},
        "Action":["kms:Decrypt","kms:DescribeKey"],
        "Resource":"*"
      }
    ]
}

以下コマンドでキーを作成します。

aws kms create-key --description "rds-key" --policy file://rds-key.json

プロキシの作成

いよいよRDS Proxyの作成を行います。
作成したシークレットやロールのARNVPCのサブネットやセキュリティグループなどを指定しましょう。
プロキシの名前はtest-rds_proxyとします。

aws rds create-db-proxy \
    --db-proxy-name test-rds-proxy \
    --engine-family POSTGRESQL \
    --auth '[{"Description": "test_rds_proxy auth", "AuthScheme": "SECRETS", "SecretArn": "【SECRET_ARN】", "IAMAuth": "DISABLED"}]' \
    --role-arn 【ROLE_ARN】 \
    --vpc-subnet-ids '["【SUBNET_1】","【SUBNET_2】"]' \
    --vpc-security-group-ids 【SECURITY_GROUP_ID】

成功するとJSONが返ります。

プロキシとDBの紐付け

作成したプロキシとDBの紐付けを行います。
ここではDBはtest-aurora-postgres-clisterというクラスタ名であるとします。

aws rds register-db-proxy-targets \
    --db-proxy-name test-rds-proxy \
    --db-cluster-identifiers test-aurora-postgresql-cluster

こちらもJSONが返れば成功です。

状態のチェック

RDS ProxyとDBの紐付けには時間がかかるため、以下コマンドで状態をチェックします。

aws rds describe-db-proxy-targets --db-proxy-name test-rds-proxy

レスポンスの中のTargetHealthの値を参照しましょう。
しばらくはPENDING_PROXY_CAPACITYとなっているかと思うので待ちましょう。

AUTH_FAILUREと出た場合はロールの設定に問題がある場合が多いです。

他にもいくつかエラー時の表記があるので、下記リンクから対処法を確認しましょう。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.troubleshooting.html

最終的に値がAVAILABLEとなればOKです。

Lambdaの用意

これでRDS Proxyの使用準備ができました。
先ほど状態チェックで使用したコマンドのレスポンスのうちEndpointにあたる値が、実際のRDS Proxyのエンドポイントです。

前回の記事でのRDSインスタンスとの導通確認ではローカルからpsqlを使って接続できることを確認しました。
しかしながらRDS ProxyVPC内からしかアクセスできない(以下参照)ので、今回はVPC内にあるLambdaからアクセスするものとします。

https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy.limitations

RDSプロキシは、データベースと同じ仮想プライベートクラウド(VPC)に存在する必要があります。データベースはアクセスできますが、プロキシはパブリックにアクセスできません。

Lambdaの内容

以下のようなLambdaRDS ProxyがあるVPCの中に配置します。
環境変数に各種設定がされており、DB_HOSTには前述のEndpointの値が入ります。
また、layers等を用いてpgを利用できる状態にしておきます。

今回クエリを投げるTASKテーブルは、前回記事で既に作成済みのものとします。

index.js
const {Client} = require('pg');

exports.handler = async (event, context) => {
  try{
    // client設定
    const client = new Client({
      host: process.env.DB_HOST,
      port: process.env.PORT,
      user: process.env.USER,
      password: process.env.PASSWORD,
    });
    
    // 接続
    await client.connect(function(err) {
      if (err) {
        console.error('connection error', err.stack)
      }
    });
    
    // クエリ実行
    const res = await client.query('SELECT id, name FROM TASK');
    console.log(res);
    
    return {
      statusCode: 200,
      body: JSON.stringify(res.rows),
    }
    
  }catch (e) {
    console.log("Error...");
    console.log(e);
    throw e;
  }finally{
    
  }
};

このLambda関数を実行することで、以下のレスポンスが返りました。

{
  "statusCode": 200,
  "body": "[{\"id\":0,\"name\":\"task1\"},{\"id\":1,\"name\":\"task2\"}]"
}

これでLambda関数からRDS Proxy経由で接続できていることがわかります。

まとめ

今回はRDS Proxyを経由してLambdaからRDSインスタンスにクエリを投げるところまでを実装しました。
簡略化した実装ですが、今回の内容を用いてAPI Gatewayと組み合わせることでDBから動的なレスポンスを返すREST APIは作れるようになります。

今回の内容が役立ちましたら幸いです。

参考

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy-setup.html#rds-proxy-connecting

https://aws.amazon.com/jp/premiumsupport/knowledge-center/rds-proxy-connection-issues/

https://www.keisuke69.net/entry/2017/06/21/121501

https://aws.amazon.com/jp/secrets-manager/

https://aws.amazon.com/jp/kms/

Discussion