0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS CDK で Infrastructure as Code する: Session Manager接続するEC2編

Last updated at Posted at 2025-05-06

こんにちは。@masatomixです。

の記事で AWS Systems Manager Session Manager (以下SSM)経由でアクセスするEC2への接続をおこないましたが、そのEC2を作るCDKのコードです。

image-20250426001428513

流れとしては、

  • 接続に必要な経路(VPCエンドポイント)の構築
  • 踏み台サーバ側のEC2に設定するEC2ロールの作成
  • EC2インスタンスの作成

って感じのコードになります。

やってみる

接続に必要な経路(VPCエンドポイント)の構築

まず、SSMを使用するのに必要な経路を構築します。

参考:Systems Manager のために VPC エンドポイントを使用して EC2 インスタンスのセキュリティを強化する

lib/VPCEndpointStack.ts
import { App, ScopedAws, Stack, StackProps } from 'aws-cdk-lib'
import { CfnSecurityGroup, CfnSecurityGroupIngress, CfnSubnet, CfnVPC, CfnVPCEndpoint } from 'aws-cdk-lib/aws-ec2'
import { Profile, getProfile, toRefs } from './Utils'

type VPCEndpointStackProps = StackProps & {
  vpc: CfnVPC
  subnets: CfnSubnet[]
}

export class VPCEndpointStack extends Stack {
  constructor(scope: App, id: string, props: VPCEndpointStackProps) {
    super(scope, id, props)
    const p = getProfile(this)
    const { accountId, region } = new ScopedAws(this)

    const { vpc, subnets } = props

    const vpcEndpoints = [
      `com.amazonaws.${region}.ec2messages`,
      `com.amazonaws.${region}.ssm`,
      `com.amazonaws.${region}.ssmmessages`
    ]

    const vpcEndpointSecurityGroup = createVPCEndpointSecurityGroup(this, `VPCEndpointSG`, vpc, p)

    vpcEndpoints.forEach((vpcEndpoint, index) => {
      new CfnVPCEndpoint(this, `vpcEndpoint-${index}`, {
        serviceName: vpcEndpoint,
        vpcId: vpc.ref,
        vpcEndpointType: 'Interface',
        subnetIds: toRefs(subnets),
        securityGroupIds: [vpcEndpointSecurityGroup.ref],
        privateDnsEnabled: true,
      })
    })
  }
}

const createVPCEndpointSecurityGroup = (stack: Stack, id: string, vpc: CfnVPC, p: Profile): CfnSecurityGroup => {
  const group = new CfnSecurityGroup(stack, id, {
    groupName: `${p.name}vpcendpoint-sg`,
    groupDescription: 'vpcendpoint-sg',
    vpcId: vpc.attrVpcId,
    tags: [{ key: 'Name', value: `${p.name}VPCEndpointSG` }],
  })

  new CfnSecurityGroupIngress(stack, 'SecurityGroupIngress000', {
    ipProtocol: '-1',
    groupId: group.ref,
    sourceSecurityGroupId: group.ref,
  })

  new CfnSecurityGroupIngress(stack, 'SecurityGroupIngress001', {
    ipProtocol: 'tcp',
    fromPort: 443,
    toPort: 443,
    groupId: group.ref,
    cidrIp: vpc.attrCidrBlock,
  })

  return group
}

設定されたVPCエンドポイント(VPC >> PrivateLink と Lattice >> エンドポイント)
vpcendpoint

そのエンドポイントに紐付けられたセキュリティグループ
sg

VPCからのポート443への接続を許可しています。

踏み台サーバに設定するEC2ロールの作成

踏み台サーバに設定するロールを作成します。SSM接続するEC2にはAmazonSSMManagedInstanceCoreというポリシーが必要なので、それを設定しています。

lib/EC2RoleStack.ts
import { App, Stack, StackProps } from 'aws-cdk-lib'
import { CfnRole } from 'aws-cdk-lib/aws-iam'

