0
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【AWS CDK】Lambda in VPCからRDSにアクセスする

Last updated at Posted at 2024-06-22

TL;DR

とりあえずソース全文

lib/cdk-sample-stack.ts
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as secretmanager from "aws-cdk-lib/aws-secretsmanager";
import * as rds from "aws-cdk-lib/aws-rds";
import { Construct } from "constructs";

const DB_NAME = "sampleDB";
const DB_USER = "sampleUser";
const DB_PORT = 3306;

export class CdkSampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC
    const vpc = new ec2.Vpc(this, "SampleVPC", {
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "isolatedSubnet",
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
      maxAzs: 2,
    });
    new ec2.InterfaceVpcEndpoint(this, "SecretmanagerEndPoint", {
      vpc,
      service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
    });

    // RDS & Proxy
    const rdsSecret = new secretmanager.Secret(this, "RDSSecret", {
      generateSecretString: {
        generateStringKey: "password",
        secretStringTemplate: `{"username": "${DB_USER}"}`,
        excludePunctuation: true,
      },
    });
    const rdsSecurityGroup = new ec2.SecurityGroup(this, "RDSSecurityGroup", {
      vpc,
    });
    const rdsProxySecurityGroup = new ec2.SecurityGroup(
      this,
      "RDSProxySecurityGroup",
      { vpc },
    );
    const rdsInstance = new rds.DatabaseInstance(this, "SampleRDS", {
      engine: rds.DatabaseInstanceEngine.MYSQL,
      databaseName: DB_NAME,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.SMALL,
      ),
      vpc,
      subnetGroup: new rds.SubnetGroup(this, "RDSSubnetGroup", {
        description: "subnetGroup",
        vpc,
        vpcSubnets: vpc.selectSubnets({
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        }),
      }),
      securityGroups: [rdsSecurityGroup],
      credentials: rds.Credentials.fromSecret(rdsSecret),
    });
    const rdsProxy = rdsInstance.addProxy("SampleRDSProxy", {
      secrets: [rdsSecret],
      vpc,
      securityGroups: [rdsProxySecurityGroup],
      requireTLS: false,
    });

    // Lambda
    const lambdaSecurityGroup = new ec2.SecurityGroup(
      this,
      "LambdaSecurityGroup",
      { vpc },
    );
    const libsLayer = new lambda.LayerVersion(this, "SampleLayer", {
      code: lambda.Code.fromAsset("src/libs"),
      compatibleRuntimes: [lambda.Runtime.PYTHON_3_9],
    });
    const lambdaFunction = new lambda.Function(this, "SampleLambda", {
      runtime: lambda.Runtime.PYTHON_3_9,
      code: lambda.Code.fromAsset("src/functions"),
      handler: "sample.lambda_handler",
      environment: {
        SECRET_ARN: rdsSecret.secretArn,
        PROXY_ENDPOINT: rdsProxy.endpoint,
      },
      layers: [libsLayer],
      vpc,
      securityGroups: [lambdaSecurityGroup],
    });
    rdsSecret.grantRead(lambdaFunction);
    rdsInstance.grantConnect(lambdaFunction, DB_USER);

    // SecurityGroup rules
    rdsProxySecurityGroup.addIngressRule(
      lambdaSecurityGroup,
      ec2.Port.tcp(DB_PORT),
      "Allow from lambda",
    );
    rdsSecurityGroup.addIngressRule(
      rdsProxySecurityGroup,
      ec2.Port.tcp(DB_PORT),
      "Allow from RDSProxy",
    );
  }
}
src/functions/sample.py
import json
import logging
import os
import sys

import boto3
import pymysql

# get secrets
secret_manager = boto3.client("secretsmanager")
secret_arn = os.getenv("SECRET_ARN")
secret = secret_manager.get_secret_value(SecretId=secret_arn)
secret_values = json.loads(secret["SecretString"])

# credentials
user_name = secret_values["username"]
password = secret_values["password"]
rds_proxy_host = os.getenv("PROXY_ENDPOINT")
db_name = secret_values["dbname"]

logger = logging.getLogger()
logger.setLevel(logging.INFO)

try:
    conn = pymysql.connect(
        host=rds_proxy_host,
        user=user_name,
        passwd=password,
        db=db_name,
        connect_timeout=5,
    )
except pymysql.MySQLError as e:
    logger.error("ERROR: Unexpected error: Could not connect to MySQL instance.")
    logger.error(e)
    sys.exit(1)

logger.info("SUCCESS: Connection to RDS for MySQL instance succeeded")


def lambda_handler(event, context):
    cust_id = "1"
    name = "custNameSample"

    sql_string = f"insert into Customer (CustID, Name) values(%s, %s)"

    with conn.cursor() as cur:
        cur.execute(
            "create table if not exists Customer ( CustID  int NOT NULL, Name varchar(255) NOT NULL, PRIMARY KEY (CustID))"
        )
        cur.execute(sql_string, (cust_id, name))
        conn.commit()

        cur.execute("select * from Customer")
        result = cur.fetchall()
        logger.info(f"{result=}")
    conn.commit()

lambdaの実装は公式のサンプルから借用しました。

VPC

const vpc = new ec2.Vpc(this, "SampleVPC", {
  ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
  subnetConfiguration: [
    {
      cidrMask: 24,
      name: "isolatedSubnet",
      subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
    },
  ],
  maxAzs: 2, // RDSProxyはsubnetが2つ必要
});
new ec2.InterfaceVpcEndpoint(this, "SecretmanagerEndPoint", {
  vpc,
  service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
});
  • RDSProxyで指定するサブネットグループは2つ以上のサブネットを含む必要があるので2つ以上作成する
  • 今回RDSのクレデンシャルをsecretsManagerで管理するので、VPCにエンドポイントをアタッチする

