[備忘録] AWS CDKでRDS,RDS Proxy,Lambda を構築

AWS CDK を学習していく過程を記録する。

# はじめに
[[備忘録]AWS CDKでAPI Gateway, Lambda を構築](https://qiita.com/m-chan/items/0e3869afd193b92dd79a)にて、API GatewayをトリガーにLambdaを実行することで、LambdaがRestAPIとして外部ネットワークから利用可能になることを確認した。  
今回は、RDS、RDS Proxy を構築し、Lambda の中の処理を書き換えて、API Gateway から Lambda 経由で mysql に接続できることを確認したい。

### RDS とは
> Amazon Relational Database Service (Amazon RDS) は、AWS クラウド でリレーショナルデータベースを簡単にセットアップし、運用し、スケーリングすることのできるウェブサービスです。

つまり、サーバの設定やOSのインストールなどすることなくすぐにDB構築が可能なサービスであり、RDSでは mysql、Oracle Database、Microsoft SQL Server、PostgreSQL など一般的なDB管理システム(RDBMS）を使用することができる。

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

つまり、アプリケーションと RDS の間でアクセスを中継し、DBへの接続を効率的に管理してくれるサービスと言える。

# 実施したこと
1. RDSの作成
1. Lambdaの作成

### 1. RDSの作成
```app-stack.ts
import { BastionHostLinux, InstanceClass, InstanceSize, InstanceType, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, Port, SecurityGroup, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { Credentials, DatabaseInstance, DatabaseInstanceEngine, MysqlEngineVersion, ParameterGroup } from 'aws-cdk-lib/aws-rds';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';

// vpc
    const vpc = new Vpc(this, 'AppVpc', {
      cidr: '10.0.0.0/16',
      vpcName: 'app-vpc',
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PublicSubnet',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'PrivateSubnet',
          subnetType: SubnetType.PRIVATE_ISOLATED,
        },
      ],
      maxAzs: 2
    });

    const bastionGroup = new SecurityGroup(this, 'Bastion to DB', {
      vpc: vpc
    });
    const lambdaToRDSProxyGroup = new SecurityGroup(this, 'Lambda to RDSProxy', {
      vpc: vpc
    });
    const dbConnectionGroup = new SecurityGroup(this, 'RDSProxy to DB', {
      vpc: vpc
    });
    
    dbConnectionGroup.addIngressRule(
      dbConnectionGroup,
      Port.tcp(3306),
      'allow db connection'
    );
    dbConnectionGroup.addIngressRule(
      lambdaToRDSProxyGroup,
      Port.tcp(3306),
      'allow lambda connection'
    );
    dbConnectionGroup.addIngressRule(
      bastionGroup,
      Port.tcp(3306),
      'allow bastion connection'
    );

    // bastion server
    const host = new BastionHostLinux(this, 'AppBastionHost', {
      vpc: vpc,
      instanceType: InstanceType.of(
        InstanceClass.T2,
        InstanceSize.MICRO
      ),
      securityGroup: bastionGroup,
      subnetSelection: {
        subnetType: SubnetType.PUBLIC
      },
    });
    host.instance.addUserData("yum -y update", "yum install -y mysql jq");

    // credential about RDS
    const databaseCredentialsSecret = new Secret(this, 'AppDbCredentialsSecret', {
      secretName: id + '-rds-credentials',
      generateSecretString: {
        secretStringTemplate: JSON.stringify({
          username: 'syscdk',
        }),
        excludePunctuation: true,
        includeSpace: false,
        generateStringKey: 'password',
      },
    });

    // vpc endpoint to access from lambda to secret manager
    new InterfaceVpcEndpoint(this, 'SecretManagerVpcEndpoint', {
      vpc: vpc,
      service: InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
    });

    // rds
    const rdsInstance = new DatabaseInstance(this, 'DBInstance', {
      engine: DatabaseInstanceEngine.mysql({
        version: MysqlEngineVersion.VER_8_0_33
      }),
      credentials: Credentials.fromSecret(databaseCredentialsSecret),
      instanceType: InstanceType.of(
        InstanceClass.T2,
        InstanceSize.MICRO
      ),
      vpc: vpc,
      vpcSubnets: {
        subnetType: SubnetType.PRIVATE_ISOLATED
      },
      securityGroups: [dbConnectionGroup],
      removalPolicy: RemovalPolicy.DESTROY,
      deletionProtection: false,
      parameterGroup: new ParameterGroup(this, 'ParameterGroup', {
        engine: DatabaseInstanceEngine.mysql({
          version: MysqlEngineVersion.VER_8_0_33
        }),
        parameters: {
          character_set_client: "utf8mb4",
          character_set_server: "utf8mb4",
        },
      }),
    });

    // rds proxy
    const proxy = rdsInstance.addProxy(id + '-proxy', {
      secrets: [databaseCredentialsSecret],
      debugLogging: true,
      vpc: vpc,
      securityGroups: [dbConnectionGroup],
    });
```

デプロイし、早速[AWS System Manager](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/what-is-systems-manager.html)を使用して踏み台サーバー経由でmysqlへの接続を試みた。  
すると Session Manager プラグイン がないため踏み台にログインできなかったので、[[備忘録] macOS で AWS Session Manager(SSM) を使ってLinuxサーバーに接続できるようになるまで](https://qiita.com/m-chan/items/9811aa0f31aabfa93a31)にまとめた手順で Session Manager プラグイン をインストールした。
```
% aws ssm start-session --target {踏み台サーバーのID}
Starting session with SessionId: *****
sh-4.2$ 
```

インストール後は無事に踏み台サーバーにログインでき、さらにSecrets Manager にある接続情報でmysqlへの接続も成功。  
動作確認のためのテーブル作成まで行った。
```
mysql -usyscdk -p -h ${DBのエンドポイント}
MySQL [(none)]> CREATE DATABASE app;
MySQL [(none)]> USE app;
MySQL [app]> CREATE TABLE test(id int primary key auto_increment, value int);
MySQL [app]> INSERT INTO test (value) VALUES (100);
MySQL [app]> INSERT INTO test (value) VALUES (200);
MySQL [app]> INSERT INTO test (value) VALUES (300);
MySQL [app]> INSERT INTO test (value) VALUES (400);
MySQL [app]> SELECT * FROM app.test;
+----+-------+
| id | value |
+----+-------+
|  1 |   100 |
|  2 |   200 |
|  3 |   300 |
|  4 |   400 |
+----+-------+
4 rows in set (0.00 sec)
```

### 2. Lambdaの作成
app-lambda.ts を下記のように書き換えた。

```app-lambda.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import * as AWS from 'aws-sdk'
import * as mysql from 'mysql2/promise';

const cert =`
-----BEGIN CERTIFICATE-----
クライアント証明書
-----END CERTIFICATE-----
`.trim()

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {

  const RESPONSE_HEADERS = {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,access-token",
  };

  const secretsManager = new AWS.SecretsManager({
    region: "ap-northeast-1",
  })
  const response = await secretsManager.getSecretValue({
    SecretId: "AppStack-rds-credentials",
  }).promise()

  const {host, username, password} = JSON.parse(response.SecretString ?? '')
  console.log(`username = ${username}`);
  const connection = await mysql.createConnection({
    host: process.env.PROXY_ENDPOINT,
    database: 'app',
    user: username,
    password: password,
    ssl: {
        cert: cert
    },
  }); 

  let result = await connection.query("select * from test")

  return {
    statusCode: 200,
    headers: RESPONSE_HEADERS,
    body: JSON.stringify(result[0])
  };
};
```

デプロイしlambdaを実行したところ、期待通り test テーブルに保存した内容が返ってきた。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280374/6a5e112d-cff2-6c1a-70ea-411345600fd5.png)


# 最後に
AWS CDKでRDS、RDS Proxy を構築し、Lambda から mysql に接続できることを確認できた。
参考にした文献を記す。
- https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/Welcome.html
- https://aws.amazon.com/jp/rds/proxy/
- https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/services-rds-tutorial.html
- https://dev.classmethod.jp/articles/do-you-really-need-rds-proxy/
- https://tech-blog.s-yoshiki.com/entry/266
- https://www.wakuwakubank.com/posts/402-mysql-commandline/

- https://qiita.com/hirai-11/items/b4fa016bb5db1af2cb1d