この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、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
lambda/main.go
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のサンプルコードを参考にしています。 踏み台サーバ関連のコードは新規追加しています。
lib/rds-proxy-go-stack.ts
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で構築する時に非常に参考になります。ぜひ色々なパターンを試してみてください。