CDK Patternsを参考にしてRDS Proxyを利用したAPIをデプロイしてみた
こんにちは、CX事業本部のうらわです。
弊社ブログ記事でも紹介しているAWS CDKのパターン集「CDK Patterns」は、CDKの実装例として非常に参考になります。
本記事ではパターンの一つ「The RDS Proxy」に少し手を加えて(個人的に満足のいく)RDS Proxyの検証環境を構築してみたのでご紹介します。
前提
下記環境で実装しています。CDKはTypeScriptで実装しています。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.6 BuildVersion: 19G2021 $ node -v v12.18.3 $ npm -v 6.14.6 $ cdk --version 1.70.0 (build c145314)
本記事記載のコードは以下のリポジトリに格納してあります。
CDK Patternsのサンプルコードと異なる点
本家のThe RDS Proxyのサンプルコードとは以下の点が異なります。
- CDKのバージョンを本記事時点の最新バージョンに更新
- 踏み台サーバを利用
- Lambda関数をGoで実装
1については、サンプルコードはCDKのバージョンが1.59.0です。本記事では記事執筆時点の最新バージョン1.70.0を使用しています。
2については、サンプルコードのLambda関数は1つの関数内でデータベース作成・テーブル作成・レコード作成を実施しています。 検証目的のコードなので仕方ありませんがあまり実用的ではないコードだと感じたため、本記事では踏み台サーバ経由でRDSに接続し事前にデータベースやテーブルを作成します。
3については、2の理由からサンプルのLambda関数コードは使わず一から書く必要があったため、個人的な勉強も兼ねてGoで書いてみました。
Lambda関数の実装
booksテーブルの全件を返却するGET /books API
を想定して実装しました。
処理は以下のような流れです。
- Secret ManagerからMySQLに接続するためSecret Managerから認証情報を取得
- TLSで接続するためCA証明書を登録
- MySQLに接続しSELECT文でbooksテーブルの全件取得
2については以下を参考にしました。
AmazonRootCA1.pem
はThe RDS Proxyのサンプルコードと一緒に格納してあるファイルを流用するか、以下のページのリンクから取得できます。
https://www.amazontrust.com/repository/AmazonRootCA1.pem
package main import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/secretsmanager" "database/sql" "github.com/go-sql-driver/mysql" ) type UserInfo struct { UserName string `json:"username"` Password string `json:"password"` } type Book struct { Id int Name string Price int } func connect() (*sql.DB, error) { svc := secretsmanager.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1")) input := &secretsmanager.GetSecretValueInput{ SecretId: aws.String(os.Getenv("RDS_SECRET_NAME")), } result, err := svc.GetSecretValue(input) if err != nil { return nil, err } // Secret Managerから認証情報を取得 var userInfo UserInfo secrets := *result.SecretString json.Unmarshal([]byte(secrets), &userInfo) // CA証明書の設定 rootCertPool := x509.NewCertPool() absPath, _ := filepath.Abs("./cert/AmazonRootCA1.pem") pem, err := ioutil.ReadFile(absPath) if err != nil { return nil, err } if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { fmt.Println("[ERROR]", "Fialed to append PEM") } mysql.RegisterTLSConfig("custom", &tls.Config{ ClientCAs: rootCertPool, }) // MySQLに接続 dsn := fmt.Sprintf( "%s:%s@tcp(%s:%s)/rds_proxy_go?charset=utf8mb4&parseTime=True&loc=Local&tls=custom", userInfo.UserName, userInfo.Password, os.Getenv("PROXY_ENDPOINT"), "3306", ) db, err := sql.Open("mysql", dsn) if err != nil { return nil, err } return db, nil } func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { jsonReq, _ := json.Marshal(request) fmt.Println(string(jsonReq)) db, err := connect() if err != nil { fmt.Println("[ERROR]", err) return events.APIGatewayProxyResponse{Body: "Error!", StatusCode: 500}, nil } defer db.Close() rows, err := db.Query("SELECT * FROM books") if err != nil { fmt.Println("[ERROR]", err) return events.APIGatewayProxyResponse{Body: "Error!", StatusCode: 500}, nil } defer rows.Close() var books []Book for rows.Next() { var book Book err := rows.Scan(&book.Id, &book.Name, &book.Price) if err != nil { fmt.Println("[ERROR]", err) return events.APIGatewayProxyResponse{Body: "Error!", StatusCode: 500}, nil } books = append(books, book) } jsonBooks, _ := json.Marshal(books) return events.APIGatewayProxyResponse{Body: string(jsonBooks), StatusCode: 200}, nil } func main() { lambda.Start(handleRequest) }
ビルドしておきます。
GOARCH=amd64 GOOS=linux go build -o main main.go
CDKによる環境構築
CDKで EC2 / RDS / RDS Proxy / API Gateway / Lambda をデプロイします。 大部分をThe RDS Proxyのサンプルコードを参考にしています。 踏み台サーバ関連のコードは新規追加しています。
import * as cdk from '@aws-cdk/core'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import * as apigw from '@aws-cdk/aws-apigateway'; import * as rds from '@aws-cdk/aws-rds'; import * as secrets from '@aws-cdk/aws-secretsmanager'; export class RdsProxyGoStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // パブリックサブネットとプライベートサブネットを用意 // NAT Gatewayは利用しない const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2, natGateways: 0, cidr: '10.1.0.0/16', subnetConfiguration: [ { name: 'public', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, { name: 'private', subnetType: ec2.SubnetType.ISOLATED, cidrMask: 24, }, ], }); const bastionGroup = new ec2.SecurityGroup( this, 'Bastion to DB Connection', { vpc, } ); const lambdaToRDSProxyGroup = new ec2.SecurityGroup( this, 'Lambda to RDS Proxy Connection', { vpc, } ); const dbConnectionGroup = new ec2.SecurityGroup( this, 'Proxy to DB Connection', { vpc, } ); dbConnectionGroup.addIngressRule( dbConnectionGroup, ec2.Port.tcp(3306), 'allow db connection' ); dbConnectionGroup.addIngressRule( lambdaToRDSProxyGroup, ec2.Port.tcp(3306), 'allow lambda connection' ); dbConnectionGroup.addIngressRule( bastionGroup, ec2.Port.tcp(3306), 'allow bastion connection' ); // パブリックサブネットに踏み台サーバを配置する const host = new ec2.BastionHostLinux(this, 'BastionHost', { vpc, instanceType: ec2.InstanceType.of( ec2.InstanceClass.T2, ec2.InstanceSize.MICRO ), securityGroup: bastionGroup, subnetSelection: { subnetType: ec2.SubnetType.PUBLIC, }, }); // 踏み台サーバにmysqlとjqをインストールしておく host.instance.addUserData('yum -y update', 'yum install -y mysql jq'); // RDSの認証情報 const databaseCredentialsSecret = new secrets.Secret( this, 'DBCredentialsSecret', { secretName: id + '-rds-credentials', generateSecretString: { secretStringTemplate: JSON.stringify({ username: 'syscdk', }), excludePunctuation: true, includeSpace: false, generateStringKey: 'password', }, } ); // Lambda関数からSecret ManagerにアクセスするためのVPCエンドポイント new ec2.InterfaceVpcEndpoint(this, 'SecretManagerVpcEndpoint', { vpc: vpc, service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER, }); const rdsInstance = new rds.DatabaseInstance(this, 'DBInstance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_5_7_30, }), credentials: rds.Credentials.fromSecret(databaseCredentialsSecret), instanceType: ec2.InstanceType.of( ec2.InstanceClass.T2, ec2.InstanceSize.MICRO ), vpc, vpcSubnets: { subnetType: ec2.SubnetType.ISOLATED, }, securityGroups: [dbConnectionGroup], removalPolicy: cdk.RemovalPolicy.DESTROY, deletionProtection: false, parameterGroup: new rds.ParameterGroup(this, 'ParameterGroup', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_5_7_30, }), parameters: { character_set_client: 'utf8mb4', character_set_server: 'utf8mb4', }, }), }); const proxy = rdsInstance.addProxy(id + '-proxy', { secrets: [databaseCredentialsSecret], debugLogging: true, vpc, securityGroups: [dbConnectionGroup], }); const rdsLambda = new lambda.Function(this, 'RdsProxyHandler', { runtime: lambda.Runtime.GO_1_X, code: lambda.Code.asset('lambda'), handler: 'main', vpc: vpc, securityGroups: [lambdaToRDSProxyGroup], environment: { PROXY_ENDPOINT: proxy.endpoint, RDS_SECRET_NAME: id + '-rds-credentials', }, }); // 認証情報へのアクセス許可 databaseCredentialsSecret.grantRead(rdsLambda); databaseCredentialsSecret.grantRead(host); const restApi = new apigw.RestApi(this, 'RestApi', { restApiName: 'rds-proxy-go', deployOptions: { stageName: 'dev', }, }); const rdsLambdaIntegration = new apigw.LambdaIntegration(rdsLambda); const booksResource = restApi.root.addResource('books'); booksResource.addMethod('GET', rdsLambdaIntegration); } }
デプロイは結構時間がかかります。
npm run cdk deploy
検証用データベースの準備
CDKによるデプロイが完了したら、検証用のデータベース等を作成します。
Session Managerで踏み台サーバにアクセス
今回はAWS CLIを使用します。ローカルマシンに以下のプラグインをインストールしておく必要があります。
準備ができたら、踏み台サーバのインスタンスIDを指定してセッションを開始します。
aws ssm start-session --target i-XXXXXXXXXXXXXXXX
※ Session Managerのアクセス制御について
CDKのBastionHostLinuxコンストラクトは以下のリンクに記載のあるようなSession Managerでのアクセスに必要な設定を自動でしてくれています。
AWS CLIでSession Managerを使用するために必要なIAMポリシーは下記リンクを参照ください。本記事ではAdministratorAccessのIAMポリシーが付与されているIAMロールを利用しています。
MySQLに接続
AWS CLIを用いてSecret Managerからユーザー名やパスワードを取得します。 以下のコマンドを実行して変数にセットします。
secret=$(aws secretsmanager get-secret-value --region ap-northeast-1 --secret-id RdsProxyGoStack-rds-credentials | jq .SecretString | jq fromjson) user=$(echo $secret | jq -r .username) password=$(echo $secret | jq -r .password) endpoint=$(echo $secret | jq -r .host) port=$(echo $secret | jq -r .port)
※ これらのコマンドは以下のAWS Secret Managerのワークショップを参考にしています。
https://secrets-manager.awssecworkshops.com/RDSFargate/RDS/
データベース・テーブル・レコードの作成
必要な情報が準備できたら、以下のコマンドでMySQLに接続しSQLを発行します。
mysql -h $endpoint -P $port -u $user -p$password # SQL CREATE DATABASE rds_proxy_go; USE rds_proxy_go; CREATE TABLE books(id int primary key auto_increment, name text, price int); INSERT INTO books (name, price) VALUES ('思い出の本', 100); INSERT INTO books (name, price) VALUES ('AWS Database Book', 200000); INSERT INTO books (name, price) VALUES ('日記帳12345', 3000000); INSERT INTO books (name, price) VALUES ('あいうえお', 12345);
SELECT文で全件取得してみます。
MySQL [rds_proxy_go]> select * from books; +----+-------------------+---------+ | id | name | price | +----+-------------------+---------+ | 1 | 思い出の本 | 100 | | 2 | AWS Database Book | 200000 | | 3 | 日記帳12345 | 3000000 | | 4 | あいうえお | 12345 | +----+-------------------+---------+ 4 rows in set (0.01 sec)
以上で準備は完了です。
books APIをたたいてみる
API GatewayのエンドポイントはCDKによるデプロイ完了後に出力されるURLかAWSコンソールにて参照します。 先ほどのSQLの実行結果と同様、booksテーブルの全レコードが返ってきます。
$ curl -X GET https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/books | jq [ { "Id": 1, "Name": "思い出の本", "Price": 100 }, { "Id": 2, "Name": "AWS Database Book", "Price": 200000 }, { "Id": 3, "Name": "日記帳12345", "Price": 3000000 }, { "Id": 4, "Name": "あいうえお", "Price": 12345 } ]
おわりに
記事の大半がCDK関連ではなく踏み台サーバ関連になってしまいました。
CDK Patternsのコードは初めて触るリソースをCDKで構築する時に非常に参考になります。ぜひ色々なパターンを試してみてください。