📑

RDS Proxy 経由で RDS に接続する

2022/04/29に公開

はじめに

Lambda を利用しているときに RDS を使いたいとなった場合、RDS Proxy を今なら選択するかなと思いますが、最初その構成を設定するのに一苦労したので記事にまとめようと思います。全体の構成としては以下の通りです。

API Gateway をパブリックインターフェースに置いて、統合してある Lambda をキックします。Lambda は、RDS Proxy、RDS と同一 VPC 内に設置してあります。RDS に接続するための秘匿情報については、Secrets Manager で管理するようにしますが、VPC 外にあるので、VPC エンドポイントを通して取得しに行きます。

ネットワーク周りの設定

VPC、サブネット、ルートテーブル

今回は以下のような構成で作成します。

細かい入力項目はこちら
作成するリソース
  • VPCのみ
  • VPC、サブネットなど

名前タグの自動生成

デフォルトのまま

IPv4 CIDRブロック

10.0.0.0/16

IPv6 CIDR ブロック
  • IPv6 CIDR ブロックなし
  • Amazon 提供の IPv6 CIDR ブロック

テナンシー

デフォルト

アベイラビリティーゾーン (AZ)

2個
カスタマイズしない

パブリックサブネットの数

0個
※ API Gateway は VPC 内のリソースにアクセスできるのでパブリックサブネットは不要です。

プライベートサブネットの数

2個
カスタマイズしない

NAT ゲートウェイ ($)

なし

VPC エンドポイント

なし

DNS オプション
  • DNS ホスト名を有効化
  • DNS 解決を有効化

こんな感じで VPC ワークフローが作成できたら完了です。
VPC の作成する際にウィザードが新しくなっており、めちゃくちゃ作成しやすくなっていてびっくりしました。

セキュリティグループ

Lambda

API Gateway と統合することでアクセス可能になるので、ルールは中身は空で作成します。

RDS Proxy

Lambda の Security Group からの 3306 ポートのインバウンドだけ受け付けます。

タイプ: カスタムTCP
ポート範囲: 3306
ソース: sg-xxxxxxxxx(Lambda のSG)

RDS

RDS Proxy の Security Group からの 3306 ポートのインバウンドだけ受け付けます。

タイプ: カスタムTCP
ポート範囲: 3306
ソース: sg-yyyyyyyyy(RDS Proxy のSG)

Secrets Manager

Lambda の Security Group からの HTTPS インバウンドだけを受け付けます。

タイプ: HTTPS
ポート範囲: 443
ソース: sg-xxxxxxxxx(Lambda のSG)

Secrets Manager 用の VPC エンドポイント

サービス名: com.amazonaws.ap-northeast-1.secretsmanager
エンドポイントタイプ: interface
VPC: 上記で作成した、VPC
セキュリティグループ: Secrets Manager 用のセキュリティグループ
サブネット: 上記で作成した、2つのプライベートサブネット
プライベートDNS: 有効

Lambda の作成

Lambda を作成しましょう。この記事ではランタイムに Python3.8 を指定していますが、お好きな言語を選択してください。
先程、VPC 周りを作成したので、それを設定します。

また、Lambda が SecretsManager にアクセスできる必要があるので、SecretsManagerReadWrite のポリシーを Lambda に付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface",
                "ec2:AssignPrivateIpAddresses",
                "ec2:UnassignPrivateIpAddresses"
            ],
            "Resource": "*"
        }
    ]
}

ソースコードは後で編集するので、一旦デフォルトのまま次に進みます。

API Gateway の作成

色々と設定が楽ちんな HTTP API で構築していきます。
今回は Lambda は1つしか作成しない予定なので、統合も合わせて設定しちゃいます。

先ほど作成した Lambda を統合に設定します。

Lambda は1つしかないので、ルート設定は以下で問題ないでしょう。

メソッド: GET
リソースパス: /
統合ターゲット: (先程作成した Lambda の名前)

ステージはデフォルトかつ自動デプロイを有効化します。
これで作成します。

疎通確認テスト

ここで、一旦疎通確認しておきます。
API Gateway によって、作成されたエンドポイントにアクセスしてみましょう。

Hello from Lambda! が返って来れば、順調に作成できています。

$ curl https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
"Hello from Lambda!"

RDS の作成

サブネットグループの作成

