検証環境として、RDSにSSM Session ManagerおよびNLBを用いたPrivateLinkでアクセスできる環境を構築することとなった。
今後も同じような環境を作ることになりそうなので、CDKで書くことにしたが、そのものズバリこうすればできる、といったサンプルが見つからなかった。
試行錯誤して環境構築できたので、まずはSSM Session ManagerでRDSにアクセスできる状態までをメモ。
実装
TypeScriptで記述。
環境変数は、 dotenv を追加し、 .env ファイルに記述している。
検証環境のため、EC2およびRDSは無料枠内のスペックで作成。
import { CfnOutput, Duration, SecretValue, Stack, StackProps, } from 'aws-cdk-lib' import { AmazonLinuxCpuType, Instance, InstanceClass, InstanceSize, InstanceType, InterfaceVpcEndpointAwsService, MachineImage, Peer, Port, SubnetType, Vpc, } from 'aws-cdk-lib/aws-ec2' import { Credentials, DatabaseInstance, DatabaseInstanceEngine, MysqlEngineVersion, } from 'aws-cdk-lib/aws-rds' import { Construct } from 'constructs' import 'dotenv/config' const { // RDSの管理者パスワード RDS_MYSQL_ADMIN_PASSWORD = '', } = process.env // 接続元のCIDRを設定 const validCidrs = [ ... ] as const const prefix = (name: string) => 'example-' + name export class CdkStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props) // サブネット指定用に変数を宣言 const ec2SubnetName = prefix('subnet-private-ec2') const rdsSubnetName = prefix('subnet-private-rds') const ec2Subnet = { subnetGroupName: ec2SubnetName } const rdsSubnet = { subnetGroupName: rdsSubnetName } // VPCの作成 // RDSは単一AZに配置するが、VPCのAZが1つだけだとRDSの配置がエラーとなるため、 // maxAzsには2以上を指定しておく const vpc = new Vpc(this, prefix('vpc'), { maxAzs: 2, natGateways: 0, subnetConfiguration: [ { cidrMask: 24, name: ec2SubnetName, subnetType: SubnetType.PRIVATE_ISOLATED, }, { cidrMask: 24, name: rdsSubnetName, subnetType: SubnetType.PRIVATE_ISOLATED, }, ], }) // 各インスタンスを単一のAZに配置するため取得 const availabilityZone = vpc.availabilityZones[0] const availabilityZones = [availabilityZone] // SSM用EC2インスタンスの作成 const ec2Instance = new Instance(this, prefix('bastion'), { vpc, availabilityZone, vpcSubnets: ec2Subnet, instanceType: InstanceType.of( InstanceClass.T4G, InstanceSize.MICRO, ), machineImage: MachineImage.latestAmazonLinux2023({ cpuType: AmazonLinuxCpuType.ARM_64, cachedInContext: true, }), ssmSessionPermissions: true, // SSMを有効化 }) // RDSインスタンスの作成 const rdsMysql = new DatabaseInstance(this, prefix('rds-mysql'), { engine: DatabaseInstanceEngine.mysql({ version: MysqlEngineVersion.VER_8_0_40, }), instanceType: InstanceType.of( InstanceClass.T4G, InstanceSize.MICRO, ), databaseName: 'example', credentials: Credentials.fromUsername('admin', { password: SecretValue.unsafePlainText(RDS_MYSQL_ADMIN_PASSWORD), }), allocatedStorage: 20, vpc, availabilityZone, vpcSubnets: rdsSubnet, multiAz: false, backupRetention: Duration.days(0), // バックアップなしに設定 }) // EC2からRDSへの接続を許可 rdsMysql.connections.allowDefaultPortFrom(ec2Instance) // SSM用VPCエンドポイントを作成 const ssmVpcEndpoints = [ InterfaceVpcEndpointAwsService.SSM, InterfaceVpcEndpointAwsService.SSM_MESSAGES, ].map(service => { const endpointId = prefix('vpc-endpoint-' + service.shortName) return vpc.addInterfaceEndpoint(endpointId, { service, subnets: { availabilityZones, ...ec2Subnet }, }) }) // SSM用VPCエンドポイントに、EC2および特定IPアドレスからの接続を許可 ssmVpcEndpoints.forEach(endpoint => { endpoint.connections.allowFrom(ec2Instance, Port.HTTPS) validCidrs.forEach(cidr => { endpoint.connections.allowFrom(Peer.ipv4(cidr), Port.HTTPS) }) }) // EC2のIDおよびRDSのエンドポイントを出力 const ountput = (id: string, value: string) => { new CfnOutput(this, id, { value }) } ountput('EC2_INSTANCE_ID', ec2Instance.instanceId) ountput('MYSQL_INSTANCE_HOSTNAME', rdsMysql.dbInstanceEndpointAddress) } }
解説
EC2インスタンス
Session Managerを使う場合、EC2インスタンスでAWS Systems Manager Agent(SSMエージェント)が実行されている必要がある。
Amazon Linux系AMIであれば、デフォルトでインストール済みかつインスタンス起動時に自動実行されるため、2025年現在は latestAmazonLinux2023 を使っておけばいい。
SSM Session Managerを使うためには、IAMロールを作成してAWS 管理ポリシー AmazonSSMManagedInstanceCore のアタッチを行う必要があるが、 ssmSessionPermissions を true にすることで、このIAMロール作成などを自動的に行ってくれる。
SSMエージェントはVPCエンドポイントに送信されたコマンドをポーリングするため、EC2のインバウンド許可は不要。また、サブネットタイプはEC2も PRIVATE_ISOLATED でいい。
VPCエンドポイント
公式ドキュメントを見ると、以下3つのVPCエンドポイントへのHTTPSトラフィックを許可する必要があると記載されている。
- ec2messages.<region>.amazonaws.com
- ssm.<region>.amazonaws.com
- ssmmessages.<region>.amazonaws.com
CDKではそれぞれ、 InterfaceVpcEndpointAwsService の EC2_MESSAGES, SSM, SSM_MESSAGES に該当。
ただ、 EC2_MESSAGES は不要になった模様。
公式ドキュメントをもう少し見てみる。
- https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-setting-up-messageAPIs.html
- 「2024 年以降にローンチされたリージョンでは、ssmmessages:* API オペレーションのみがサポート」との記載
- https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-create-vpc.html
結果としては、 EC2_MESSAGES なしでも接続可能だった。
また、SSMエージェントからポーリングされるため、EC2からのHTTPSアクセスを許可しておく。
振り返り
SSM Session Managerを使うのが初めてだったので、どういった仕組みなのか調べたが、EC2をISOLATEDサブネットに配置してもOKというのは面白い。
今回はVPCエンドポイントへのアクセスをCIDRで制限したが、IAMロールで実行可能な処理を細かく指定したりもできるので、パブリックサブネットにEC2を置いてSSHより、Session Managerを使ったほうがセキュアな環境を構築できると感じた。