以下の内容はhttps://hepokon365.hatenablog.com/entry/2025/04/19/210000より取得しました。


SSM Session Managerを使ってRDSへ接続できる環境をAWS CDKで作成する

検証環境として、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 のアタッチを行う必要があるが、 ssmSessionPermissionstrue にすることで、この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ではそれぞれ、 InterfaceVpcEndpointAwsServiceEC2_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を使ったほうがセキュアな環境を構築できると感じた。




以上の内容はhttps://hepokon365.hatenablog.com/entry/2025/04/19/210000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14