[AWS CDK] Lambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続してみた

Lambda関数からDBに接続したいあなたに
2022.04.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

なんとなくLambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続したいな

こんにちは、のんピ(@non____97)です。

皆さんはLambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続したいと思ったことはありますか? 私はあります。

今までLambda関数からRDBに接続したことがなかったので、なんとなくやってみたいと思いました。

また、最近まだまだ寒いです(2022/4/4)。マネージメントコンソールから真心を込めて作るのも冷え性の私にとっては非常に辛いです。そこでAWS CDKで全てのリソースを作成します。

こちらのコードのリポジトリは以下になります。

作成されるリソースの構成

作成されるリソースの構成は以下の通りです。

構成図

DBクラスター接続用Lambda関数からRDS Proxyを経由してAmazon Aurora DBクラスター(PostgreSQL 13.4)に接続できることを確認します。

Amazon Linux 2のEC2インスタンスはDB内にテーブルを作成したりなどの初期設定で使用します。

また、DBクラスター接続用Lambda関数だけでなくRDS Proxy自身もSecrets Managerにシークレットを取得しに行く必要があるので、RDS ProxyもNAT Gatewayへのルーティングがあるサブネットに配置しています。もし、インターネットにルーティングされることが気になるようであれば、Secrets ManagerのVPCエンドポイントを作成し、RDS ProxyをAmazon Aurora DBクラスターと同じサブネットに配置します。

つまづいたポイント

つまづいたポイントは以下の2点です。

  1. pg-nativeの取り扱い
  2. TLSを使ったDBクラスターへの接続

まず、1つ目のpg-nativeの取り扱いについてです。

Lambda関数からDBクラスターに接続するにあたって、PostgreSQLのクライアントをインストールする必要があります。

今回はnode-postgresを使用しました。

こちらを使用してnpx cdk deployをしたところ、以下のようにエラーが出力されました。

> npx cdk deploy 
MFA token for arn:aws:iam::<AWSアカウントID>:mfa/<IAMユーザー名>: 430772
Bundling asset LambdaAuroraStack/DbAccessFunction/Code/Stage...
✘ [ERROR] Could not resolve "pg-native"

    node_modules/pg/lib/native/client.js:4:21:
      4 │ var Native = require('pg-native')
        ╵                      ~~~~~~~~~~~

  You can mark the path "pg-native" as external to exclude it from the bundle, which will remove
  this error. You can also surround this "require" call with a try/catch block to handle this
  failure at run-time instead of bundle-time.

1 error

/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:474
      throw new Error(`Failed to bundle asset ${this.node.path}, bundle output is located at ${bundleErrorDir}: ${err}`);
            ^
Error: Failed to bundle asset LambdaAuroraStack/DbAccessFunction/Code/Stage, bundle output is located at /<ディレクトリパス>a/cdk.out/bundling-temp-26fc234568dafead65a0b40a064d644e0d951743e4c3676911b851738b116dc6-error: Error: bash -c npx --no-install esbuild --bundle "/<ディレクトリパス>a/src/lambda/handlers/db-access.ts" --target=node14 --platform=node --outfile="/<ディレクトリパス>a/cdk.out/bundling-temp-26fc234568dafead65a0b40a064d644e0d951743e4c3676911b851738b116dc6/index.js" --minify --sourcemap --external:aws-sdk run in directory /<ディレクトリパス>a exited with status 1
    at AssetStaging.bundle (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:474:13)
    at AssetStaging.stageByBundling (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:322:10)
    at stageThisAsset (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:188:35)
    at Cache.obtain (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/private/cache.ts:24:13)
    at new AssetStaging (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:213:44)
    at new Asset (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-s3-assets/lib/asset.ts:131:21)
    at AssetCode.bind (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-lambda/lib/code.ts:282:20)
    at new Function (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-lambda/lib/function.ts:692:29)
    at new NodejsFunction (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/function.ts:100:5)
    at new LambdaAuroraStack (/<ディレクトリパス>a/lib/lambda-aurora-stack.ts:289:5)

Subprocess exited with error 1

node-postgresのIssueを探したところ、同様のチケットがありました。

このチケットではBabelやwebpackでバンドルした際にこのエラーが発生した場合の回避方法が紹介されています。IgnorePluginで回避しているというコメントがあったので、同様にLambda関数をビルドする際にpg-nativeはバンドルしないようにします。

実際のコードは以下の通りです。

./lib/lambda-aurora-stack.ts