RDS & RDSProxy

const rdsSecret = new secretmanager.Secret(this, "RDSSecret", {
  generateSecretString: {
    generateStringKey: "password",
    secretStringTemplate: `{"username": "${DB_USER}"}`,
    excludePunctuation: true, // 自動生成されるパスワードから句読点を除外
  },
});
const rdsSecurityGroup = new ec2.SecurityGroup(this, "RDSSecurityGroup", {
  vpc,
});
const rdsProxySecurityGroup = new ec2.SecurityGroup(
  this,
  "RDSProxySecurityGroup",
  { vpc },
);
const rdsInstance = new rds.DatabaseInstance(this, "SampleRDS", {
  engine: rds.DatabaseInstanceEngine.MYSQL,
  databaseName: DB_NAME,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.SMALL,
  ),
  vpc,
  subnetGroup: new rds.SubnetGroup(this, "RDSSubnetGroup", {
    description: "subnetGroup",
    vpc,
    vpcSubnets: vpc.selectSubnets({
      subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
    }),
  }),
  securityGroups: [rdsSecurityGroup],
  credentials: rds.Credentials.fromSecret(rdsSecret),
});
const rdsProxy = rdsInstance.addProxy("SampleRDSProxy", {
  secrets: [rdsSecret], // RDSのシークレットを指定する
  vpc,
  securityGroups: [rdsProxySecurityGroup],
  requireTLS: false,
});
  • RDSとRDSProxy用のセキュリティグループを作成する
  • RDSProxyのシークレットは新規作成せずに、RDSのシークレットを指定する

Lambda

const lambdaSecurityGroup = new ec2.SecurityGroup(this, "LambdaSecurityGroup", {
  vpc,
});
const libsLayer = new lambda.LayerVersion(this, "SampleLayer", {
  code: lambda.Code.fromAsset("src/libs"),
  compatibleRuntimes: [lambda.Runtime.PYTHON_3_9],
});
const lambdaFunction = new lambda.Function(this, "SampleLambda", {
  runtime: lambda.Runtime.PYTHON_3_9,
  code: lambda.Code.fromAsset("src/functions"),
  handler: "sample.lambda_handler",
  environment: {
    SECRET_ARN: rdsSecret.secretArn,
    PROXY_ENDPOINT: rdsProxy.endpoint, // LambdaはRDSのhostではなくRDSProxyのエンドポイントを参照する
  },
  layers: [libsLayer],
  vpc,
  securityGroups: [lambdaSecurityGroup],
});
rdsSecret.grantRead(lambdaFunction);
rdsInstance.grantConnect(lambdaFunction, DB_USER); // RDSProxyではなくRDSでgrantする
  • 環境変数でRDSProxyのエンドポイントを渡す
    • secretsManagerでRDSのクレデンシャルを作成するとhostというキーでhost名も管理できるがRDSProxyを経由する場合はLambdaからは参照する必要なし
  • grantするのはRDSProxyではなくRDS
    • 個人的にRDSProxyじゃないんかいて思う

SecutiryGroup

rdsProxySecurityGroup.addIngressRule(
  lambdaSecurityGroup,
  ec2.Port.tcp(DB_PORT),
  "Allow from lambda",
);
rdsSecurityGroup.addIngressRule(
  rdsProxySecurityGroup,
  ec2.Port.tcp(DB_PORT),
  "Allow from RDSProxy",
);
  • RDSProxyセキュリティグループのインバウンドルールでLambdaのセキュリティグループを指定する
  • RDSセキュリティグループのインバウンドルールでRDSProxyのセキュリティグループを指定する

動作確認してみる

Lambdaのtestから実行

[INFO]	2024-06-22T22:12:54.144Z		SUCCESS: Connection to RDS for MySQL instance succeeded
START RequestId: fad5086b-1c70-4e0e-b002-d7deae86a67e Version: $LATEST
[INFO]	2024-06-22T22:12:54.169Z	fad5086b-1c70-4e0e-b002-d7deae86a67e	result=((1, 'custNameSample'))
END RequestId: fad5086b-1c70-4e0e-b002-d7deae86a67e
REPORT RequestId: fad5086b-1c70-4e0e-b002-d7deae86a67e	Duration: 26.82 ms	Billed Duration: 27 ms	Memory Size: 128 MB	Max Memory Used: 72 MB	Init Duration: 518.03 ms

RDSへの接続とレコードの追加が確認できました!:tada:

まとめ

  • VPC
    • RDSProxyはサブネットを2つ以上必要とするので2つ以上作成する
    • secretsManagerへのエンドポイントをアタッチする
  • RDS
    • RDSインスタンス、RDSProxyそれぞれにセキュリティグループを作成する
    • RDSProxyのシークレットはRDSのシークレットを指定する
  • Lambda
    • DB接続時、hostはRDSのhostではなくRDSProxyのエンドポイントを指定する
    • Lambda用のセキュリティグループを作成する
    • secretsManagerでgrantRead()する
    • RDSインスタンスでgrantConnect()する
  • セキュリティグループ
    • RDSProxyセキュリティグループのインバウンドルールでLambdaのセキュリティグループを指定する
    • RDSセキュリティグループのインバウンドルールでRDSProxyのセキュリティグループを指定する
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0