EventBridge Schedulerを使って、特定の期間のみNATゲートウェイを有効化することを実現する

2024.04.13

こんちには。

データアナリティクス事業本部 機械学習チームの中村(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を組み合わせて活用される方のご参考になれば幸いです。

参考