こんちには。
データアナリティクス事業本部 機械学習チームの中村(nokomoro3)です。
本記事ではEventBridge Schedulerを使って、特定の期間のみNATゲートウェイを有効化することを実現したいと思います。
NATゲートウェイを有効化・無効化については以下を参考とし、EventBridge Schedulerではこの対象のテンプレートのUpdateStackを実行することで実現していきます。
作成するテンプレート
3つのテンプレートを作成してS3バケットに配置します。
今回はs3://cm-nakamura-sample-20240413/cloudformation/
にテンプレートを配置したとして進めます。
target.yml
は今回のEventBridge Schedulerでupdate-stackの対象となるVPCやSubnet、NATゲートウェイなどのリソースを定義します。
scheduler.yml
はEventBridge Schedulerの定義で、NATゲートウェイの起動と削除それぞれをSchedulerとして定義しています。
instance.yml
は動作確認用のEC2やSession Managerで接続するためのVPCエンドポイントの設定を記載しています。
target.ymlの定義
target.yml
ではEventBridge Schedulerでupdate-stackの対象となるVPCやSubnet、NATゲートウェイなどのリソースを定義します。
以下の元記事を参考に、PublicとPrivateの2個のSubnetだけのシンプルな構成にしました。
元記事と同様ですが、Parameterの"EnableNatGateway"
の部分がtrueであればNATゲートウェイが追加され、falseだと削除されるような形となります。
また、NATゲートウェイの作成に伴うEIPやRouteの追加もポイントになります。
AWSTemplateFormatVersion: "2010-09-09"
Description: Network Layer Template
#------------------------------------------------------------------------------
# Parameters
#------------------------------------------------------------------------------
Parameters:
SystemName:
Description: This value is used as the resource prefix.
Type: String
MinLength: 1
Default: example
Env:
Description: Environment Name
Type: String
Default: dev
AllowedValues:
- dev
- prd
VpcCidr:
Description: First and Second Octet of VPC, For example (10.0/172.16/192.168)
Type: String
Default: 10.0
AllowedPattern: "^(10\\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|172\\.(1[6-9]|2[0-9]|3[0-1])|192\\.168)$"
ConstraintDescription: xxx.xxx
EnableNatGateway:
Description: Enable NAT Gateway.
Type: String
Default: false
AllowedValues: [true, false]
#------------------------------------------------------------------------------
# Conditions
#------------------------------------------------------------------------------
Conditions:
EnableNatGateway:
!Equals [true, !Ref EnableNatGateway]
#------------------------------------------------------------------------------
# Mappings
#------------------------------------------------------------------------------
Mappings:
VpcConfig:
dev:
Vpc : .0.0/16
PublicSubnet : .0.0/24
PrivateSubnet: .10.0/24
#------------------------------------------------------------------------------
# Resources
#------------------------------------------------------------------------------
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Sub [ "${VpcCidr}${Subnet}", { Subnet: !FindInMap [ VpcConfig, !Ref Env, Vpc ]}]
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Env}-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Env}-igw
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref InternetGateway
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Env}-public-rtb
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Env}-private-rtb
PrivateRoute:
Type: AWS::EC2::Route
Condition: EnableNatGateway
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, "Fn::GetAZs": { Ref: "AWS::Region" } ]
CidrBlock: !Sub [ "${VpcCidr}${Subnet}", { Subnet: !FindInMap [ VpcConfig, !Ref Env, PublicSubnet ]}]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Env}-public-subnet
VpcId: !Ref Vpc
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, "Fn::GetAZs": { Ref: "AWS::Region" } ]
CidrBlock: !Sub [ "${VpcCidr}${Subnet}", { Subnet: !FindInMap [ VpcConfig, !Ref Env, PrivateSubnet ]}]
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Env}-private-subnet
VpcId: !Ref Vpc
PrivateRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet
RouteTableId: !Ref PrivateRouteTable
NatGateway:
Type: AWS::EC2::NatGateway
Condition: EnableNatGateway
Properties:
AllocationId: !GetAtt NatGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Env}-ngw
NatGatewayEIP:
Type: AWS::EC2::EIP
Condition: EnableNatGateway
Properties:
Domain: vpc
Outputs:
Vpc:
Value: !Ref Vpc
Export:
Name: !Sub ${SystemName}-${Env}-vpc
PublicSubnet:
Value: !Ref PublicSubnet
Export:
Name: !Sub ${SystemName}-${Env}-public-subnet
PrivateSubnet:
Value: !Ref PrivateSubnet
Export:
Name: !Sub ${SystemName}-${Env}-private-subnet
instance.ymlの定義
instance.ymlは動作確認用のEC2やSession Managerで接続するためのVPCエンドポイントの設定を記載しています。EC2はPrivateSubnetに配置します。
こちらのテンプレートは以下の記事を参考にいたしました。
なおAMIはal2023-ami-minimal-*
を使用しないようご注意ください。minimalだとSession Managerによる接続ができなくなるようです。
AWSTemplateFormatVersion:
"2010-09-09"
Description:
AWS Private Subnet EC2 Launch and Connect by SSM
#------------------------------------------------------------------------------
# Parameters
#------------------------------------------------------------------------------
Parameters:
InstanceType:
Type: String
Default: "t2.nano"
VPCId:
Type: String
SubnetId:
Type: String
PJPrefix:
Type: String
Default: "cm-nakamura-ec2"
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64'
#------------------------------------------------------------------------------
# Resources
#------------------------------------------------------------------------------
Resources:
EC2SG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Bastion EC2 Security Group"
VpcId: !Ref VPCId
Tags:
- Key: Name
Value: !Sub "${PJPrefix}-ec2-bastion-sg"
EndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Bastion Endpoint Security Group"
VpcId: !Ref VPCId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref EC2SG
Tags:
- Key: Name
Value: !Sub "${PJPrefix}-endpoint-bastion-sg"
IamRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${PJPrefix}-ec2-bastion-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref IamRole
EC2Instance:
Type: "AWS::EC2::Instance"
Properties:
Tags:
- Key: Name
Value: !Sub "${PJPrefix}-bastion"
ImageId: !Ref LatestAmiId
InstanceType: !Ref InstanceType
SecurityGroupIds:
- !Ref EC2SG
SubnetId: !Ref SubnetId
IamInstanceProfile: !Ref InstanceProfile
SSMVPCEndpoint:
Type: "AWS::EC2::VPCEndpoint"
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssm'
VpcId: !Ref VPCId
SubnetIds:
- !Ref SubnetId
SecurityGroupIds:
- !Ref EndpointSG
SSMMessagesVPCEndpoint:
Type: "AWS::EC2::VPCEndpoint"
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssmmessages'
VpcId: !Ref VPCId
SubnetIds:
- !Ref SubnetId
SecurityGroupIds:
- !Ref EndpointSG
EC2MessagesVPCEndpoint:
Type: "AWS::EC2::VPCEndpoint"
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ec2messages'
VpcId: !Ref VPCId
SubnetIds:
- !Ref SubnetId
SecurityGroupIds:
- !Ref EndpointSG
まずは手動で動作確認
EventBridge Schedulerで動作させる前に、まずはAWS CLIから動作確認していきます。
最初にtarget.yml
のcreate-stackを実行します。
aws --profile $PROFILE --region us-east-1 \
cloudformation create-stack \
--stack-name cm-nakamura-stack \
--template-url https://cm-nakamura-sample-20240413.s3.amazonaws.com/cloudformation/base.yml \
--parameters ParameterKey=SystemName,ParameterValue=cm-nakamura-sample
デフォルトではNATゲートウェイは作成されていません。
次に検証用のEC2インスタンスを作成します。
VPC_ID=$(aws --profile $PROFILE --region us-east-1 \
cloudformation describe-stacks \
--stack-name cm-nakamura-stack \
| jq -r '.Stacks[] | .Outputs[] | select(.OutputKey == "Vpc") | .OutputValue')
PRIVATE_SUBNET_ID=$(aws --profile $PROFILE --region us-east-1 \
cloudformation describe-stacks \
--stack-name cm-nakamura-stack \
| jq -r '.Stacks[] | .Outputs[] | select(.OutputKey == "PrivateSubnet") | .OutputValue')
aws --profile $PROFILE --region us-east-1 \
cloudformation create-stack \
--stack-name cm-nakamura-instance-stack \
--template-url https://cm-nakamura-sample-20240413.s3.amazonaws.com/cloudformation/instance.yml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters "ParameterKey=VPCId,ParameterValue=${VPC_ID}" "ParameterKey=SubnetId,ParameterValue=${PRIVATE_SUBNET_ID}"
起動がおわりましたら、マネジメントコンソールからSession Managerを使ってEC2インスタンスに接続してpingしてみましょう。
想定通り失敗します。
ping www.google.com -c 5
# PING www.google.com (172.253.63.147) 56(84) bytes of data.
#
# --- www.google.com ping statistics ---
# 5 packets transmitted, 0 received, 100% packet loss, time 4130ms
次に、update-stackでNATゲートウェイを作成します。
前述の通りParameterKey=EnableNatGateway,ParameterValue=true
の部分がポイントです。
aws --profile $PROFILE --region us-east-1 \
cloudformation update-stack \
--stack-name cm-nakamura-stack \
--use-previous-template \
--parameters ParameterKey=SystemName,ParameterValue=cm-nakamura-sample ParameterKey=EnableNatGateway,ParameterValue=true
再度、マネジメントコンソールからEC2インスタンスに接続してpingしてみましょう。
次は想定通り成功するようになっています。
ping www.google.com -c 5
# PING www.google.com (142.251.16.147) 56(84) bytes of data.
# 64 bytes from bl-in-f147.1e100.net (142.251.16.147): icmp_seq=1 ttl=57 time=3.61 ms
# 64 bytes from bl-in-f147.1e100.net (142.251.16.147): icmp_seq=2 ttl=57 time=2.56 ms
# 64 bytes from bl-in-f147.1e100.net (142.251.16.147): icmp_seq=3 ttl=57 time=2.56 ms
# 64 bytes from bl-in-f147.1e100.net (142.251.16.147): icmp_seq=4 ttl=57 time=2.54 ms
# 64 bytes from bl-in-f147.1e100.net (142.251.16.147): icmp_seq=5 ttl=57 time=2.54 ms
# --- www.google.com ping statistics ---
# 5 packets transmitted, 5 received, 0% packet loss, time 4005ms
# rtt min/avg/max/mdev = 2.540/2.764/3.613/0.424 ms
再び、update-stackでNATゲートウェイを削除しておきます。
aws --profile $PROFILE --region us-east-1 \
cloudformation update-stack \
--stack-name cm-nakamura-stack \
--use-previous-template \
--parameters ParameterKey=SystemName,ParameterValue=cm-nakamura-sample ParameterKey=EnableNatGateway,ParameterValue=false
scheduler.ymlの定義
EventBridge Schedulerのテンプレートを見ていきます。
こちらは先ほど手動で行ったNATゲートウェイの起動と削除それぞれSchedulerとして定義しています。
こちらは実際にはマネジメントコンソールから作成・テストして、IaCジェネレータを用いて出力したものを整形しました。
IaCジェネレータについては以下を参照ください。
AWSTemplateFormatVersion: "2010-09-09"
Description: NatGateway up down Schedulers
Parameters:
TargetStackName:
Description: This value is used as the resource prefix.
Type: String
MinLength: 1
Default: example-stack
Resources:
IAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: "eventbridge-execution-role"
Description: "Role for EventBridge Scheduler"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
Service:
- "scheduler.amazonaws.com"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/PowerUserAccess"
SchedulerUp:
Type: "AWS::Scheduler::Schedule"
Properties:
Name: "cm-nakamura-up-natgw"
GroupName: "default"
ScheduleExpression: "cron(00 10 13 * ? *)"
ScheduleExpressionTimezone: "Asia/Tokyo"
FlexibleTimeWindow:
Mode: "OFF"
State: "ENABLED"
Target:
Arn: "arn:aws:scheduler:::aws-sdk:cloudformation:updateStack"
Input: !Sub |-
{
"StackName": "${TargetStackName}",
"UsePreviousTemplate": "true",
"Parameters": [
{
"ParameterKey": "SystemName",
"ParameterValue": "cm-nakamura-sample"
},
{
"ParameterKey": "EnableNatGateway",
"ParameterValue": "true"
}
]
}
RoleArn:
Fn::GetAtt:
- "IAMRole"
- "Arn"
SchedulerDown:
Type: "AWS::Scheduler::Schedule"
Properties:
Name: "cm-nakamura-down-natgw"
GroupName: "default"
ScheduleExpression: "cron(30 10 13 * ? *)"
ScheduleExpressionTimezone: "Asia/Tokyo"
FlexibleTimeWindow:
Mode: "OFF"
State: "ENABLED"
Target:
Arn: "arn:aws:scheduler:::aws-sdk:cloudformation:updateStack"
Input: !Sub |-
{
"StackName": "${TargetStackName}",
"UsePreviousTemplate": "true",
"Parameters": [
{
"ParameterKey": "SystemName",
"ParameterValue": "cm-nakamura-sample"
},
{
"ParameterKey": "EnableNatGateway",
"ParameterValue": "false"
}
]
}
RoleArn:
Fn::GetAtt:
- "IAMRole"
- "Arn"
作成するリソースはScheduler向けのIAMロールと、Schedulerの2つとなります。
2つのSchedulerはそれぞれ対象となるスタックを更新しますが、対象スタックに対するパラメータの"EnableNatGateway"
の部分がtrue, falseと異なる形となります。
前述の通りtrueだとNATゲートウェイが追加され、falseだとNATゲートウェイが削除されます。
スケジューラの実行時間はcronで設定しており、毎月13日の10時に起動され、毎月13日の10時30分に停止されるような設定内容となります。ここは必要に応じて修正してお使いください。
動作確認
まずはscheduler.yml
のcreate-stackを行います。
aws --profile $PROFILE --region us-east-1 \
cloudformation create-stack \
--stack-name cm-nakamura-scheduler-stack \
--template-url https://cm-nakamura-sample-20240413.s3.amazonaws.com/cloudformation/scheduler.yml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters ParameterKey=TargetStackName,ParameterValue=cm-nakamura-stack
時間まで待ち、まずは想定通りに13日の10時にNATゲートウェイとその他のリソースが作成されることを確認できました。
EC2からの動作確認は必要に応じて行われてください。(ここでは割愛いたします)
次に13日の10時30分にNATゲートウェイとその他のリソースが削除されることを確認できました。
後片付け
作成したスタックを一通り削除しておきましょう。
aws --profile $PROFILE --region us-east-1 \
cloudformation delete-stack \
--stack-name cm-nakamura-scheduler-stack
aws --profile $PROFILE --region us-east-1 \
cloudformation delete-stack \
--stack-name cm-nakamura-instance-stack
aws --profile $PROFILE --region us-east-1 \
cloudformation delete-stack \
--stack-name cm-nakamura-stack
まとめ
いかがでしたでしょうか。本記事がCloudFormationとEventBridge Schedulerを組み合わせて活用される方のご参考になれば幸いです。
参考
- 時限式CloudFormation を作成してみた | DevelopersIO
- 時限式CloudFormation を作成してみた その2 | DevelopersIO
- ECSとStep Functionsでdbtを動かし、Redshiftへデータを連携するデータパイプラインを構築する | DevelopersIO
- 必要な時だけNAT Gatewayを作成する方法 | DevelopersIO
- EventBridgeからEventBridge Schedulerに移行してみる #Python - Qiita
- AWS CloudFormation IaC generatorを試してみた。 | DevelopersIO
- スタックの作成 - AWS CloudFormation
- cron 式のリファレンス - Amazon EventBridge
- EventBridge Schedulerを使ってEC2の起動停止を行う(できればCFnまで)
- 【CloudFormation】一撃でプライベートサブネットにEC2を起動し、SSMポートフォワード経由でRDPする | DevelopersIO
- AWS CloudFormation IaC generatorを試してみた。 | DevelopersIO