概要
前回はRDS Proxyは1つで、どちらのユーザも同じ読書ができるエンドポイントを使用していた。
今回は、読取専用のユーザは読取専用のエンドポイントを介してアクセスを行うようにする。
読取専用のエンドポイントを作るため、RDSクラスタには読書用のインスタンスと読取専用のインスタンスを用意する。
アーキテクチャ
CDK
VPCでサブネットIDをSystemMangerのパラメータストアに追加
読取用のエンドポイントの作成に必要。
import { Aspects, Stack, StackProps, Tag, Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Peer, Port, PrivateSubnet, PrivateSubnetProps, SecurityGroup, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { SubnetGroup } from 'aws-cdk-lib/aws-rds';
import { StringListParameter } from 'aws-cdk-lib/aws-ssm';
const DB_PORT = 5432;
interface VpcStackProps extends StackProps {
subnetGroupName: string
ssmParamKeySubnetIds: string
}
export class VpcStack extends Stack {
constructor(scope: Construct, id: string, props: VpcStackProps) {
super(scope, id, { ...props, subnetGroupName: undefined } as StackProps);
const cidr = '10.0.0.0/16';
const vpc = new Vpc(this, 'VPC', {
cidr,
natGateways: 0,
subnetConfiguration: [
{
cidrMask: 24,
name: "public-subnet",
subnetType: SubnetType.PUBLIC
},
],
maxAzs: 3
})
// Tags.of(vpc).add('Stack', id);
Tags.of(vpc).add('Name', 'vpc');
// プライベートSubnet(RDS用)
const privateSubnetProps: PrivateSubnetProps[] = [
{ availabilityZone: 'ap-northeast-1a', vpcId: vpc.vpcId, cidrBlock: '10.0.3.0/24' },
{ availabilityZone: 'ap-northeast-1c', vpcId: vpc.vpcId, cidrBlock: '10.0.4.0/24' },
{ availabilityZone: 'ap-northeast-1d', vpcId: vpc.vpcId, cidrBlock: '10.0.5.0/24' },
]
const subnets = privateSubnetProps.map((prop, i) => {
const subnet = new PrivateSubnet(this, `MyPrivateSubnet${i}`, prop);
Tags.of(subnet).add('Name', `private-subnet-${i}`);
Tags.of(subnet).add('aws-cdk:subnet-type', SubnetType.PRIVATE_ISOLATED);
return subnet
});
//------------------ Aurora用の設定 ----------------------------------
const dbConnectionGroup = new SecurityGroup(this, 'SecurityGroupForPrivateSubnets', {
vpc,
description: 'seburity group for Aurora'
})
Tags.of(dbConnectionGroup).add('Name', 'SecurityGroupForPrivateSubnets');
// securityGroupPrivate.addIngressRule(Peer.ipv4(cidr), Port.allTcp());
const subnetGroupForAurora = new SubnetGroup(this, 'SubnetGroupForAurora', {
vpc,
vpcSubnets: { subnets },
description: 'subnet group for Aurora db',
subnetGroupName: props.subnetGroupName
});
Tags.of(subnetGroupForAurora).add('Name', 'SubnetGroupForAurora');
+ // 読取専用 RDSProxyのエンドポイントを作るためにSubnetIdのリストが必要
+ const subnetIds = subnets.map(subnet => subnet.subnetId);
+ const subnetIdsParameter = new StringListParameter(this, "ssm-subnet-ids", {
+ parameterName: props.ssmParamKeySubnetIds,
+ stringListValue: subnetIds
+ });
+ Tags.of(subnetIdsParameter).add('Name', 'ssm-subnet-ids');
//------------------ 踏み台用の設定 ----------------------------------
const securityGroupPublic = new SecurityGroup(this, 'SecurityGroupForPublicSubnets', {
vpc,
description: 'seburity group for bastion'
})
Tags.of(securityGroupPublic).add('Name', 'SecurityGroupForPublicSubnets');
dbConnectionGroup.addIngressRule(securityGroupPublic, Port.tcp(DB_PORT));
//------------------ Lambda用の設定 ----------------------------------
// We need this security group to add an ingress rule and allow our lambda to query the proxy
const lambdaToRDSProxyGroup = new SecurityGroup(this, 'Lambda to RDS Proxy Connection', {
vpc,
description: 'seburity group for lambda'
});
Tags.of(lambdaToRDSProxyGroup).add('Name', 'SecurityGroupForPrivateSubnetsForLambda');
dbConnectionGroup.addIngressRule(lambdaToRDSProxyGroup, Port.tcp(DB_PORT), 'allow lambda connection');
//------------------ 共通設定 ----------------------------------
// 作成したリソース全てにタグをつける
Aspects.of(this).add(new Tag('Stack', id));
}
}
RDS Proxyに読取エンドポイントの追加
読取専用のRDSインスタンスが必要なので、作成するインスタンス数を2としている。
targetRole: 'READ_ONLY',
としたエンドポイントを追加。
import { Aspects, RemovalPolicy, Stack, StackProps, Tag, Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { InstanceClass, InstanceSize, InstanceType, SecurityGroup, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { AuroraPostgresEngineVersion, CfnDBProxyEndpoint, Credentials, DatabaseCluster, DatabaseClusterEngine, DatabaseProxy, DatabaseSecret, ParameterGroup, ProxyTarget, SubnetGroup } from 'aws-cdk-lib/aws-rds';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { AccountPrincipal, Role } from 'aws-cdk-lib/aws-iam';
import { StringListParameter, StringParameter } from 'aws-cdk-lib/aws-ssm';
interface AuroraStackProps extends StackProps {
vpcId: string
sgId: string
subnetGroupName: string
dbAdminName: string
dbAdminSecretName: string
dbReadOnlyUserName: string
dbReadOnlyUserSecretName: string
+ ssmParamKeySubnetIds: string
}
export class AuroraStack extends Stack {
constructor(scope: Construct, id: string, props: AuroraStackProps) {
// デフォルトのpropsとの意図しない競合を防ぐため、自前で設定したプロパティを削除して親に渡す
const superProps = {
...props, vpcId: undefined, sgId: undefined, subnetName: undefined
, dbSecretName: undefined, dbAdminName: undefined, dbUserPassword: undefined
} as StackProps
super(scope, id, superProps);
const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId: props.vpcId })
const securityGroup = SecurityGroup.fromLookupById(this, 'SecurityGroup', props.sgId);
// subnetGroupNameはlowecaseで作成されている
const subnetGroup = SubnetGroup.fromSubnetGroupName(this, 'SubnetGroup', props.subnetGroupName.toLowerCase());
const secret = this.createSecret({ secretName: props.dbAdminSecretName, rdsName: props.dbAdminName });
const cluster = new DatabaseCluster(this, 'clusterForAurora', {
// LTSのバージョンを選択.RDSProxyは10と11のみのサポート 2021.12.10
engine: DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_11_13 }),
removalPolicy: RemovalPolicy.DESTROY, // 本番運用だと消しちゃだめだと思う
defaultDatabaseName: 'postgres',
instanceProps: {
vpc,
securityGroups: [securityGroup],
// postgresを使える最安値 (2021.12.10)
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
},
+ instances: 2,
- instances: 1,
subnetGroup,
credentials: Credentials.fromSecret(secret)
});
// RDSでの作成ユーザをシークレットに登録
const secretForDBUser = this.createSecret({ secretName: props.dbReadOnlyUserSecretName, rdsName: props.dbReadOnlyUserName });
const proxy = cluster.addProxy('Proxy', {
secrets: [cluster.secret!, secretForDBUser],
vpc,
securityGroups: [securityGroup],
requireTLS: true,
iamAuth: true
});
Tags.of(proxy).add('Name', 'AuroraRDSProxy');
const role = new Role(this, 'DBProxyRole', { assumedBy: new AccountPrincipal(this.account) });
Tags.of(role).add('Name', 'AuroraProxyRole');
proxy.grantConnect(role, props.dbAdminName);
proxy.grantConnect(role, props.dbReadOnlyUserName);
+ // 読取専用エンドポイント
+ const readOnlyEndpoint = new CfnDBProxyEndpoint(this, 'readOnlyProxyEndpoint', {
+ dbProxyEndpointName: 'readOnlyProxyEndpoint',
+ dbProxyName: proxy.dbProxyName,
+ vpcSubnetIds: StringParameter.valueFromLookup(this, props.ssmParamKeySubnetIds).split(','),
+ targetRole: 'READ_ONLY',
+ vpcSecurityGroupIds: [props.sgId]
+ })
+ Tags.of(readOnlyEndpoint).add('Name', 'readOnlyProxyEndpoint');
// 作成したリソース全てにタグをつける
Aspects.of(this).add(new Tag('Stack', id));
}
private createSecret(props: { secretName: string, rdsName: string }) {
const secret = new DatabaseSecret(this, props.secretName, {
secretName: props.secretName,
username: props.rdsName
});
Tags.of(secret).add('Name', props.secretName);
return secret;
}
}
読取専用のLambda
VPC_ID=vpc-hoge
PRIVATE_SG_ID=sg-fuga
PUBLIC_SG_ID=sg-hoge
SUBNET_GROUP_NAME=SubnetGroupForAurora
PUBLIC_SUBNET_ID=subnet-hoge
DB_SECRET_NAME=db-secrets
DB_ADMIN_NAME=rdsadmin
DB_PROXY_RESOURCE_ID=prx-xxxxxxx (RDSプロキシのリソースID(ARNの最後))
DB_PROXY_ENDPOINT=proxy.proxy-hoge.ap-northeast-1.rds.amazonaws.com
+ DB_PROXY_READ_ONLY_ENDPOINT=readOnlyProxyEndpoint.endpoint.proxy-hoge.ap-northeast-1.rds.amazonaws.com
DB_USER_SECRET_NAME=db-rdsuser-secrets
DB_USER_NAME=rdsuser
+ SSM_PARAM_KEY_SUBNET_IDS=/subnet_ids
import { Aspects, Duration, Stack, StackProps, Tag, Tags } from 'aws-cdk-lib';
import { NodejsFunction, BundlingOptions } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import { NODE_LAMBDA_LAYER_DIR } from './process/setup';
import { ISecurityGroup, IVpc, SecurityGroup, SelectedSubnets, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { LambdaIntegration, RestApi } from 'aws-cdk-lib/aws-apigateway';
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
interface PrivateLambdaStackProps extends StackProps {
vpcId: string
sgId: string
rdsProxyResourceId: string
dbAdminName: string
dbProxyEndpoint: string
dbReadOnlyUserName: string
dbProxyReadOnlyEndpoint: string
}
export class PrivateLambdaStack extends Stack {
constructor(scope: Construct, id: string, props: PrivateLambdaStackProps) {
super(scope, id, props);
const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId: props.vpcId })
const securityGroup = SecurityGroup.fromLookupById(this, 'SecurityGroup', props.sgId);
const vpcSubnets = vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_ISOLATED })
const nodeModulesLayer = new lambda.LayerVersion(this, 'NodeModulesLayer',
{
code: lambda.AssetCode.fromAsset(NODE_LAMBDA_LAYER_DIR),
compatibleRuntimes: [lambda.Runtime.NODEJS_14_X]
}
);
const bundling = {
externalModules: [
'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
'date-fns', // Layrerに入れておきたいモジュール
'pg'
],
}
const lambdaParamsDefault = {
layers: [nodeModulesLayer],
vpc,
vpcSubnets,
securityGroups: [securityGroup],
bundling
}
const helloLambda = this.createLambda({
...lambdaParamsDefault,
entry: `../src/handler/api/hello.ts`,
name: 'hellorLambda',
descritption: 'サンプル用メッセージ表示'
})
const rdsEnv = {
DB_PORT: '5432',
DB_HOST: props.dbProxyEndpoint,
DB_USER: props.dbAdminName,
DB_DBNAME: 'postgres',
}
const rdsEnvReadOnly = {
...rdsEnv,
DB_USER: props.dbReadOnlyUserName,
+ DB_HOST: props.dbProxyReadOnlyEndpoint,
+ IS_READ_ONLY: 'true'
}
const dbConnectArn = `arn:aws:rds-db:${this.region}:${this.account}:dbuser:${props.rdsProxyResourceId}`
const rdsAdminResource = `${dbConnectArn}/${props.dbAdminName}`;
const rdsReadOnlyUserResource = `${dbConnectArn}/${props.dbReadOnlyUserName}`;
const adminPolicy = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['rds-db:connect'],
resources: [rdsAdminResource],
})
const readOnlyUserPolicy = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['rds-db:connect'],
resources: [rdsReadOnlyUserResource],
})
const electricLambda = this.createLambda({
...lambdaParamsDefault,
entry: `../src/handler/api/getElectric.ts`,
name: 'electricLambda',
descritption: 'RDSAdminでテーブル参照',
environment: { ...rdsEnv },
initialPolicy: [adminPolicy],
timeoutSec: 10, // DBへの再接続を1秒置き、3回まで行うため、デフォルトの3秒より延ばしておく
})
const api = new RestApi(this, 'ServerlessRestApi', { cloudWatchRole: false });
api.root.addResource('hello').addMethod('GET', new LambdaIntegration(helloLambda));
const electricResource = api.root.addResource('electric')
electricResource.addMethod('GET', new LambdaIntegration(electricLambda));
const electricLambda2 = this.createLambda({
...lambdaParamsDefault,
entry: `../src/handler/api/getElectric.ts`,
name: 'electricLambda2',
descritption: 'RDS 読取専用ユーザでテーブル参照',
environment: { ...rdsEnvReadOnly },
initialPolicy: [readOnlyUserPolicy],
timeoutSec: 10,
})
const electricReadonlyResource = api.root.addResource('electric-readonly');
electricReadonlyResource.addMethod('GET', new LambdaIntegration(electricLambda2));
const electricLambda3 = this.createLambda({
...lambdaParamsDefault,
entry: `../src/handler/api/postElectric.ts`,
name: 'electricLambda3',
descritption: 'RDS 読取専用ユーザでテーブル挿入',
environment: { ...rdsEnvReadOnly },
initialPolicy: [readOnlyUserPolicy],
timeoutSec: 10,
})
electricReadonlyResource.addMethod('POST', new LambdaIntegration(electricLambda3));
const electricLambda4 = this.createLambda({
...lambdaParamsDefault,
entry: `../src/handler/api/postElectric.ts`,
name: 'electricLambda4',
descritption: 'RDSAdminでテーブル挿入',
environment: { ...rdsEnv },
initialPolicy: [adminPolicy],
timeoutSec: 10,
})
electricResource.addMethod('POST', new LambdaIntegration(electricLambda4));
// // 認証情報へのアクセス許可
// const secret = Secret.fromSecretNameV2(this, 'RDSSecret', props.dbSecretName);
// secret.grantRead(electricLambda);
Aspects.of(this).add(new Tag('Stack', id));
}
private createLambda(props: {
layers: lambda.LayerVersion[]
vpc: IVpc
vpcSubnets: SelectedSubnets
securityGroups: ISecurityGroup[]
bundling: BundlingOptions
name: string
descritption: string
entry: string
environment?: Record<string, string>
initialPolicy?: PolicyStatement[]
timeoutSec?: number
}) {
const func = new NodejsFunction(this, props.name, {
runtime: lambda.Runtime.NODEJS_14_X,
entry: props.entry,
functionName: props.name,
description: props.descritption,
layers: props.layers,
vpc: props.vpc,
vpcSubnets: props.vpcSubnets,
securityGroups: props.securityGroups,
bundling: props.bundling,
environment: props.environment,
initialPolicy: props.initialPolicy,
timeout: props.timeoutSec ? Duration.seconds(props.timeoutSec) : undefined,
});
Tags.of(func).add('Name', props.name);
return func;
}
}
Postgres接続のソース修正
リーダーエンドポイントのホスト名に .endpoint. があるため、証明書のコモンネームと不一致とされてしまう。
コモンネームの一致まで検証しないように、 sslmode を verify-full ではなく verify-ca にする。
const getPool = () => {
const { DB_PORT, DB_HOST, DB_USER, DB_DBNAME, AWS_REGION } = process.env;
const signerOptions = {
region: AWS_REGION,
hostname: DB_HOST,
port: Number(DB_PORT),
username: DB_USER,
}
+ // 読取専用エンドポイントの場合は verify-ca で接続(ホストのドメインに.endpointが含まれるため証明書のコモンネームと不一致となるため検証スキップ)
+ const ssl = process.env.IS_READ_ONLY ? {
+ ca: fs.readFileSync('/opt/nodejs/data/AmazonRootCA1.pem'),
+ requestCert: true,
+ rejectUnauthorized: false
+ } : { ca: fs.readFileSync('/opt/nodejs/data/AmazonRootCA1.pem') };
return new Pool({
host: signerOptions.hostname,
port: signerOptions.port,
user: signerOptions.username,
database: DB_DBNAME,
// IAM認証のため、パスワードの代わりにトークンを使用する。
password: () => signer.getAuthToken(signerOptions),
+ ssl
- ssl: { ca: fs.readFileSync('/opt/nodejs/data/AmazonRootCA1.pem') }
});
}
動作確認
読取専用のエンドポイント経由で接続されたことをログから確認する。
参考
公式
新しい SSL/TLS 証明書を使用して PostgreSQL DB インスタンスに接続するようにアプリケーションを更新する
Amazon RDS Proxy が Amazon Aurora Replicas の読み取り専用エンドポイントを追加
Working with Amazon RDS Proxy endpoints
cdk v2 - CfnDBProxyEndpoint
ssm parameter store
postgres - ssl
野生
RDS Proxy でリーダーエンドポイントが利用可能になりました
そもそもコネクションプーリングとは?
Amazon RDS が使用する IAM ロールをうっかり絵を描いて整理してみた
Use Amazon RDS Proxy with read-only endpoints
RDS Proxy のリーダーエンドポイントに IAM 認証を使う方法
RDS Proxy 経由で Aurora の Reader エンドポイントに同時多重でクエリを発行して均等分散することを確認した
github RDS Proxyの追加
実践!AWS CDK #23 RDS インスタンス
aws-cdk-lib.aws_rds module
RDSとAuroraで変更を検討するパラメータ(PostgreSQL)
github - aws-samples/amazon-rds-init-cdk
github - aws-cdk-rds-proxy
CDK の Vpc.fromLookup では StringParameter.valueFromLookup を使う
CDKで作成したVPCネットワークのElastic IPを取得する
AWS-CDKで「これ、どうかくの?」
CDK Tips&Trics
AWS CDKでAWS Systems Manager パラメータストア及びAWS Secrets Managerからパラメータを取り込む方法
Node.js アプリから Heroku Postgres に接続できなくなったので SSL 通信設定を直す
SSL設定を変更(rejectUnauthorized: false )
メモ
読取専用エンドポイントに変更すると、タイムアウトとなる。 -> 読取専用エンドポイントのセキュリティグループの設定をしていなかった。