こんにちは。@masatomixです。
の記事で AWS Systems Manager Session Manager (以下SSM)経由でアクセスするEC2への接続をおこないましたが、そのEC2を作るCDKのコードです。
流れとしては、
- 接続に必要な経路(VPCエンドポイント)の構築
- 踏み台サーバ側のEC2に設定するEC2ロールの作成
- EC2インスタンスの作成
って感じのコードになります。
やってみる
接続に必要な経路(VPCエンドポイント)の構築
まず、SSMを使用するのに必要な経路を構築します。
参考:Systems Manager のために VPC エンドポイントを使用して EC2 インスタンスのセキュリティを強化する
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 >> エンドポイント)
VPCからのポート443への接続を許可しています。
踏み台サーバに設定するEC2ロールの作成
踏み台サーバに設定するロールを作成します。SSM接続するEC2にはAmazonSSMManagedInstanceCore
というポリシーが必要なので、それを設定しています。
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 >> ロール)
たしかに AmazonSSMManagedInstanceCore
ポリシーが設定されています。
EC2インスタンスの作成
最後にEC2インスタンスを作成します。
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,
})
踏み台のインスタンス
IAM ロールに、EC2_SSM_Role
が設定されていますね!
本丸のサーバは、192.168.1.204が割り当てられました(最初のキャプチャと異なりますが、IPは自動採番なので適宜読み替えてください)。
全体のCDK
最後にエントリポイントとなるコード。
#!/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そうです。以上、お疲れさまでした!