CDKを使ってSSM(Session Manager)で接続できるEC2を作成する方法について。

参考

EC2 に Session Manager で接続の方法については以下を参照。

基本

SSM Agent

SSM(Session Manager)での接続は、対象のEC2インスタンスでSSM Agentが動作している必要があります。 ここではSSM Agentが最初から組み込まれているAmazon Linux 2023を使うことで、SSM Agentの設定について省略します。

例 Amazon Linux 2023 で SSM Agent のステータス確認

$ sudo systemctl status amazon-ssm-agent
● amazon-ssm-agent.service - amazon-ssm-agent
     Loaded: loaded (/usr/lib/systemd/system/amazon-ssm-agent.service; enabled; preset: enabled)
     Active: active (running) since Tue 2025-01-14 12:38:26 UTC; 4min 39s ago
   Main PID: 1604 (amazon-ssm-agen)
      Tasks: 43 (limit: 1058)
     Memory: 99.5M
        CPU: 1.454s
     CGroup: /system.slice/amazon-ssm-agent.service
             ├─1604 /usr/bin/amazon-ssm-agent
             ├─1657 /usr/bin/ssm-agent-worker
             ├─1762 /usr/bin/ssm-session-worker user-abc123defghijklmnopqrstuvw
             ├─1783 sh
             ├─2010 /usr/bin/ssm-session-worker awscli-test-123456789abcdefghijklmnopq
             └─2024 sh

IAMロール

SSM(Session Manager)での接続は、対象のEC2インスタンスにSSM用のロール AmazonSSMManagedInstanceCore を割り当てる必要があります。 CDKの場合、以下のようなロールを作成してEC2に設定する必要があります。

    const thisRole = new iam.Role(this, "Ec2RoleId", {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
      ],
    });

パブリックIP + Public subnet

CDKでEC2を作成する場合、パブリックIP(IPv4)を割り当てることができます。 パブリックIPが割り当てられたEC2を Public subnetに配置すると、NatGateway無しでSSM(セッションマネージャ)による接続ができるようになります。

必要条件

  • パブリックIP (CDKでEC2を作成する時、associatePublicIpAddress を無しにするか true にする)
  • EC2 を Public subnet に配置
セキュリティグループの Inbound に何も設定しないと、外部からのSSH接続が禁止にできます。

import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";

export class CdkEc2Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const stackName = "CdkEc2_";

    // VPC
    const thisVpc = new ec2.Vpc(this, `${stackName}Vpc`, {
      natGateways: 0,
      maxAzs: 1,
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
      subnetConfiguration: [
        {
          name: `${stackName}publicSubnet`,
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
      ],
    });

    // セキュリティグループ
    const thisSecurityGroup = new ec2.SecurityGroup(
      this,
      `${stackName}SecurityGroup`,
      {
        vpc: thisVpc,
        description: "Allow all outbound",
        allowAllOutbound: true,
      }
    );

    // ロール
    const thisRole = new iam.Role(this, `${stackName}Role`, {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
      ],
    });

    // EC2インスタンス
    const instance = new ec2.Instance(this, `${stackName}AmazonLinux2023`, {
      vpc: thisVpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
      role: thisRole,
      securityGroup: thisSecurityGroup,
      instanceName: `${stackName}AmazonLinux2023`,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
        cpuType: ec2.AmazonLinuxCpuType.X86_64,
      }),

      // 秘密鍵
      keyPair: ec2.KeyPair.fromKeyPairName(this, "test_keypair", "test_keypair"),

      // SSMを有効
      ssmSessionPermissions: true,

      // パブリックIPv4アドレスを付与する。デフォルトの設定なので無くて良い
      // associatePublicIpAddress: true,
    });
  }
}

NatGateway + Private subnet

SSM(セッションマネージャ)の接続をNatGateway経由で行うEC2を作成する方法。 EC2を配置する Private subnet で、 subnetType を PRIVATE_WITH_EGRESS にすると、 NatGateway経由でインターネットにアクセスできるようになります。 この場合、EC2にパブリックIPは必要ありません。

import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";