// Lambda Function DB access
new nodejs.NodejsFunction(this, "DbAccessFunction", {
  entry: "./src/lambda/handlers/db-access.ts",
  runtime: lambda.Runtime.NODEJS_14_X,
  bundling: {
    minify: true,
    sourceMap: true,
    externalModules: ["pg-native"],

2つ目はTLSを使ったDBクラスターへの接続です。

Lambda関数はDBクライアントを定義する際にssl: trueとすることで、TLSでDBに接続できます。

// DB Client
const dbClient = new Client({
  user: secret.username,
  host: process.env.PGHOST,
  database: secret.dbname,
  password: secret.password,
  port: secret.port,
  ssl: true,
});

ただ、証明書を指定して接続したいときもありますよね?

ということで、証明書を指定して接続してみます。

まず、証明書を用意します。

証明書は以下AWS公式ドキュメントで紹介されている通り、https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem からダウンロードします。

ダウンロードしたファイルは./src/cert/global-bundle.pemに保存しました。

次にこちらの証明書をLambda関数にアップロードします。

その際はaws-cdk-lib.aws_lambda_nodejs moduleCommand hooksを使用します。

実際のコードは以下の通りです。

./lib/lambda-aurora-stack.ts

// Lambda Function DB access
new nodejs.NodejsFunction(this, "DbAccessFunction", {
  entry: "./src/lambda/handlers/db-access.ts",
  runtime: lambda.Runtime.NODEJS_14_X,
  bundling: {
    minify: true,
    sourceMap: true,
    externalModules: ["pg-native"],
    commandHooks: {
      beforeBundling() {
        return [];
      },
      afterBundling(inputDir: string, outputDir: string): string[] {
        return [`cp -p ./src/cert/global-bundle.pem ${outputDir}`];
      },
      beforeInstall() {
        return [];
      },
    },
  },

最後にアップロードした証明書をLambda関数内で読み込みます。

./src/lambda/handlers/db-access.ts

// DB Client
const dbClient = new Client({
  user: secret.username,
  host: process.env.PGHOST,
  database: secret.dbname,
  password: secret.password,
  port: secret.port,
  ssl: {
    rejectUnauthorized: true,
    cert: fs.readFileSync("global-bundle.pem", "utf-8").toString(),
  },
});

ちなみにRDS Proxy側でTLSを強制している状態でTLSを使用しないで接続しようとすると、以下のようにエラーが出力されて接続することができません。

{
    "errorType": "error",
    "errorMessage": "This RDS Proxy requires TLS connections",
    "code": "28000",
    "length": 67,
    "name": "error",
    "severity": "FATAL",
    "stack": [
        "error: This RDS Proxy requires TLS connections",
        "    at Em.parseErrorMessage (/node_modules/pg-protocol/src/parser.ts:369:69)",
        "    at Em.handlePacket (/node_modules/pg-protocol/src/parser.ts:188:21)",
        "    at Em.parse (/node_modules/pg-protocol/src/parser.ts:103:30)",
        "    at Socket.<anonymous> (/node_modules/pg-protocol/src/index.ts:7:48)",
        "    at Socket.emit (events.js:400:28)",
        "    at addChunk (internal/streams/readable.js:293:12)",
        "    at readableAddChunk (internal/streams/readable.js:267:9)",
        "    at Socket.Readable.push (internal/streams/readable.js:206:10)",
        "    at TCP.onStreamRead (internal/stream_base_commons.js:188:23)"
    ]
}

DBクラスター接続用のLambda関数の処理の説明

DBクラスター接続用のLambda関数で行っている処理は以下の通りです。

  • Secrets ManagerからDBクラスターのシークレットを取得
  • 取得したシークレットを利用してRDS Proxy経由でDBクラスターに接続
  • test_tableテーブルのレコードを表示
  • test_tableテーブルにレコードを追加
  • 再度test_tableテーブルのレコードを表示
  • DBクラスターとの接続のクローズ

実際のコードは以下の通りです。

./src/lambda/handlers/db-access.ts

import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
import { Client } from "pg";
import * as fs from "fs";

export const handler = async (): Promise<void | Error> => {
  // Get secret value
  const secretsManagerClient = new SecretsManagerClient({
    region: process.env.AWS_REGION!,
  });
  const getSecretValueCommand = new GetSecretValueCommand({
    SecretId: process.env.SECRET_ID,
  });
  const getSecretValueCommandResponse = await secretsManagerClient.send(
    getSecretValueCommand
  );
  const secret = JSON.parse(getSecretValueCommandResponse.SecretString!);

  // DB Client
  const dbClient = new Client({
    user: secret.username,
    host: process.env.PGHOST,
    database: secret.dbname,
    password: secret.password,
    port: secret.port,
    ssl: {
      rejectUnauthorized: false,
      cert: fs.readFileSync("global-bundle.pem").toString(),
    },
    // Also OK
    // ssl: true,
  });

  // DB Connect
  await dbClient.connect();

  // Query
  const beforeInsertQuery = await dbClient.query("SELECT * FROM test_table");
  console.log(beforeInsertQuery.rows);

  const insertQuery = await dbClient.query(
    "INSERT INTO test_table (name) VALUES ($1)",
    ["non-97"]
  );
  console.log(insertQuery.rows);

  const afterInsertQuery = await dbClient.query("SELECT * FROM test_table");
  console.log(afterInsertQuery.rows);

  // DB Connect Close
  await dbClient.end();

  return;
};

動作確認

それでは、npx cdk deployで各種リソースをデプロイします。

npx cdk deploy実行後にマネージメントコンソールを開くと、Aurora DBクラスターやRDS Proxy、DBクラスター接続用のLambda関数など各種リソースが正常に作成できていることを確認できました。

  • Aurora DBクラスター Aurora DBクラスター

  • RDS Proxy RDS Proxy

  • DBクラスター接続用のLambda関数 DBクラスター接続用のLambda関数

それでは、EC2インスタンスからAurora DBクラスターに接続します。

$ get_secrets_value=$(aws secretsmanager get-secret-value \
    --secret-id prd-db-cluster/AdminLoginInfo \
    --region us-east-1 \
    | jq -r .SecretString)

$ export PGHOST=db-proxy.proxy-cicjym7lykmq.us-east-1.rds.amazonaws.com
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=> \conninfo
You are connected to database "testDB" as user "postgresAdmin" on host "db-proxy.proxy-cicjym7lykmq.us-east-1.rds.amazonaws.com" (address "10.10.0.46") at port "5432".
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)

次にテスト用のテーブルを作成します。

testDB=> CREATE TABLE test_table (
    id SERIAL NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);
CREATE TABLE
testDB=>
testDB=> \dt
              List of relations
 Schema |    Name    | Type  |     Owner
--------+------------+-------+---------------
 public | test_table | table | postgresAdmin
(1 row)

testDB=>
testDB=> \d test_table
                                        Table "public.test_table"
   Column   |            Type             | Collation | Nullable |                Default
------------+-----------------------------+-----------+----------+----------------------------------------
 id         | integer                     |           | not null | nextval('test_table_id_seq'::regclass)
 name       | character varying(255)      |           | not null |
 created_at | timestamp without time zone |           | not null | CURRENT_TIMESTAMP
Indexes:
    "test_table_pkey" PRIMARY KEY, btree (id)

testDB=>

作成したテーブルにレコードを追加します。

testDB=> INSERT INTO test_table (name) VALUES ('non-97');
INSERT 0 1
testDB=>
testDB=> SELECT * FROM test_table;
 id |  name  |         created_at
----+--------+----------------------------
  1 | non-97 | 2022-04-04 17:41:21.988478
(1 row)

testDB=> INSERT INTO test_table (name) VALUES ('non-97');
INSERT 0 1
testDB=> SELECT * FROM test_table;
 id |  name  |         created_at
----+--------+----------------------------
  1 | non-97 | 2022-04-04 17:41:21.988478
  2 | non-97 | 2022-04-04 17:42:18.329333
(2 rows)

2件のレコードが追加されたことを確認できました。

それでは、DBクラスター接続用のLambda関数を実行します。

DBクラスター接続用のLambda関数を実行すると以下のようなログが出力されました。

START RequestId: 89c47b87-81fb-41a4-aaae-e241d3285403 Version: $LATEST
2022-04-04T09:07:54.409Z	89c47b87-81fb-41a4-aaae-e241d3285403	INFO	[
  { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z },
  { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z }
]
2022-04-04T09:07:54.466Z	89c47b87-81fb-41a4-aaae-e241d3285403	INFO	[]
2022-04-04T09:07:54.486Z	89c47b87-81fb-41a4-aaae-e241d3285403	INFO	[
  { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z },
  { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z },
  { id: 3, name: 'non-97', created_at: 2022-04-04T18:07:54.449Z }
]
END RequestId: 89c47b87-81fb-41a4-aaae-e241d3285403
REPORT RequestId: 89c47b87-81fb-41a4-aaae-e241d3285403	Duration: 1000.24 ms	Billed Duration: 1001 ms	Memory Size: 128 MB	Max Memory Used: 69 MB	Init Duration: 288.08 ms	
XRAY TraceId: 1-624ab568-318e78e3790e197c5b99ca95	SegmentId: 5ef91179409d5a94	Sampled: true

正しくレコードの表示、追加がされていますね。

念の為もう一度実行しましたが、以下のように正しくレコードの表示、追加が出来ています。

START RequestId: bf75845a-fc3d-41a8-b1e4-e0369833f14d Version: $LATEST
2022-04-04T11:06:47.487Z	bf75845a-fc3d-41a8-b1e4-e0369833f14d	INFO	[
  { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z },
  { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z },
  { id: 3, name: 'non-97', created_at: 2022-04-04T18:07:54.449Z }
]
2022-04-04T11:06:47.545Z	bf75845a-fc3d-41a8-b1e4-e0369833f14d	INFO	[]
2022-04-04T11:06:47.565Z	bf75845a-fc3d-41a8-b1e4-e0369833f14d	INFO	[
  { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z },
  { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z },
  { id: 3, name: 'non-97', created_at: 2022-04-04T18:07:54.449Z },
  { id: 4, name: 'non-97', created_at: 2022-04-04T20:06:47.527Z }
]
END RequestId: bf75845a-fc3d-41a8-b1e4-e0369833f14d
REPORT RequestId: bf75845a-fc3d-41a8-b1e4-e0369833f14d	Duration: 937.91 ms	Billed Duration: 938 ms	Memory Size: 128 MB	Max Memory Used: 69 MB	Init Duration: 244.94 ms	
XRAY TraceId: 1-624ad146-74b55a8e75775490061291a7	SegmentId: 1f11c84f47a696a9	Sampled: true

また、その際のRDS Proxyのログを確認すると以下のようになっていました。

1回目のDBクラスター接続用のLambda関数実行ログ

2022-04-04T09:07:54.176Z [INFO] [proxyEndpoint=default] [clientConnection=339077222] A new client connected from 10.10.0.38:37297.
2022-04-04T09:07:54.315Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] Received Startup Message: [username="postgresAdmin", database="testDB", protocolMajorVersion=3, protocolMinorVersion=0, sslEnabled=true]
2022-04-04T09:07:54.328Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] Proxy authentication with PostgreSQL native password authentication succeeded for user "postgresAdmin" with TLS on.
2022-04-04T09:07:54.349Z [INFO] [dbConnection=2685642450] A TCP connection was established from the proxy at 10.10.0.53:1319 to the database at 10.10.0.86:5432.
2022-04-04T09:07:54.391Z [DEBUG] [dbConnection=2685642450] The new database connection successfully authenticated with TLS on.
2022-04-04T09:07:54.395Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The client connection borrowed the database connection [dbConnection=2685642450] for the next query/transaction.
2022-04-04T09:07:54.398Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The database connection [dbConnection=2685642450] borrowed from the connection pool is being released to the connection pool.
2022-04-04T09:07:54.449Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The client connection borrowed the database connection [dbConnection=2685642450] for the next query/transaction.
2022-04-04T09:07:54.449Z [WARN] [proxyEndpoint=default] [clientConnection=339077222] The client session was pinned to the database connection [dbConnection=2685642450] for the remainder of the session. The proxy can't reuse this connection until the session ends. Reason: A parse message was detected.
2022-04-04T09:07:54.488Z [INFO] [proxyEndpoint=default] [clientConnection=339077222] The client connection closed. Reason: The client requested that the connection close.
2022-04-04T09:07:54.489Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The database connection [dbConnection=2685642450] borrowed from the connection pool is being released to the connection pool.

2回目のDBクラスター接続用のLambda関数実行ログ

2022-04-04T11:06:47.273Z [INFO] [proxyEndpoint=default] [clientConnection=2790996939] A new client connected from 10.10.0.61:13632.
2022-04-04T11:06:47.412Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] Received Startup Message: [username="postgresAdmin", database="testDB", protocolMajorVersion=3, protocolMinorVersion=0, sslEnabled=true]
2022-04-04T11:06:47.430Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] Proxy authentication with PostgreSQL native password authentication succeeded for user "postgresAdmin" with TLS on.
2022-04-04T11:06:47.450Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The client connection borrowed the database connection [dbConnection=3594132134] for the next query/transaction.
2022-04-04T11:06:47.456Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The database connection [dbConnection=3594132134] borrowed from the connection pool is being released to the connection pool.
2022-04-04T11:06:47.526Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The client connection borrowed the database connection [dbConnection=3594132134] for the next query/transaction.
2022-04-04T11:06:47.526Z [WARN] [proxyEndpoint=default] [clientConnection=2790996939] The client session was pinned to the database connection [dbConnection=3594132134] for the remainder of the session. The proxy can't reuse this connection until the session ends. Reason: A parse message was detected.
2022-04-04T11:06:47.567Z [INFO] [proxyEndpoint=default] [clientConnection=2790996939] The client connection closed. Reason: The client requested that the connection close.
2022-04-04T11:06:47.569Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The database connection [dbConnection=3594132134] borrowed from the connection pool is being released to the connection pool.

Lambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続するのは思ったより簡単

Lambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続してみました。

単純にDBクラスターに接続する程度であれば非常に簡単ですね。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!