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,
}),
});
}
}
これは困った
ということで、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
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最適化が有効になりました。
できらぁ!
スタック内の全てのEC2インスタンスをまとめて設定する
複数のEC2インスタンスをスタック内で定義している場合に面倒だな
EC2インスタンスが数台しかない場合は、上述した方法で対応するのも良いでしょう。
しかし、EC2インスタンスが複数ある場合に都度設定するのもちょっと面倒です。
ということで、以下2つの方法で複数のEC2インスタンスをまとめてEBS最適化インスタンスにしてみます。
- スタックの
node.children
からCfnInstance
のノードを探してプロパティを上書きする - 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インスタンスもEbsOptimized
がtrue
になっていますね。
できらぁ!ということです。
2. EBS最適化インスタンスのカスタムコンストラクトを定義する
1つ目の方法でもスタック内の全てのEC2インスタンスをEBS最適化インスタンスに出来ることが分かりました。
しかし、複数のスタックでEC2インスタンスを管理している場合は何だか忘れそうですよね?
ということで、EBS最適化インスタンスのカスタムコンストラクトを定義して、EC2インスタンスを定義する際はカスタムコンストラクトを使用するようにします。
作成したカスタムコンストラクトは以下の通りです。ebsOptimized
をtrue
にするだけの非常にシンプルなものです。
./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インスタンスもEbsOptimized
がtrue
になっていますね。
できらぁ!ということです。
L2 Contructのプロパティが足りなくても何とかなります
AWS CDKでスタック内の全てのEC2インスタンスをEBS最適化インスタンスにしてみました。
運良くEBS最適化設定はL1 Constructにプロパティがありました。もし、L1 Constructにもプロパティがない場合はrawオーバーライドかカスタムリソースを使うことになります。いずれの方法も以下AWS公式ドキュメントで紹介されているので、ご参照ください。
また、今回使用したコードは以下リポジトリに保存しています。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!