検証環境として、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_MYSQL_ADMIN_PASSWORD = '',
} = process.env
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 }
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,
},
],
})
const availabilityZone = vpc.availabilityZones[0]
const availabilityZones = [availabilityZone]
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,
})
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),
})
rdsMysql.connections.allowDefaultPortFrom(ec2Instance)
const ssmVpcEndpoints = [
InterfaceVpcEndpointAwsService.SSM,
InterfaceVpcEndpointAwsService.SSM_MESSAGES,
].map(service => {
const endpointId = prefix('vpc-endpoint-' + service.shortName)
return vpc.addInterfaceEndpoint(endpointId, {
service,
subnets: { availabilityZones, ...ec2Subnet },
})
})
ssmVpcEndpoints.forEach(endpoint => {
endpoint.connections.allowFrom(ec2Instance, Port.HTTPS)
validCidrs.forEach(cidr => {
endpoint.connections.allowFrom(Peer.ipv4(cidr), Port.HTTPS)
})
})
const ountput = (id: string, value: string) => {
new CfnOutput(this, id, { value })
}
ountput('EC2_INSTANCE_ID', ec2Instance.instanceId)
ountput('MYSQL_INSTANCE_HOSTNAME', rdsMysql.dbInstanceEndpointAddress)
}
}
解説
Session Managerを使う場合、EC2インスタンスでAWS Systems Manager Agent(SSMエージェント)が実行されている必要がある。
Amazon Linux系AMIであれば、デフォルトでインストール済みかつインスタンス起動時に自動実行されるため、2025年現在は latestAmazonLinux2023 を使っておけばいい。
SSM Session Managerを使うためには、IAMロールを作成してAWS 管理ポリシー AmazonSSMManagedInstanceCore のアタッチを行う必要があるが、 ssmSessionPermissions を true にすることで、このIAMロール作成などを自動的に行ってくれる。
dev.classmethod.jp
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 は不要になった模様。
dev.classmethod.jp
公式ドキュメントをもう少し見てみる。
結果としては、 EC2_MESSAGES なしでも接続可能だった。
また、SSMエージェントからポーリングされるため、EC2からのHTTPSアクセスを許可しておく。
振り返り
SSM Session Managerを使うのが初めてだったので、どういった仕組みなのか調べたが、EC2をISOLATEDサブネットに配置してもOKというのは面白い。
今回はVPCエンドポイントへのアクセスをCIDRで制限したが、IAMロールで実行可能な処理を細かく指定したりもできるので、パブリックサブネットにEC2を置いてSSHより、Session Managerを使ったほうがセキュアな環境を構築できると感じた。