データベースの作成の前に、サブネットグループの作成をしときます。
先程作成した、VPC、2つのプライベートサブネット で構成されるサブネットグループを先に作成してください。

クラスター & データベースの作成

今回はライブラリに、pymysql を使いたいので DBは Aurora MySQL を選びます。
インスタンス設定のところですが、RDS Proxy はサーバーレス未対応なので、インスタンスクラスを選択します。

Aurora サーバーレスクラスターでは RDS Proxy を使用できません。
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html#:~:text=Aurora サーバーレスクラスターでは RDS Proxy を使用できません。

また、接続設定のところで、事前に作成した VPC や セキュリティグループを選択します。
データベース認証は、パスワード認証 にします。

追加設定のところで、データベースも作成します。今回はDB名を test_db として作成しています。

Secrets Manager の作成

ご丁寧に、サンプルコードが置いてあったので、Lambda に貼り付けて実行してみましょう。
コメントは邪魔なので消すのと、エラーハンドリングは簡略化します。

import boto3
import base64
from botocore.exceptions import ClientError

def lambda_handler(event, context):

    secret_name = "YourSecretName" # 書き換えてください
    region_name = "ap-northeast-1"

    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except Exception as e:
        raise e
    else:
        if 'SecretString' in get_secret_value_response:
            return get_secret_value_response['SecretString']
        else:
            return base64.b64decode(get_secret_value_response['SecretBinary'])

実行結果

結果が、timed out になってしまう場合は、IAM周りの権限設定が不足しているか、ネットワーク(特にセキュリティグループ)の設定を見直してください。

$ curl https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com | jp
{
  "username": "admin",
  "password": "xxxxxxxxxxxxxxx",
  "engine": "mysql",
  "host": "database-1.cluster-xxxxxxxxxx.ap-northeast-1.rds.amazonaws.com",
  "port": 3306,
  "dbname": "test_db",
  "dbClusterIdentifier": "database-1"
}

RDS Proxy の作成

続いて、RDS Proxy を作成していきます。
ターゲットグループ と、接続 の設定に気をつけてください。

ターゲットグループ

  • データベースは先程作成したデータベースを指定します
  • リーダーエンドポイントは今回はテストなので不要です

接続

  • 作成したシークレットを指定します
  • IAM ロールを作成します
  • IAM 認証は無効です
  • サブネットは最初に作成した2つのプライベートサブネットを指定します
  • RDS Proxy 用のセキュリティグループを指定します

Lambda を編集

pymysql をレイヤーに追加

pymysql は、組み込みのライブラリではないため、レイヤーとして追加する必要があります。
他の SQL ライブラリを使ってもらっても問題ありません。

AWS CloudShell を開きます。

$ sudo amazon-linux-extras install python3.8 -y
$ sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1
$ python3 -V
Python 3.8.5

$ curl -O https://bootstrap.pypa.io/get-pip.py
$ python3 get-pip.py

$ pip install pymysql -t python
$ zip -r pymysql.zip python

$ aws s3 cp pymysql.zip s3://${input-bucket-name}/ # bucket名は要編集

あとは、zip をアップロードするなりして、Lambda 側に追加するだけです。

コード

接続する場合は、以下のようになります。

try:
    get_secret_value_response = client.get_secret_value(
        SecretId=secret_name
    )
except Exception as e:
    raise e
else:
    secret_info = json.loads(get_secret_value_response["SecretString"])

# DB操作
connect = pymysql.connect(
    # Proxy のエンドポイントを指定するのを忘れないでください
    host="proxy.proxy-xxxxx.ap-northeast-1.rds.amazonaws.com",
    user=secret_info["username"],
    passwd=secret_info["password"],
    db=secret_info["dbname"],
    connect_timeout=5,
    cursorclass=pymysql.cursors.DictCursor,
)

あとは、SQL 文を実行するだけです。

疎通確認

RDS Proxy のモニタリングを見てみると、クエリーが来ているのが確認できます。

あと、DB のメトリクスもちゃんと反応しているのが確認できたら、疎通完了です。

お疲れ様でした。作成経験はあるのですが、またネットワーク周りで詰まってしまいました。
ネットワーク難しい...

あと、zenn の マークダウン に ラジオボタンが欲しいので、catnose さんよろしくお願いします。

GitHubで編集を提案

Discussion