AWS CDKでスタック内の全てのEC2インスタンスをEBS最適化インスタンスにしてみた

L2 Contructのプロパティが足りなくても何とかなります
2022.06.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS CDKでデプロイしたEC2インスタンスをEBS最適化インスタンスにしたい

こんにちは、のんピ(@non____97)です。

皆さんはEBS最適化インスタンスは好きですかか? 私は好きです。

EBS最適化インスタンスにすることで、EC2インスタンスとEBSボリューム間に専用のネットワークスループットが確保されます。結果として、EBSボリュームのパフォーマンスを安定的に引き出すことができるようになるありがたい機能です。

AWS公式ドキュメントも「EBS ボリュームの最高のパフォーマンスを実現します。」とかなり熱く紹介していますね。

特にコストなくパフォーマンスを向上できるのであれば、AWS CDKでパパッとEC2インスタンスを作るときもEBS最適化インスタンスとしてデプロイしたいと思うのは当然でしょう。

しかし、L2 ConstructであるInstanceのプロパティを探してもEBS最適化を有効にするような設定はありません。

実際、以下のようなコードでデプロイすると、デプロイされたEC2インスタンスはEBS最適化無効になっています。

./lib/ec2-instance-stack.ts

import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib";
import { Construct } from "constructs";

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

    // VPC
    const vpc = new ec2.Vpc(this, "VPC", {
      cidr: "10.10.0.0/24",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 0,
      maxAzs: 2,
      subnetConfiguration: [
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 },
      ],
    });

    // EC2 Instance
    new ec2.Instance(this, "EC2 Instance", {
      instanceType: new ec2.InstanceType("t3.micro"),
      machineImage: ec2.MachineImage.latestAmazonLinux({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      vpc: vpc,
      blockDevices: [
        {
          deviceName: "/dev/xvda",
          volume: ec2.BlockDeviceVolume.ebs(8, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
      propagateTagsToVolumeOnCreation: true,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }),
    });
  }
}

EBS最適化が無効

これは困った

ということで、AWS CDKでEC2インスタンスをEBS最適化インスタンスとしてデプロイする方法を色々考えてみました。

CloudFormationとL1 ConstructのEBS最適化設定の仕方

そもそもCloudFormationでEBS最適化設定が出来るのか確認します。

AWS::EC2::Instanceを確認すると、EbsOptimizedというプロパティがあることから、CloudFormationでは設定出来るようですね。

Type: AWS::EC2::Instance
Properties: 
  AdditionalInfo: String
  Affinity: String
  AvailabilityZone: String
  BlockDeviceMappings: 
    - BlockDeviceMapping
  CpuOptions: 
    CpuOptions
  CreditSpecification: 
    CreditSpecification
  DisableApiTermination: Boolean
  EbsOptimized: Boolean
  ElasticGpuSpecifications: 
    - ElasticGpuSpecification
  ElasticInferenceAccelerators: 
    - ElasticInferenceAccelerator
  EnclaveOptions: 
    EnclaveOptions
  HibernationOptions: 
    HibernationOptions
  HostId: String
  HostResourceGroupArn: String
  IamInstanceProfile: String
  ImageId: String
  InstanceInitiatedShutdownBehavior: String
  InstanceType: String
  Ipv6AddressCount: Integer
  Ipv6Addresses: 
    - InstanceIpv6Address
  KernelId: String
  KeyName: String
  LaunchTemplate: 
    LaunchTemplateSpecification
  LicenseSpecifications: 
    - LicenseSpecification
  Monitoring: Boolean
  NetworkInterfaces: 
    - NetworkInterface
  PlacementGroupName: String
  PrivateDnsNameOptions: 
    PrivateDnsNameOptions
  PrivateIpAddress: String
  PropagateTagsToVolumeOnCreation: Boolean
  RamdiskId: String
  SecurityGroupIds: 
    - String
  SecurityGroups: 
    - String
  SourceDestCheck: Boolean
  SsmAssociations: 
    - SsmAssociation
  SubnetId: String
  Tags: 
    - Tag
  Tenancy: String
  UserData: String
  Volumes: 
    - Volume

次に、L1 Constructであるclass CfnInstanceのプロパティも確認してみましょう。

確かにebsOptimizedというプロパティがありますね。

ebsOptimized?

Type: boolean | IResolvable (optional)

Indicates whether the instance is optimized for Amazon EBS I/O.

This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal Amazon EBS I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS-optimized instance.

Default: false

class CfnInstance (construct) · AWS CDK

L2 Constructのプロパティを上書きする

L1 ConstructではEBS最適化設定が出来ることが分かったので、L1 Constructを使いましょう

というのは私はちょっと嫌です。

プロパティが少し足りないからといってL1 Constructを使うというのは、AWS CDKを使う旨味が少なくなる気がします。

対応として、L2 Constructでインスタンスを作成してからプロパティを上書きします。

こちらの手法はAWS公式ドキュメントでも紹介されています。

実際のコードは以下の通りです。

./lib/ec2-instance-stack.ts

import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib";
import { Construct } from "constructs";

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

    // VPC
    const vpc = new ec2.Vpc(this, "VPC", {
      cidr: "10.10.0.0/24",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 0,
      maxAzs: 2,
      subnetConfiguration: [
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 },
      ],
    });

    // EC2 Instance
    const ec2Instance = new ec2.Instance(this, "EC2 Instance", {
      instanceType: new ec2.InstanceType("t3.micro"),
      machineImage: ec2.MachineImage.latestAmazonLinux({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      vpc: vpc,
      blockDevices: [
        {
          deviceName: "/dev/xvda",
          volume: ec2.BlockDeviceVolume.ebs(8, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
      propagateTagsToVolumeOnCreation: true,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }),
    });

    const cfnInstance = ec2Instance.node.defaultChild as ec2.CfnInstance;
    cfnInstance.ebsOptimized = true;
  }
}

こちらのコードでデプロイしてみると、EC2インスタンスのEBS最適化有効になりました。

L2 Constructのプロパティを上書きする

できらぁ!

スタック内の全てのEC2インスタンスをまとめて設定する

複数のEC2インスタンスをスタック内で定義している場合に面倒だな

EC2インスタンスが数台しかない場合は、上述した方法で対応するのも良いでしょう。

しかし、EC2インスタンスが複数ある場合に都度設定するのもちょっと面倒です。

ということで、以下2つの方法で複数のEC2インスタンスをまとめてEBS最適化インスタンスにしてみます。

  1. スタックのnode.childrenからCfnInstanceのノードを探してプロパティを上書きする
  2. EBS最適化インスタンス用のカスタムコンストラクトを作成する

1. スタックの"node.children"から"CfnInstance"のノードを探してプロパティを上書きする

まず、1つ目の方法です。

./lib/ec2-instance-stack.ts内にconsole.log(this.node.children)を追加します。そしてnpx cdk synthをし、スタックのコンストラクトノード配下を表示してニヤニヤしてみましょう。

出力結果がかなり長いので折りたたみます。

console.log(this.node.children)の結果
[
  <ref *1> Vpc {
    node: Node {
      host: [Circular *1],
      _locked: false,
      _children: [Object],
      _context: {},
      _metadata: [],
      _dependencies: Set(0) {},
      _validations: [],
      id: 'VPC',
      scope: [Ec2InstanceStack]
    },
    stack: Ec2InstanceStack {
      node: [Node],
      _missingContext: [],
      _stackDependencies: {},
      templateOptions: {},
      _logicalIds: [LogicalIDs],
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}',
      environment: 'aws://unknown-account/unknown-region',
      terminationProtection: undefined,
      _stackName: 'Ec2InstanceStack',
      tags: [TagManager],
      artifactId: 'Ec2InstanceStack',
      templateFile: 'Ec2InstanceStack.template.json',
      _versionReportingEnabled: true,
      synthesizer: [DefaultStackSynthesizer],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    env: {
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}'
    },
    _physicalName: undefined,
    _allowCrossEnvironment: false,
    physicalName: '${Token[TOKEN.201]}',
    natDependencies: [],
    incompleteSubnetDefinition: false,
    publicSubnets: [ [PublicSubnet], [PublicSubnet] ],
    privateSubnets: [],
    isolatedSubnets: [],
    subnetConfiguration: [ [Object] ],
    _internetConnectivityEstablished: DependencyGroup {
      _deps: [Array],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    networkBuilder: NetworkBuilder {
      subnetCidrs: [Array],
      networkCidr: [CidrBlock],
      nextAvailableIp: 168427552
    },
    dnsHostnamesEnabled: true,
    dnsSupportEnabled: true,
    internetConnectivityEstablished: DependencyGroup {
      _deps: [Array],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    resource: CfnVPC {
      node: [Node],
      stack: [Ec2InstanceStack],
      logicalId: '${Token[Ec2InstanceStack.VPC.Resource.LogicalID.202]}',
      cfnOptions: [Object],
      rawOverrides: {},
      dependsOn: Set(0) {},
      cfnResourceType: 'AWS::EC2::VPC',
      _cfnProperties: [Object],
      attrCidrBlock: '${Token[TOKEN.203]}',
      attrCidrBlockAssociations: [Array],
      attrDefaultNetworkAcl: '${Token[TOKEN.205]}',
      attrDefaultSecurityGroup: '${Token[TOKEN.206]}',
      attrIpv6CidrBlocks: [Array],
      attrVpcId: '${Token[TOKEN.208]}',
      cidrBlock: '10.10.0.0/24',
      enableDnsHostnames: true,
      enableDnsSupport: true,
      instanceTenancy: 'default',
      ipv4IpamPoolId: undefined,
      ipv4NetmaskLength: undefined,
      tags: [TagManager],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    vpcDefaultNetworkAcl: '${Token[TOKEN.205]}',
    vpcCidrBlockAssociations: [ '#{Token[TOKEN.204]}' ],
    vpcCidrBlock: '${Token[TOKEN.203]}',
    vpcDefaultSecurityGroup: '${Token[TOKEN.206]}',
    vpcIpv6CidrBlocks: [ '#{Token[TOKEN.207]}' ],
    availabilityZones: [ '${Token[TOKEN.210]}', '${Token[TOKEN.212]}' ],
    vpcId: '${Token[TOKEN.213]}',
    vpcArn: 'arn:${Token[AWS.Partition.10]}:ec2:${Token[AWS.Region.11]}:${Token[AWS.AccountId.7]}:vpc/${Token[TOKEN.213]}',
    internetGatewayId: '${Token[TOKEN.248]}',
    [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] }
  },
  <ref *2> Instance {
    node: Node {
      host: [Circular *2],
      _locked: false,
      _children: [Object],
      _context: {},
      _metadata: [],
      _dependencies: Set(0) {},
      _validations: [],
      id: 'EC2 Instance Amazon Linux 2',
      scope: [Ec2InstanceStack],
      _defaultChild: [CfnInstance]
    },
    stack: Ec2InstanceStack {
      node: [Node],
      _missingContext: [],
      _stackDependencies: {},
      templateOptions: {},
      _logicalIds: [LogicalIDs],
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}',
      environment: 'aws://unknown-account/unknown-region',
      terminationProtection: undefined,
      _stackName: 'Ec2InstanceStack',
      tags: [TagManager],
      artifactId: 'Ec2InstanceStack',
      templateFile: 'Ec2InstanceStack.template.json',
      _versionReportingEnabled: true,
      synthesizer: [DefaultStackSynthesizer],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    env: {
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}'
    },
    _physicalName: undefined,
    _allowCrossEnvironment: false,
    physicalName: '${Token[TOKEN.252]}',
    securityGroups: [ [SecurityGroup] ],
    securityGroup: SecurityGroup {
      node: [Node],
      stack: [Ec2InstanceStack],
      env: [Object],
      _physicalName: undefined,
      _allowCrossEnvironment: false,
      physicalName: '${Token[TOKEN.253]}',
      canInlineRule: false,
      connections: [Connections],
      peerAsTokenCount: 0,
      directIngressRules: [],
      directEgressRules: [Array],
      allowAllOutbound: true,
      disableInlineRules: false,
      securityGroup: [CfnSecurityGroup],
      securityGroupId: '${Token[TOKEN.255]}',
      securityGroupVpcId: '${Token[TOKEN.256]}',
      securityGroupName: '${Token[TOKEN.257]}',
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    connections: <ref *3> Connections {
      _securityGroups: [ReactiveList],
      _securityGroupRules: [ReactiveList],
      skip: false,
      remoteRule: false,
      connections: [Circular *3],
      defaultPort: undefined
    },
    role: <ref *4> Role {
      node: [Node],
      stack: [Ec2InstanceStack],
      env: [Object],
      _physicalName: undefined,
      _allowCrossEnvironment: false,
      physicalName: '${Token[TOKEN.258]}',
      grantPrincipal: [Circular *4],
      principalAccount: '${Token[AWS.AccountId.7]}',
      assumeRoleAction: 'sts:AssumeRole',
      managedPolicies: [],
      attachedPolicies: [AttachedPolicies],
      dependables: Map(0) {},
      _didSplit: false,
      assumeRolePolicy: [PolicyDocument],
      inlinePolicies: {},
      permissionsBoundary: undefined,
      roleId: '${Token[TOKEN.263]}',
      roleArn: '${Token[TOKEN.264]}',
      roleName: '${Token[TOKEN.266]}',
      policyFragment: [PrincipalPolicyFragment],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    grantPrincipal: <ref *4> Role {
      node: [Node],
      stack: [Ec2InstanceStack],
      env: [Object],
      _physicalName: undefined,
      _allowCrossEnvironment: false,
      physicalName: '${Token[TOKEN.258]}',
      grantPrincipal: [Circular *4],
      principalAccount: '${Token[AWS.AccountId.7]}',
      assumeRoleAction: 'sts:AssumeRole',
      managedPolicies: [],
      attachedPolicies: [AttachedPolicies],
      dependables: Map(0) {},
      _didSplit: false,
      assumeRolePolicy: [PolicyDocument],
      inlinePolicies: {},
      permissionsBoundary: undefined,
      roleId: '${Token[TOKEN.263]}',
      roleArn: '${Token[TOKEN.264]}',
      roleName: '${Token[TOKEN.266]}',
      policyFragment: [PrincipalPolicyFragment],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    userData: LinuxUserData { props: {}, lines: [], onExitLines: [] },
    instance: CfnInstance {
      node: [Node],
      stack: [Ec2InstanceStack],
      logicalId: '${Token[Ec2InstanceStack.EC2.Instance.Amazon.Linux.2.Resource.LogicalID.275]}',
      cfnOptions: [Object],
      rawOverrides: {},
      dependsOn: Set(0) {},
      cfnResourceType: 'AWS::EC2::Instance',
      _cfnProperties: [Object],
      attrAvailabilityZone: '${Token[TOKEN.276]}',
      attrPrivateDnsName: '${Token[TOKEN.277]}',
      attrPrivateIp: '${Token[TOKEN.278]}',
      attrPublicDnsName: '${Token[TOKEN.279]}',
      attrPublicIp: '${Token[TOKEN.280]}',
      additionalInfo: undefined,
      affinity: undefined,
      availabilityZone: '${Token[TOKEN.210]}',
      blockDeviceMappings: [Array],
      cpuOptions: undefined,
      creditSpecification: undefined,
      disableApiTermination: undefined,
      ebsOptimized: undefined,
      elasticGpuSpecifications: undefined,
      elasticInferenceAccelerators: undefined,
      enclaveOptions: undefined,
      hibernationOptions: undefined,
      hostId: undefined,
      hostResourceGroupArn: undefined,
      iamInstanceProfile: '${Token[TOKEN.274]}',
      imageId: '${Token[TOKEN.270]}',
      instanceInitiatedShutdownBehavior: undefined,
      instanceType: 't3.micro',
      ipv6AddressCount: undefined,
      ipv6Addresses: undefined,
      kernelId: undefined,
      keyName: undefined,
      launchTemplate: undefined,
      licenseSpecifications: undefined,
      monitoring: undefined,
      networkInterfaces: undefined,
      placementGroupName: undefined,
      privateDnsNameOptions: undefined,
      privateIpAddress: undefined,
      propagateTagsToVolumeOnCreation: true,
      ramdiskId: undefined,
      securityGroupIds: [Array],
      securityGroups: undefined,
      sourceDestCheck: undefined,
      ssmAssociations: undefined,
      subnetId: '${Token[TOKEN.222]}',
      tags: [TagManager],
      tenancy: undefined,
      userData: '${Token[TOKEN.272]}',
      volumes: undefined,
      _logicalIdOverride: '${Token[TOKEN.282]}',
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    osType: 0,
    instanceId: '${Token[TOKEN.281]}',
    instanceAvailabilityZone: '${Token[TOKEN.276]}',
    instancePrivateDnsName: '${Token[TOKEN.277]}',
    instancePrivateIp: '${Token[TOKEN.278]}',
    instancePublicDnsName: '${Token[TOKEN.279]}',
    instancePublicIp: '${Token[TOKEN.280]}',
    [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] }
  },
  <ref *5> CfnParameter {
    node: Node {
      host: [Circular *5],
      _locked: false,
      _children: {},
      _context: {},
      _metadata: [Array],
      _dependencies: Set(0) {},
      _validations: [],
      id: 'SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn2-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter',
      scope: [Ec2InstanceStack]
    },
    stack: Ec2InstanceStack {
      node: [Node],
      _missingContext: [],
      _stackDependencies: {},
      templateOptions: {},
      _logicalIds: [LogicalIDs],
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}',
      environment: 'aws://unknown-account/unknown-region',
      terminationProtection: undefined,
      _stackName: 'Ec2InstanceStack',
      tags: [TagManager],
      artifactId: 'Ec2InstanceStack',
      templateFile: 'Ec2InstanceStack.template.json',
      _versionReportingEnabled: true,
      synthesizer: [DefaultStackSynthesizer],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    logicalId: '${Token[Ec2InstanceStack.SsmParameterValue:--aws--servi...:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter.LogicalID.269]}',
    _type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>',
    _default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2',
    _allowedPattern: undefined,
    _allowedValues: undefined,
    _constraintDescription: undefined,
    _description: undefined,
    _maxLength: undefined,
    _maxValue: undefined,
    _minLength: undefined,
    _minValue: undefined,
    _noEcho: undefined,
    [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] }
  },
  <ref *6> Import {
    node: Node {
      host: [Circular *6],
      _locked: false,
      _children: {},
      _context: {},
      _metadata: [],
      _dependencies: Set(0) {},
      _validations: [],
      id: 'SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn2-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118',
      scope: [Ec2InstanceStack]
    },
    stack: Ec2InstanceStack {
      node: [Node],
      _missingContext: [],
      _stackDependencies: {},
      templateOptions: {},
      _logicalIds: [LogicalIDs],
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}',
      environment: 'aws://unknown-account/unknown-region',
      terminationProtection: undefined,
      _stackName: 'Ec2InstanceStack',
      tags: [TagManager],
      artifactId: 'Ec2InstanceStack',
      templateFile: 'Ec2InstanceStack.template.json',
      _versionReportingEnabled: true,
      synthesizer: [DefaultStackSynthesizer],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    env: {
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}'
    },
    _physicalName: undefined,
    _allowCrossEnvironment: false,
    physicalName: '${Token[TOKEN.271]}',
    parameterName: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2',
    parameterArn: 'arn:${Token[AWS.Partition.10]}:ssm:${Token[AWS.Region.11]}:${Token[AWS.AccountId.7]}:parameter/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2',
    parameterType: 'AWS::EC2::Image::Id',
    stringValue: '${Token[TOKEN.270]}',
    [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] }
  },
  <ref *7> Instance {
    node: Node {
      host: [Circular *7],
      _locked: false,
      _children: [Object],
      _context: {},
      _metadata: [],
      _dependencies: Set(0) {},
      _validations: [],
      id: 'EC2 Instance Windows Server 2022',
      scope: [Ec2InstanceStack],
      _defaultChild: [CfnInstance]
    },
    stack: Ec2InstanceStack {
      node: [Node],
      _missingContext: [],
      _stackDependencies: {},
      templateOptions: {},
      _logicalIds: [LogicalIDs],
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}',
      environment: 'aws://unknown-account/unknown-region',
      terminationProtection: undefined,
      _stackName: 'Ec2InstanceStack',
      tags: [TagManager],
      artifactId: 'Ec2InstanceStack',
      templateFile: 'Ec2InstanceStack.template.json',
      _versionReportingEnabled: true,
      synthesizer: [DefaultStackSynthesizer],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    env: {
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}'
    },
    _physicalName: undefined,
    _allowCrossEnvironment: false,
    physicalName: '${Token[TOKEN.283]}',
    securityGroups: [ [SecurityGroup] ],
    securityGroup: SecurityGroup {
      node: [Node],
      stack: [Ec2InstanceStack],
      env: [Object],
      _physicalName: undefined,
      _allowCrossEnvironment: false,
      physicalName: '${Token[TOKEN.284]}',
      canInlineRule: false,
      connections: [Connections],
      peerAsTokenCount: 0,
      directIngressRules: [],
      directEgressRules: [Array],
      allowAllOutbound: true,
      disableInlineRules: false,
      securityGroup: [CfnSecurityGroup],
      securityGroupId: '${Token[TOKEN.286]}',
      securityGroupVpcId: '${Token[TOKEN.287]}',
      securityGroupName: '${Token[TOKEN.288]}',
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    connections: <ref *8> Connections {
      _securityGroups: [ReactiveList],
      _securityGroupRules: [ReactiveList],
      skip: false,
      remoteRule: false,
      connections: [Circular *8],
      defaultPort: undefined
    },
    role: <ref *9> Role {
      node: [Node],
      stack: [Ec2InstanceStack],
      env: [Object],
      _physicalName: undefined,
      _allowCrossEnvironment: false,
      physicalName: '${Token[TOKEN.289]}',
      grantPrincipal: [Circular *9],
      principalAccount: '${Token[AWS.AccountId.7]}',
      assumeRoleAction: 'sts:AssumeRole',
      managedPolicies: [],
      attachedPolicies: [AttachedPolicies],
      dependables: Map(0) {},
      _didSplit: false,
      assumeRolePolicy: [PolicyDocument],
      inlinePolicies: {},
      permissionsBoundary: undefined,
      roleId: '${Token[TOKEN.294]}',
      roleArn: '${Token[TOKEN.295]}',
      roleName: '${Token[TOKEN.297]}',
      policyFragment: [PrincipalPolicyFragment],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    grantPrincipal: <ref *9> Role {
      node: [Node],
      stack: [Ec2InstanceStack],
      env: [Object],
      _physicalName: undefined,
      _allowCrossEnvironment: false,
      physicalName: '${Token[TOKEN.289]}',
      grantPrincipal: [Circular *9],
      principalAccount: '${Token[AWS.AccountId.7]}',
      assumeRoleAction: 'sts:AssumeRole',
      managedPolicies: [],
      attachedPolicies: [AttachedPolicies],
      dependables: Map(0) {},
      _didSplit: false,
      assumeRolePolicy: [PolicyDocument],
      inlinePolicies: {},
      permissionsBoundary: undefined,
      roleId: '${Token[TOKEN.294]}',
      roleArn: '${Token[TOKEN.295]}',
      roleName: '${Token[TOKEN.297]}',
      policyFragment: [PrincipalPolicyFragment],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    userData: WindowsUserData { lines: [], onExitLines: [] },
    instance: CfnInstance {
      node: [Node],
      stack: [Ec2InstanceStack],
      logicalId: '${Token[Ec2InstanceStack.EC2.Instance.Windows.Server.2022.Resource.LogicalID.306]}',
      cfnOptions: [Object],
      rawOverrides: {},
      dependsOn: Set(0) {},
      cfnResourceType: 'AWS::EC2::Instance',
      _cfnProperties: [Object],
      attrAvailabilityZone: '${Token[TOKEN.307]}',
      attrPrivateDnsName: '${Token[TOKEN.308]}',
      attrPrivateIp: '${Token[TOKEN.309]}',
      attrPublicDnsName: '${Token[TOKEN.310]}',
      attrPublicIp: '${Token[TOKEN.311]}',
      additionalInfo: undefined,
      affinity: undefined,
      availabilityZone: '${Token[TOKEN.210]}',
      blockDeviceMappings: [Array],
      cpuOptions: undefined,
      creditSpecification: undefined,
      disableApiTermination: undefined,
      ebsOptimized: undefined,
      elasticGpuSpecifications: undefined,
      elasticInferenceAccelerators: undefined,
      enclaveOptions: undefined,
      hibernationOptions: undefined,
      hostId: undefined,
      hostResourceGroupArn: undefined,
      iamInstanceProfile: '${Token[TOKEN.305]}',
      imageId: '${Token[TOKEN.301]}',
      instanceInitiatedShutdownBehavior: undefined,
      instanceType: 't3.micro',
      ipv6AddressCount: undefined,
      ipv6Addresses: undefined,
      kernelId: undefined,
      keyName: undefined,
      launchTemplate: undefined,
      licenseSpecifications: undefined,
      monitoring: undefined,
      networkInterfaces: undefined,
      placementGroupName: undefined,
      privateDnsNameOptions: undefined,
      privateIpAddress: undefined,
      propagateTagsToVolumeOnCreation: true,
      ramdiskId: undefined,
      securityGroupIds: [Array],
      securityGroups: undefined,
      sourceDestCheck: undefined,
      ssmAssociations: undefined,
      subnetId: '${Token[TOKEN.222]}',
      tags: [TagManager],
      tenancy: undefined,
      userData: '${Token[TOKEN.303]}',
      volumes: undefined,
      _logicalIdOverride: '${Token[TOKEN.313]}',
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    osType: 1,
    instanceId: '${Token[TOKEN.312]}',
    instanceAvailabilityZone: '${Token[TOKEN.307]}',
    instancePrivateDnsName: '${Token[TOKEN.308]}',
    instancePrivateIp: '${Token[TOKEN.309]}',
    instancePublicDnsName: '${Token[TOKEN.310]}',
    instancePublicIp: '${Token[TOKEN.311]}',
    [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] }
  },
  <ref *10> CfnParameter {
    node: Node {
      host: [Circular *10],
      _locked: false,
      _children: {},
      _context: {},
      _metadata: [Array],
      _dependencies: Set(0) {},
      _validations: [],
      id: 'SsmParameterValue:--aws--service--ami-windows-latest--Windows_Server-2022-English-Full-Base:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter',
      scope: [Ec2InstanceStack]
    },
    stack: Ec2InstanceStack {
      node: [Node],
      _missingContext: [],
      _stackDependencies: {},
      templateOptions: {},
      _logicalIds: [LogicalIDs],
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}',
      environment: 'aws://unknown-account/unknown-region',
      terminationProtection: undefined,
      _stackName: 'Ec2InstanceStack',
      tags: [TagManager],
      artifactId: 'Ec2InstanceStack',
      templateFile: 'Ec2InstanceStack.template.json',
      _versionReportingEnabled: true,
      synthesizer: [DefaultStackSynthesizer],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    logicalId: '${Token[Ec2InstanceStack.SsmParameterValue:--aws--servi...:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter.LogicalID.300]}',
    _type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>',
    _default: '/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base',
    _allowedPattern: undefined,
    _allowedValues: undefined,
    _constraintDescription: undefined,
    _description: undefined,
    _maxLength: undefined,
    _maxValue: undefined,
    _minLength: undefined,
    _minValue: undefined,
    _noEcho: undefined,
    [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] }
  },
  <ref *11> Import {
    node: Node {
      host: [Circular *11],
      _locked: false,
      _children: {},
      _context: {},
      _metadata: [],
      _dependencies: Set(0) {},
      _validations: [],
      id: 'SsmParameterValue:--aws--service--ami-windows-latest--Windows_Server-2022-English-Full-Base:C96584B6-F00A-464E-AD19-53AFF4B05118',
      scope: [Ec2InstanceStack]
    },
    stack: Ec2InstanceStack {
      node: [Node],
      _missingContext: [],
      _stackDependencies: {},
      templateOptions: {},
      _logicalIds: [LogicalIDs],
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}',
      environment: 'aws://unknown-account/unknown-region',
      terminationProtection: undefined,
      _stackName: 'Ec2InstanceStack',
      tags: [TagManager],
      artifactId: 'Ec2InstanceStack',
      templateFile: 'Ec2InstanceStack.template.json',
      _versionReportingEnabled: true,
      synthesizer: [DefaultStackSynthesizer],
      [Symbol(@aws-cdk/core.DependableTrait)]: [Object]
    },
    env: {
      account: '${Token[AWS.AccountId.7]}',
      region: '${Token[AWS.Region.11]}'
    },
    _physicalName: undefined,
    _allowCrossEnvironment: false,
    physicalName: '${Token[TOKEN.302]}',
    parameterName: '/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base',
    parameterArn: 'arn:${Token[AWS.Partition.10]}:ssm:${Token[AWS.Region.11]}:${Token[AWS.AccountId.7]}:parameter/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base',
    parameterType: 'AWS::EC2::Image::Id',
    stringValue: '${Token[TOKEN.301]}',
    [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] }
  }
]