export class EC2RoleStack extends Stack {
  public readonly role: CfnRole

  constructor(scope: App, id: string, props?: StackProps) {
    super(scope, id, props)

    this.role = new CfnRole(this, 'EC2_SSM_Role', {
      path: '/',
      roleName: 'EC2_SSM_Role',
      assumeRolePolicyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Sid: '',
            Effect: 'Allow',
            Principal: {
              Service: 'ec2.amazonaws.com',
            },
            Action: 'sts:AssumeRole',
          },
        ],
      },
      // maxSessionDuration: 3600,
      managedPolicyArns: ['arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'],
      description: 'EC2_SSM_Role',
    })
  }
}

設定されたロールです。(IAM >> ロール)

role

たしかに AmazonSSMManagedInstanceCore ポリシーが設定されています。

EC2インスタンスの作成

最後にEC2インスタンスを作成します。

lib/BastionStack.ts
import { App, Stack, StackProps } from 'aws-cdk-lib'
import {
  CfnInstance,
  CfnKeyPair,
  CfnSecurityGroup,
  CfnSecurityGroupIngress,
  CfnSubnet,
  CfnVPC,
} from 'aws-cdk-lib/aws-ec2'
import { getProfile } from './Utils'
import { CfnInstanceProfile, CfnRole } from 'aws-cdk-lib/aws-iam'

type BastionStackProps = StackProps & {
  vpc: CfnVPC
  subnet: CfnSubnet
  role: CfnRole
}

export class BastionStack extends Stack {
  constructor(scope: App, id: string, props: BastionStackProps) {
    super(scope, id, props)
    const p = getProfile(this)

    const { vpc, subnet, role } = props

    // SecurityGroupの作成
    const bastionSG = createBastionSG(this, vpc, 'bastion')
    const ec2SG = createEC2SG(this, vpc, bastionSG, 'ec2')

    // キーペアの作成
    const keyPair = new CfnKeyPair(this, 'KeyPair', {
      keyName: `${p.name}-KeyPair`,
    })

    const instanceProfile = new CfnInstanceProfile(this, 'InstanceProfileEc2', {
      roles: [role.ref],
    })

    // EC2インスタンスの作成
    createEC2({
      stack: this, subnet, groupSet: [bastionSG.ref],
      associatePublicIpAddress: false,
      name: 'bastion',
      instanceProfile,
    })
    createEC2({
      stack: this, subnet, groupSet: [ec2SG.ref],
      associatePublicIpAddress: false,
      name: 'dest',
      keyName: keyPair.ref,
    })
  }
}

type FC = (args: {
  stack: Stack,
  subnet: CfnSubnet,
  groupSet: string[],
  keyName?: string,
  associatePublicIpAddress: boolean,
  name: string,
  instanceProfile?: CfnInstanceProfile
}) => CfnInstance

const createEC2: FC = ({
  stack, subnet, groupSet,
  keyName,
  associatePublicIpAddress,
  name,
  instanceProfile
}): CfnInstance => {

  const baseProps = {
    imageId: 'ami-04beabd6a4fb6ab6f',
    // imageId: 'ami-00d101850e971728d',
    instanceType: 't2.micro',
    networkInterfaces: [
      {
        associatePublicIpAddress,
        deviceIndex: '0',
        subnetId: subnet.attrSubnetId,
        groupSet,
      },
    ],
    tags: [{ key: 'Name', value: `${name}-ec2` }],
  }

  const keyNameProps = keyName ? { ...baseProps, keyName } : baseProps
  const props = instanceProfile ? { ...keyNameProps, iamInstanceProfile: instanceProfile.ref } : keyNameProps
  return new CfnInstance(stack, `${name}-BastionEC2`, props)
}