export class CdkEc2Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const stackName = "CdkEc2_";

    // VPC
    const thisVpc = new ec2.Vpc(this, `${stackName}Vpc`, {
      natGateways: 1,
      maxAzs: 1,
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
      subnetConfiguration: [
        {
          name: `${stackName}PublicSubnet`,
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: `${stackName}PrivateSubnet`,
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
      ],
    });

    const thisSecurityGroup = new ec2.SecurityGroup(
      this,
      `${stackName}SecurityGroup`,
      {
        vpc: thisVpc,
        description: "Allow all outbound",
        allowAllOutbound: true,
      }
    );

    const thisRole = new iam.Role(this, `${stackName}Role`, {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
      ],
    });

    const instance = new ec2.Instance(this, `${stackName}AmazonLinux2023`, {
      vpc: thisVpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      role: thisRole,
      securityGroup: thisSecurityGroup,
      instanceName: `${stackName}AmazonLinux2023`,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
        cpuType: ec2.AmazonLinuxCpuType.X86_64,
      }),

      // 秘密鍵
      keyPair: ec2.KeyPair.fromKeyPairName(this, "jp_test", "jp_test_keypair"),

      // SSMを有効
      ssmSessionPermissions: true,

      // パブリックIPv4アドレスを付与しない
      associatePublicIpAddress: false,
    });
  }
}
  

SSM用VPCエンドポイント + Private subnet

SSM用VPCエンドポイントを作成すると、それらのエンドポイント経由で Private Subnet のEC2にSSMでの接続ができます。 この場合、EC2にパブリックIPは必要ありません。

注意 EC2は外部への経路がないためインターネットにアクセスできません。 パッケージ等のインストールをしたい場合、S3へのアクセス設定を追加してS3経由で行うなどの手間が必要です。

必要条件

  • SSM用VPCエンドポイン
  • EC2からSSMエンドポイントへのHTTPS (ポート 443)を許可
EC2は Private subnet に配置して、パブリックIP無しにできます。

参考

SSMでVPCエンドポイントを使用する場合、最低でも以下の3つのサービスのエンドポイントが必要です。 さらにEC2から下記エンドポイントへ HTTPS (ポート 443) アウトバウンドを許可する必要があります:

・ec2messages.region.amazonaws.com
・ssm.region.amazonaws.com
・smmessages.region.amazonaws.com

3個の必須以外に、以下のようなサービスのエンドポイントが必要な場合があります。

・ec2.InterfaceVpcEndpointAwsService.KMS
・ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS
・ec2.GatewayVpcEndpointAwsService.S3

import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";

export class CdkEc2Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const stackName = "CdkEc2_";

    // VPC
    const thisVpc = new ec2.Vpc(this, `${stackName}Vpc`, {
      natGateways: 0,
      maxAzs: 1,
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
      subnetConfiguration: [
        {
          name: `${stackName}PrivateSubnet`,
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        },
      ],
    });

    // EC2のセキュリティグループ
    const ec2SecurityGroup = new ec2.SecurityGroup(
      this,
      `${stackName}Ec2SecurityGroup`,
      {
        vpc: thisVpc,
        description: "Allow all outbound",
        allowAllOutbound: true,
      }
    );

    const thisRole = new iam.Role(this, `${stackName}Role`, {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
      ],
    });

	// EC2インスタンス
    const instance = new ec2.Instance(this, `${stackName}AmazonLinux2023`, {
      vpc: thisVpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
      role: thisRole,
      securityGroup: ec2SecurityGroup,
      instanceName: `${stackName}AmazonLinux2023`,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
        cpuType: ec2.AmazonLinuxCpuType.X86_64,
      }),

      // 秘密鍵
      keyPair: ec2.KeyPair.fromKeyPairName(this, "test_keypair", "test_keypair"),

      // SSMを有効
      ssmSessionPermissions: true,

      // パブリックIPv4アドレスを付与しない
      associatePublicIpAddress: false,
    });

    // SSMエンドポイント用セキュリティグループ
    const ssmEpSecurityGroup = new ec2.SecurityGroup(
      this,
      `${stackName}SsmEpSecurityGroup`,
      {
        vpc: thisVpc,
        description: "Allow all outbound",
        allowAllOutbound: true,
      }
    );

    // SSMエンドポイントの設定
    thisVpc.addInterfaceEndpoint("SSMEndpoint", {
      service: ec2.InterfaceVpcEndpointAwsService.SSM,
      securityGroups: [ssmEpSecurityGroup],
    });
    thisVpc.addInterfaceEndpoint("SSMMessagesEndpoint", {
      service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
      securityGroups: [ssmEpSecurityGroup],
    });
    thisVpc.addInterfaceEndpoint("EC2MessagesEndpoint", {
      service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
      securityGroups: [ssmEpSecurityGroup],
    });

    // EC2からSSMエンドポイントへの HTTPS (ポート 443)を許可
    ssmEpSecurityGroup.addIngressRule(
      ec2.Peer.securityGroupId(ec2SecurityGroup.securityGroupId),
      ec2.Port.tcp(443),
      "https from EC2",
      false
    );
  }
}