ニヤニヤしていると、EC2インスタンスを表すノードはdefaultChildの型がCfnInstanceであることが分かります。

これを活用して、スタックのコンストラクトノード配下でdefaultChildの型がCfnInstanceのノードのプロパティを更新してあげます。

./lib/ec2-instance-stack.ts

import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib";
import { Construct } from "constructs";

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

    // VPC
    const vpc = new ec2.Vpc(this, "VPC", {
      cidr: "10.10.0.0/24",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 0,
      maxAzs: 2,
      subnetConfiguration: [
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 },
      ],
    });

    // EC2 Instance
    new ec2.Instance(this, "EC2 Instance Amazon Linux 2", {
      instanceType: new ec2.InstanceType("t3.micro"),
      machineImage: ec2.MachineImage.latestAmazonLinux({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      vpc: vpc,
      blockDevices: [
        {
          deviceName: "/dev/xvda",
          volume: ec2.BlockDeviceVolume.ebs(8, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
      propagateTagsToVolumeOnCreation: true,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }),
    });

    new ec2.Instance(this, "EC2 Instance Windows Server 2022", {
      instanceType: new ec2.InstanceType("t3.micro"),
      machineImage: ec2.MachineImage.latestWindows(
        ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE
      ),
      vpc: vpc,
      blockDevices: [
        {
          deviceName: "/dev/sda1",
          volume: ec2.BlockDeviceVolume.ebs(30, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
      propagateTagsToVolumeOnCreation: true,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }),
    });

    // Enable EBS Optimized
    this.node.children.forEach((child) => {
      if (child.node.defaultChild instanceof ec2.CfnInstance) {
        child.node.defaultChild.ebsOptimized = true;
      }
    });
  }
}

デプロイ後、EC2インスタンスがEBS最適化インスタンスになっているかAWS CLIで確認してみます。

aws ec2 describe-instances \
    --filters Name=instance-state-name,Values=running \
  | jq -r ".Reservations[].Instances[] | [.InstanceId, .EbsOptimized, .Tags]"
[
  "i-0b8a53f3c266a70b6",
  true,
  [
    {
      "Key": "aws:cloudformation:stack-id",
      "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7"
    },
    {
      "Key": "aws:cloudformation:stack-name",
      "Value": "Ec2InstanceStack"
    },
    {
      "Key": "aws:cloudformation:logical-id",
      "Value": "EC2InstanceAmazonLinux26836F6B7"
    },
    {
      "Key": "Name",
      "Value": "Ec2InstanceStack/EC2 Instance Amazon Linux 2"
    }
  ]
]
[
  "i-0a152d994af257d05",
  true,
  [
    {
      "Key": "aws:cloudformation:logical-id",
      "Value": "EC2InstanceWindowsServer20227EF538A3"
    },
    {
      "Key": "Name",
      "Value": "Ec2InstanceStack/EC2 Instance Windows Server 2022"
    },
    {
      "Key": "aws:cloudformation:stack-name",
      "Value": "Ec2InstanceStack"
    },
    {
      "Key": "aws:cloudformation:stack-id",
      "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7"
    }
  ]
]

いずれのEC2インスタンスもEbsOptimizedtrueになっていますね。

できらぁ!ということです。

2. EBS最適化インスタンスのカスタムコンストラクトを定義する

1つ目の方法でもスタック内の全てのEC2インスタンスをEBS最適化インスタンスに出来ることが分かりました。

しかし、複数のスタックでEC2インスタンスを管理している場合は何だか忘れそうですよね?

ということで、EBS最適化インスタンスのカスタムコンストラクトを定義して、EC2インスタンスを定義する際はカスタムコンストラクトを使用するようにします。

作成したカスタムコンストラクトは以下の通りです。ebsOptimizedtrueにするだけの非常にシンプルなものです。

./lib/ebs-optimized-ec2-Instance.ts

import { aws_ec2 as ec2 } from "aws-cdk-lib";
import { Construct } from "constructs";

export class EbsOptimizedEc2Instance extends ec2.Instance {
  constructor(scope: Construct, id: string, props: ec2.InstanceProps) {
    super(scope, id, props);

    const cfnInstance = this.node.defaultChild as ec2.CfnInstance;
    cfnInstance.ebsOptimized = true;
  }
}

スタック側では作成したカスタムコンストラクトをインポートし、EC2インスタンス定義時にカスタムコンストラクトを使うように指定します。

./lib/ec2-instance-stack.ts

import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib";
import { Construct } from "constructs";
import { EbsOptimizedEc2Instance } from "./ebs-optimized-ec2-Instance";

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

    // VPC
    const vpc = new ec2.Vpc(this, "VPC", {
      cidr: "10.10.0.0/24",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 0,
      maxAzs: 2,
      subnetConfiguration: [
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 },
      ],
    });

    // EC2 Instance
    new EbsOptimizedEc2Instance(this, "EC2 Instance Amazon Linux 2", {
      instanceType: new ec2.InstanceType("t3.micro"),
      machineImage: ec2.MachineImage.latestAmazonLinux({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      vpc: vpc,
      blockDevices: [
        {
          deviceName: "/dev/xvda",
          volume: ec2.BlockDeviceVolume.ebs(8, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
      propagateTagsToVolumeOnCreation: true,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }),
    });

    new EbsOptimizedEc2Instance(this, "EC2 Instance Windows Server 2022", {
      instanceType: new ec2.InstanceType("t3.micro"),
      machineImage: ec2.MachineImage.latestWindows(
        ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE
      ),
      vpc: vpc,
      blockDevices: [
        {
          deviceName: "/dev/sda1",
          volume: ec2.BlockDeviceVolume.ebs(30, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
      propagateTagsToVolumeOnCreation: true,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }),
    });
  }
}

npx cdk diffで差分を確認しましたが、差分はないようですね。

> npx cdk diff 
Stack Ec2InstanceStack
There were no differences

できらぁ!

と、npx cdk diffして差分がないことを確認しただけで高らかに宣言するのも違う気がするので、一度EC2インスタンスを削除して再度デプロイしてみます。

デプロイ後、EC2インスタンスがEBS最適化インスタンスになっているかAWS CLIで確認してみます。

aws ec2 describe-instances \
    --filters Name=instance-state-name,Values=running \
  | jq -r ".Reservations[].Instances[] | [.InstanceId, .EbsOptimized, .Tags]"
[
  "i-0038ef076fb3b8f04",
  true,
  [
    {
      "Key": "aws:cloudformation:stack-id",
      "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7"
    },
    {
      "Key": "aws:cloudformation:logical-id",
      "Value": "EC2InstanceAmazonLinux26836F6B7"
    },
    {
      "Key": "aws:cloudformation:stack-name",
      "Value": "Ec2InstanceStack"
    },
    {
      "Key": "Name",
      "Value": "Ec2InstanceStack/EC2 Instance Amazon Linux 2"
    }
  ]
]
[
  "i-0e2a59f2be24abc90",
  true,
  [
    {
      "Key": "aws:cloudformation:stack-name",
      "Value": "Ec2InstanceStack"
    },
    {
      "Key": "aws:cloudformation:stack-id",
      "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7"
    },
    {
      "Key": "Name",
      "Value": "Ec2InstanceStack/EC2 Instance Windows Server 2022"
    },
    {
      "Key": "aws:cloudformation:logical-id",
      "Value": "EC2InstanceWindowsServer20227EF538A3"
    }
  ]
]

いずれのEC2インスタンスもEbsOptimizedtrueになっていますね。

できらぁ!ということです。

L2 Contructのプロパティが足りなくても何とかなります

AWS CDKでスタック内の全てのEC2インスタンスをEBS最適化インスタンスにしてみました。

運良くEBS最適化設定はL1 Constructにプロパティがありました。もし、L1 Constructにもプロパティがない場合はrawオーバーライドかカスタムリソースを使うことになります。いずれの方法も以下AWS公式ドキュメントで紹介されているので、ご参照ください。

また、今回使用したコードは以下リポジトリに保存しています。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!