背景
ただ起動しているだけでジリジリと地味なコストがかかるAWSのリソースは
できることなら夜間に停止したり、遊びたい時間だけ動かすなどして何とか費用を節約したいところです。
EC2やRDSなどの簡単に停止できるものはいいとして、ElastiCacheやNATゲートウェイなどの停止するには削除するしかないリソースは困りものです。
とはいえ費用なやっぱり節約したいので必要な時に新規作成し、いらなくなったら削除するという操作を毎回やる方法を考えます。
手動で削除するのは大変なので自動化したいところですが、CloudFormationでリソースを構築している場合、CloudFormationの管理下に置きつつ新規作成/削除を行う方法がよくわかっていませんでした。
今回やり方を調べましたので備忘録として記事にしておきます。
テスト用CloudFormationテンプレート
ポイントは以下の3つです。
- NATゲートウェイとそのセキュリティグループ、NATゲートウェイへのルートを作成するかどうかを
CreateNatGateway
のパラメータで指定し、その値を各リソースのCondition
に与えることで作成要否を判断する。 - プライベートサブネットにEC2インスタンスを作成し、NATゲートウェイの有無によってインターネットにアクセスできるかが変わるかを確認する。
- このテンプレート単体でテストができるようにVPCエンドポイントなどを作成していますが、参考程度に見ておいてください。
AWSTemplateFormatVersion: "2010-09-09"
Description: "Temporary Resource Test"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: "Configuration"
Parameters:
- CreateNatGateway
- ClientKeyName
Parameters:
CreateNatGateway:
Type: String
Default: "true"
AllowedValues:
- "true"
- "false"
ClientKeyName:
Type: "AWS::EC2::KeyPair::KeyName"
Conditions:
CreateNatGatewayCondition:
!Equals [!Ref CreateNatGateway, "true"]
Resources:
VPC:
Type: "AWS::EC2::VPC"
Properties:
CidrBlock: "10.1.0.0/16"
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: "default"
Tags:
-
Key: "Name"
Value: "cfn-tmp-rc-vpc"
PublicSubnet1:
Type: "AWS::EC2::Subnet"
Properties:
AvailabilityZone: !Sub "${AWS::Region}a"
CidrBlock: "10.1.0.0/20"
VpcId: !Ref VPC
MapPublicIpOnLaunch: false
Tags:
-
Key: "Name"
Value: "cfn-tmp-rc-public-subnet01"
PrivateSubnet1:
Type: "AWS::EC2::Subnet"
Properties:
AvailabilityZone: !Sub "${AWS::Region}a"
CidrBlock: "10.1.64.0/20"
VpcId: !Ref VPC
MapPublicIpOnLaunch: false
Tags:
-
Key: "Name"
Value: "cfn-tmp-rc-private-subnet01"
InternetGateway:
Type: "AWS::EC2::InternetGateway"
Properties:
Tags:
- Key: "Name"
Value: "cfn-tmp-rc-igw"
InternetGatewayAttachment:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
NatGateway:
Type: "AWS::EC2::NatGateway"
Condition: CreateNatGatewayCondition
Properties:
SubnetId: !Ref PublicSubnet1
Tags:
-
Key: "Name"
Value: "cfn-tmp-rc-nat-gw"
AllocationId: !GetAtt NatGatewayElasticIp.AllocationId
NatGatewayElasticIp:
Type: "AWS::EC2::EIP"
Condition: CreateNatGatewayCondition
Properties:
Domain: "vpc"
PublicRouteTable:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref VPC
Tags:
-
Key: "Name"
Value: "cfn-tmp-rc-public-rtb"
PrivateRouteTable:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref VPC
Tags:
-
Key: "Name"
Value: "cfn-tmp-rc-private-rtb"
PublicRoute1:
Type: "AWS::EC2::Route"
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: "0.0.0.0/0"
GatewayId: !Ref InternetGateway
PrivateRoute1:
Type: "AWS::EC2::Route"
Condition: CreateNatGatewayCondition
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: "0.0.0.0/0"
NatGatewayId: !Ref NatGateway
SubnetRouteTableAssociation1:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet1
SubnetRouteTableAssociation2:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet1
SSMEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "cfn-tmp-rc-ssm-ep-sg"
VpcId: !Ref VPC
Tags:
- Key: Name
Value: "cfn-tmp-rc-ssm-ep-sg"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !GetAtt VPC.CidrBlock
SSMEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref SSMEndpointSecurityGroup
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
SubnetIds:
- !Ref PrivateSubnet1
VpcEndpointType: Interface
VpcId: !Ref VPC
SSMEndpointMessages:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref SSMEndpointSecurityGroup
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
SubnetIds:
- !Ref PrivateSubnet1
VpcEndpointType: Interface
VpcId: !Ref VPC
SSMEndpointEC2:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref SSMEndpointSecurityGroup
ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
SubnetIds:
- !Ref PrivateSubnet1
VpcEndpointType: Interface
VpcId: !Ref VPC
Client:
Type: "AWS::EC2::Instance"
Properties:
ImageId: "ami-0df2ca8a354185e1e"
InstanceType: "t2.micro"
KeyName: !Ref ClientKeyName
AvailabilityZone: !Sub "${AWS::Region}a"
Tenancy: "default"
SubnetId: !Ref PrivateSubnet1
EbsOptimized: false
SecurityGroupIds:
- !Ref ClientSecurityGroup
SourceDestCheck: true
BlockDeviceMappings:
-
DeviceName: "/dev/xvda"
Ebs:
Encrypted: false
VolumeSize: 8
VolumeType: "gp3"
DeleteOnTermination: true
IamInstanceProfile: !Ref ClientInstanceProfile
Tags:
-
Key: "Name"
Value: "cfn-tmp-rc-client"
ClientSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: "cfn-tmp-rc-client-sg"
GroupName: "cfn-tmp-rc-client-sg"
VpcId: !Ref VPC
SecurityGroupIngress:
-
CidrIp: "0.0.0.0/0"
FromPort: 22
IpProtocol: "tcp"
ToPort: 22
SecurityGroupEgress:
-
CidrIp: "0.0.0.0/0"
IpProtocol: "-1"
ClientRole:
Type: "AWS::IAM::Role"
Properties:
Path: "/"
RoleName: "cfn-tmp-rc-client-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
MaxSessionDuration: 3600
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
ClientInstanceProfile:
Type: "AWS::IAM::InstanceProfile"
Properties:
Path: "/"
InstanceProfileName: !Ref ClientRole
Roles:
- !Ref ClientRole
使い方
ここで想定するのは以下のようなシナリオです。
- まずは何もない状態から
CreateNatGateway
をtrue
にしてNATゲートウェイに関係するリソースを作成する。 - NATゲートウェイがいらなくなったら
CreateNatGateway
をfalse
にした変更セットを作成して適用する。
NATゲートウェイの作成
- CloudFormationの「スタックの作成」より上記のテンプレートを選択し、以下のようにパラメータを入力してスタックを作成します。
-
CreateNatGateway
にはtrue
を指定します。 -
ClientKeyName
には検証用のEC2インスタンスのSSHキーを指定します。
NATゲートウェイの削除
-
作成したスタックに以下のような変更セットを作成して適用します。テンプレート自体は変更せず、
CreateNatGateway
のパラメータのみをfalse
にセットします。
-
EC2のPINGの様子を見るとPINGの応答が返ってこなくなることがわかります。ちなみにSession Managerとの通信はVPCエンドポイント経由で行っているのでNATゲートウェイの有無は関係ありません。
まとめ
- やったことは極めて単純。リソースの作成要否を各リソースの
Condition
属性で指定してやればよいだけ。
おまけ
やっぱりCloudFormationで変更セットを作るって初心者にはとっつきにくいので、自作アプリやEventBridgeなどからNATゲートウェイの作成/削除を簡単に制御出来たらいいなと思ったのでStep Functionsを作成してみました。
テンプレートに以下の内容を追加してください。
(前略)
ClientRole:
Type: "AWS::IAM::Role"
Properties:
Path: "/"
RoleName: "cfn-tmp-rc-client-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
MaxSessionDuration: 3600
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
# ここから追加
ClientInstanceProfile:
Type: "AWS::IAM::InstanceProfile"
Properties:
Path: "/"
InstanceProfileName: !Ref ClientRole
Roles:
- !Ref ClientRole
StepFunctionsStateMachine:
Type: "AWS::StepFunctions::StateMachine"
Properties:
StateMachineName: "cfn-tmp-rc-create-delete-function"
DefinitionString: |
{
"StartAt": "Create or Delete",
"States": {
"Create or Delete": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.CreateNatGateway",
"BooleanEquals": false,
"Next": "SetDelete"
}
],
"Default": "SetCreate"
},
"SetCreate": {
"Type": "Pass",
"Result": "create",
"ResultPath": "$.Operation",
"Next": "CreateChangeSet"
},
"SetDelete": {
"Type": "Pass",
"ResultPath": "$.Operation",
"Result": "delete",
"Next": "CreateChangeSet"
},
"CreateChangeSet": {
"Type": "Task",
"Parameters": {
"ChangeSetName.$": "States.Format('{}-nat-gw-{}', $.Operation, States.UUID())",
"StackName.$": "$.StackName",
"Capabilities": [
"CAPABILITY_NAMED_IAM"
],
"UsePreviousTemplate": true,
"Parameters": [
{
"ParameterKey": "CreateNatGateway",
"ParameterValue.$": "States.Format($.CreateNatGateway)"
},
{
"ParameterKey": "ClientKeyName",
"UsePreviousValue": true
}
]
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:createChangeSet",
"ResultPath": "$.CreateChangeSetResult",
"Next": "DescribeChangeSet"
},
"DescribeChangeSet": {
"Type": "Task",
"Parameters": {
"ChangeSetName.$": "$.CreateChangeSetResult.Id"
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:describeChangeSet",
"ResultPath": "$.DescribeChangeSetResult",
"Next": "CheckChangeSetStatus"
},
"CheckChangeSetStatus": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.DescribeChangeSetResult.Status",
"StringEquals": "FAILED",
"Next": "Fail"
},
{
"Variable": "$.DescribeChangeSetResult.Status",
"StringEquals": "CREATE_COMPLETE",
"Next": "ExecuteChangeSet"
}
],
"Default": "WaitForCreation"
},
"ExecuteChangeSet": {
"Type": "Task",
"End": true,
"Parameters": {
"ChangeSetName.$": "$.CreateChangeSetResult.Id"
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:executeChangeSet"
},
"Fail": {
"Type": "Fail",
"Error": "Failed to Create a Change Set"
},
"WaitForCreation": {
"Type": "Wait",
"Seconds": 5,
"Next": "DescribeChangeSet"
}
},
"Comment": "NAT Gateway Create/Destroy Flow"
}
RoleArn: !GetAtt StepFunctionExecutionRole.Arn
StateMachineType: "STANDARD"
LoggingConfiguration:
IncludeExecutionData: false
Level: "OFF"
StepFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
Path: "/"
RoleName: "cfn-tmp-rc-create-delete-step-function-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- states.amazonaws.com
Action:
- sts:AssumeRole
MaxSessionDuration: 3600
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
- "arn:aws:iam::aws:policy/AWSCloudFormationFullAccess"
Step FunctionsからCloudFormationのAPIを呼び出し、変更セットの作成と適用を行っています。
変更セットを作成しても適用可能な状態になるまで少し時間がかかるのでループで作成状態の取得をしています。
実行する際は以下のパラメータを渡して実行する必要があります。
-
CreateNatGateway
にNATゲートウェイを作成する場合はtrue
、作成しない場合はfalse
を指定します。 -
StackName
は上記のテンプレートで作成したスタックのIDを指定します。このIDはCloudFormationのスタックの詳細画面から確認できます。
上記の例ではStep Funtionsのテスト用データを示していますが、Step Functionsを呼び出すことができればどのような方法でも構いません。たとえば利用者のPCが起動したタイミングでNATゲートウェイを作るとか、自作の管理用コンソールからAWS CLIなどを使って好きなタイミングで作るとか、自由自在に操作することができるようになります。