const createBastionSG = (stack: Stack, vpc: CfnVPC, prefix: string): CfnSecurityGroup => {
  const group = new CfnSecurityGroup(stack, `${prefix}SG`, {
    groupName: `${prefix}-sg`,
    groupDescription: `${prefix} SecurityGroup`,
    vpcId: vpc.attrVpcId,
    tags: [{ key: 'Name', value: `${prefix}-sg` }],
  })
  return group
}

const createEC2SG = (stack: Stack, vpc: CfnVPC, bastionSG: CfnSecurityGroup, prefix: string): CfnSecurityGroup => {
  const group = new CfnSecurityGroup(stack, `${prefix}SG`, {
    groupName: `${prefix}-sg`,
    groupDescription: `${prefix} SecurityGroup`,
    vpcId: vpc.attrVpcId,
    tags: [{ key: 'Name', value: `${prefix}-sg` }],
  })

  new CfnSecurityGroupIngress(stack, `${prefix}SGIngress000`, {
    // ipProtocol: '-1',
    ipProtocol: 'tcp',
    fromPort: 22,
    toPort: 22,
    groupId: group.ref,
    sourceSecurityGroupId: bastionSG.ref,
  })
  return group
}

一部抜粋すると、下記のコードあたりで、作成したロールを指定してEC2インスタンスを作成していますね。

    const instanceProfile = new CfnInstanceProfile(this, 'InstanceProfileEc2', {
      roles: [role.ref],
    })

    // EC2インスタンスの作成
    createEC2({
      stack: this, subnet, groupSet: [bastionSG.ref],
      associatePublicIpAddress: false,
      name: 'bastion',
      instanceProfile,
    })

踏み台のインスタンス

bastion

IAM ロールに、EC2_SSM_Roleが設定されていますね!

本丸のインスタンス
dest

本丸のサーバは、192.168.1.204が割り当てられました(最初のキャプチャと異なりますが、IPは自動採番なので適宜読み替えてください)。

全体のCDK

最後にエントリポイントとなるコード。

bin/cdk-samples.ts
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { VPCStack } from "../lib/VPCStack";
import { BastionStack } from "../lib/BastionStack";
import { VPCEndpointStack } from "../lib/VPCEndpointStack";
import { EC2RoleStack } from "../lib/EC2RoleStack";

const main = () => {
    const app = new cdk.App()
    const vpcStack = new VPCStack(app, "VPCStack")
    new VPCEndpointStack(app, "VPCEndpointStack", {
        vpc: vpcStack.vpc,
        subnets: vpcStack.privateSubnets,
    })
    const ec2roleStack = new EC2RoleStack(app, "EC2RoleStack")
    new BastionStack(app, "BastionStack", {
        vpc: vpcStack.vpc, subnet: vpcStack.privateSubnets[0], role: ec2roleStack.role
    })
}


main()

CDKのコードの説明は以上です。ということでCDKを実行します。下記サイトを参考にしつつ、コマンドを実行してみてください。
参考: AWS CDK で Infrastructure as Code する: EC2編

$ yarn cdk deploy --all --profile ${profile}
... しばらくかかります
$

接続する

さてCDKの実行が終わったので、前の記事にしたがって接続します。

前記事 :Session Manager経由のEC2アクセスから、SSHポートフォワードまでのメモ

$ aws ssm start-session --target ${instance_id} --profile ${profile} \
 --document-name AWS-StartPortForwardingSessionToRemoteHost \
 --parameters localPortNumber=2222,portNumber=22,host=192.168.1.204

Starting session with SessionId: masatomix@xxx-orhlql7j9qhcuijuudqiofg34e
Port 2222 opened for sessionId masatomix@xxx-orhlql7j9qhcuijuudqiofg34e.
Waiting for connections...

ポートフォワードが出来たので、別プロンプトで、

$ ssh -l ec2-user localhost -p2222 -i ~/.ssh/hoge.pem
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
Last login: Tue May  6 12:47:25 2025 from 192.168.1.234
[ec2-user@ip-192-168-1-204 ~]$

OKそうです。以上、お疲れさまでした!

関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?