1
0

Eventbridge Schedulerを使って、簡単にEC2の不要コストを削減する

Last updated at Posted at 2024-03-06

はじめに

今回は検証環境等の停止忘れによる課金を抑えるために、Eventbridge Schedulerにて自動で起動停止するYAMLを作成しました。
基本的にCFnのYAMLテンプレートをコピーすれば、だれでもすぐ利用できるように心がけてCFnのYAMLテンプレートを作成しました。
なので、とりあえず停止忘れがないように自動停止を簡単に導入したい!という人には役立つかと思います。
また、月間 1400万回の呼び出しを無料で行うことができるので、導入によるコストもほとんどかからないと思います。

目次

完成版テンプレート
背景
前提
完成版テンプレート解説
実行
試行錯誤(おまけ)
まとめ

完成版テンプレート

現時点では下記のような形に落ち着きました。
IAMも1つのテンプレート内に記載しているので、下記をまるごとコピペしてデプロイすれば、Parametersで指定したEC2を自動で起動停止できると思います。
YAMLテンプレート内の詳しい解説は「完成版テンプレート解説」を参照してください。

CFn YAMLテンプレート(折り畳んでます)
AWSTemplateFormatVersion: '2010-09-09'
Description: Scheduler Template

Parameters:
  InstanceId01:
    Type: String
    Description: Instance ID to start and stop
    ConstraintDescription: Please enter Instance ID or nothing
  InstanceId02:
    Type: String
    Description: Instance ID to start and stop
    ConstraintDescription: Please enter Instance ID or nothing
  InstanceId03:
    Type: String
    Description: Instance ID to start and stop
    ConstraintDescription: Please enter Instance ID or nothing
  InstanceId04:
    Type: String
    Description: Instance ID to start and stop
    ConstraintDescription: Please enter Instance ID or nothing
  InstanceId05:
    Type: String
    Description: Instance ID to start and stop
    ConstraintDescription: Please enter Instance ID or nothing
# ---Start Parameter--- #
  StartMinutes:
    Type: Number
    Description: Specified minutes starting instances
    ConstraintDescription: Please enter a number of 0-59
    MinValue: 0
    MaxValue: 59
  StartHours:
    Description: Specified hours starting instances
    ConstraintDescription: Please enter a number of 0-23
    Type: Number
    MinValue: 0
    MaxValue: 23
  StartDayOfWeek:
    Type: String
    Description: The day of the week starting instances
    ConstraintDescription: Please enter the day of the week
    AllowedPattern: (SUN|MON|TUE|WED|THU|FRI|SAT)((,|-)(SUN|MON|TUE|WED|THU|FRI|SAT))*
# ---Stop Parameter--- #
  StopMinutes:
    Type: Number
    Description: Specified minutes starting instances
    ConstraintDescription: Please enter a number of 0-59
    MinValue: 0
    MaxValue: 59
  StopHours:
    Description: Specified hours starting instances
    ConstraintDescription: Please enter a number of 0-23
    Type: Number
    MinValue: 0
    MaxValue: 23
  StopDayOfWeek:
    Type: String
    Description: The day of the week starting instances
    ConstraintDescription: Please enter the day of the week
    AllowedPattern: (SUN|MON|TUE|WED|THU|FRI|SAT)((,|-)(SUN|MON|TUE|WED|THU|FRI|SAT))*

Conditions:
  ExistInstanceId01: !Not [!Equals [!Ref InstanceId01, ""]]
  ExistInstanceId02: !Not [!Equals [!Ref InstanceId02, ""]]
  ExistInstanceId03: !Not [!Equals [!Ref InstanceId03, ""]]
  ExistInstanceId04: !Not [!Equals [!Ref InstanceId04, ""]]
  ExistInstanceId05: !Not [!Equals [!Ref InstanceId05, ""]]

Resources:
  ScheduleRole:
    Type: AWS::IAM::Role
    Properties: 
      RoleName: ec2-schedule-role
      Path: /
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "scheduler.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "ec2-schedule-role-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement: 
              - Sid: "AllowExecutionOfEC2StartStop"
                Effect: "Allow"
                Action:
                  - "ec2:StartInstances"
                  - "ec2:StopInstances"
                Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
                Condition:
                  StringNotEquals:
                    aws:ResourceTag/Env: "prd"
      Tags:
        - Key: Name
          Value: ec2-schedule-role

  ScheduleGroup:
    Type: AWS::Scheduler::ScheduleGroup
    Properties: 
      Name: "ec2-schedule-group"

  EC2StartSchedule:
    Type: AWS::Scheduler::Schedule
    Properties: 
      Description: "Start EC2 Instance"
      FlexibleTimeWindow: 
        Mode: "OFF"
      GroupName: !Ref ScheduleGroup
      Name: "ec2-start-schedule"
      ScheduleExpression: !Sub "cron(${StartMinutes} ${StartHours} ? * ${StartDayOfWeek} *)"
      ScheduleExpressionTimezone: "Asia/Tokyo"
      State: "ENABLED"
      Target: 
        Arn: "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
        Input: !Join
          - ""
          - - "{\"InstanceIds\": ["
            - !If [ExistInstanceId01, !Sub "\"${InstanceId01}\"", ""]
            - !If [ExistInstanceId02, !Sub ",\"${InstanceId02}\"", ""]
            - !If [ExistInstanceId03, !Sub ",\"${InstanceId03}\"", ""]
            - !If [ExistInstanceId04, !Sub ",\"${InstanceId04}\"", ""]
            - !If [ExistInstanceId05, !Sub ",\"${InstanceId05}\"", ""]
            - "]}"
        RoleArn: !GetAtt ScheduleRole.Arn
        RetryPolicy: 
          MaximumRetryAttempts: 0

  EC2StopSchedule:
    Type: AWS::Scheduler::Schedule
    Properties: 
      Description: "Stop EC2 Instance"
      FlexibleTimeWindow: 
        Mode: "OFF"
      GroupName: !Ref ScheduleGroup
      Name: "ec2-stop-schedule"
      ScheduleExpression: !Sub "cron(${StopMinutes} ${StopHours} ? * ${StopDayOfWeek} *)"
      ScheduleExpressionTimezone: "Asia/Tokyo"
      State: "ENABLED"
      Target: 
        Arn: "arn:aws:scheduler:::aws-sdk:ec2:stopInstances"
        Input: !Join
          - ""
          - - "{\"InstanceIds\": ["
            - !If [ExistInstanceId01, !Sub "\"${InstanceId01}\"", ""]
            - !If [ExistInstanceId02, !Sub ",\"${InstanceId02}\"", ""]
            - !If [ExistInstanceId03, !Sub ",\"${InstanceId03}\"", ""]
            - !If [ExistInstanceId04, !Sub ",\"${InstanceId04}\"", ""]
            - !If [ExistInstanceId05, !Sub ",\"${InstanceId05}\"", ""]
            - "]}"
        RoleArn: !GetAtt ScheduleRole.Arn
        RetryPolicy: 
          MaximumRetryAttempts: 0

背景

検証環境等のEC2の停止忘れにより無駄な課金の発生を抑えたく、使いまわせるYAMLを書いてみようと思い立ちました。
その際にLambdaを利用してタグベースで行えるのもいいかと思いましたが、コードの管理などをせずシンプルで簡単に導入したいと思ったため、マネージドサービスであるEventBridge Schedulerを利用してみました。

前提

  • VPCやサブネットなどは準備されている前提です
  • EC2もすでに準備されており、インスタンスIDが取得できる前提です

完成版テンプレート解説

こちらもポイントになりそうな部分をピックアップします。

Parametersセクション

Parameters:
  InstanceId0X:
    Type: String
    Description: Instance ID to start and stop
    ConstraintDescription: Please enter Instance ID or nothing
  • この部分は起動停止対象となるインスタンスIDを入力するためのパラメータ
  • 01から順番入力する

現時点では5つまで入力可能ですが、それ以上増やす場合は記載を追加する

# ---Start Parameter--- #
  StartMinutes:
    Type: Number
    Description: Specified minutes starting instances
    ConstraintDescription: Please enter a number of 0-59
    MinValue: 0
    MaxValue: 59
  StartHours:
    Description: Specified hours starting instances
    ConstraintDescription: Please enter a number of 0-23
    Type: Number
    MinValue: 0
    MaxValue: 23
  StartDayOfWeek:
    Type: String
    Description: The day of the week starting instances
    ConstraintDescription: Please enter the day of the week
    AllowedPattern: (SUN|MON|TUE|WED|THU|FRI|SAT)((,|-)(SUN|MON|TUE|WED|THU|FRI|SAT))*
# ---Stop Parameter--- #
  StopMinutes:
    Type: Number
    Description: Specified minutes starting instances
    ConstraintDescription: Please enter a number of 0-59
    MinValue: 0
    MaxValue: 59
  StopHours:
    Description: Specified hours starting instances
    ConstraintDescription: Please enter a number of 0-23
    Type: Number
    MinValue: 0
    MaxValue: 23
  StopDayOfWeek:
    Type: String
    Description: The day of the week starting instances
    ConstraintDescription: Please enter the day of the week
    AllowedPattern: (SUN|MON|TUE|WED|THU|FRI|SAT)((,|-)(SUN|MON|TUE|WED|THU|FRI|SAT))*
  • Start Parameterの部分はEC2インスタンスを起動する「分」、「時間」、「曜日」を入力するためのパラメータ
  • Stop Parameter部分はEC2インスタンスを停止する「分」、「時間」、「曜日」を入力するためのパラメータ
  • MinValue,MaxValue,AllowedPatternで適切な値を入力していない際はエラーがでるように入力規制を実施

今回は曜日単位での起動停止を想定したため、曜日と時間のみとなっているが、年月日を指定したい場合は
Parametersに追加し、ResourcesセクションのScheduleExpression:の値を、追加したParametersを参照するよう修正する


Conditionsセクション

Conditions:
  ExistInstanceId01: !Not [!Equals [!Ref InstanceId01, ""]]
  ExistInstanceId02: !Not [!Equals [!Ref InstanceId02, ""]]
  ExistInstanceId03: !Not [!Equals [!Ref InstanceId03, ""]]
  ExistInstanceId04: !Not [!Equals [!Ref InstanceId04, ""]]
  ExistInstanceId05: !Not [!Equals [!Ref InstanceId05, ""]]

IF関数を使うためにConditionsを設定します。
Conditionsの内容としては、InstanceId0X(Parametersで入力する)の値が空欄でなければTrue、空欄だったらFalseを返します。

現時点では5つまで入力可能ですが、それ以上増やす場合は記載を追加する


Resourcesセクション

  • IAM

          Policies:
          - PolicyName: "ec2-schedule-role-policy"
            PolicyDocument:
              Version: "2012-10-17"
              Statement: 
                - Sid: "AllowExecutionOfEC2StartStop"
                  Effect: "Allow"
                  Action:
                    - "ec2:StartInstances"
                    - "ec2:StopInstances"
                  Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
                  Condition:
                    StringNotEquals:
                      aws:ResourceTag/Env: "prd"
    
    • 環境タグ(Env)に「prd」がない場合のみ、EventBridge SchedulerがEC2の起動停止ができるようなEC2の権限を付与
    • EventBridgeに付けるIAMロールによって特定のタグ以外の起動停止を許可することで、万が一起動停止したくないEC2を指定してしまった場合でも、権限不足により起動停止できないように制御する
  • EventBridge Scheduler

    ScheduleExpression: !Sub "cron(${StartMinutes} ${StartHours} ? * ${StartDayOfWeek} *)"
    
    • cron形式で記載する必要がある
    • 変数部分は!Subで起動する「分」、「時間」、「曜日」をParametersから取得する

    今回は曜日単位での起動停止を想定したため、曜日と時間のみとなっているが、年月日を指定したい場合は*?になっている部分を、Parametersの値を参照するように修正する

          Input: !Join
            - ""
            - - "{\"InstanceIds\": ["
              - !If [ExistInstanceId01, !Sub "\"${InstanceId01}\"", ""]
              - !If [ExistInstanceId02, !Sub ",\"${InstanceId02}\"", ""]
              - !If [ExistInstanceId03, !Sub ",\"${InstanceId03}\"", ""]
              - !If [ExistInstanceId04, !Sub ",\"${InstanceId04}\"", ""]
              - !If [ExistInstanceId05, !Sub ",\"${InstanceId05}\"", ""]
              - "]}"
    
    • EventBridge Schedulerによる操作対象となるEC2の指定
    • !Joinを用いてInputセクションをJSON形式で結合する
    • !Join内にて、IF関するを利用し、ConditionsがTrueであれば!SubでParametersから取得したインスタンスIDの値を入れ、Falseであれば何も入れない処理を行う

    現時点では5つまで入力可能ですが、それ以上増やす場合はParametersとConditionsの記載に加え、ここの
    !Ifの記載も増やす必要があります。

    台数を増やす際の修正箇所が増えたりして、あまりやりたくなかったですが、今回はこの方法しか思いつけなかったため、こちらで実装しました。

実行

  • CFnのYAMLテンプレートをデプロイ
    まず、完成版テンプレートで記載したYAMLを用いて、CFnのスタックを作成します。

スタック作成画面から「ファイル選択」から完成版テンプレートで作成したCFnのYAMLテンプレートを選んで、スタックを作成していきます。
image.png

下記のスタックの詳細の画面で起動停止するEC2のインスタンスIDと、起動停止する「分」、「時間」、「曜日」を指定します。
今回は下記のように設定しています

  • EC2インスタンスは2台(InstanceId01とInstanceId02のみ入力)を対象としています
  • 起動時間は16:05
  • 停止時間は16:15
  • 曜日は起動停止ともにMON-FRIの平日を対象としています

image.png

image.png

そのあとは特に何も変えずに「次へ」で進みます。

レビュー画面で「AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。」の注意にチェックを入れて、「送信」を押下します。
image.png

問題なくスタックの作成ができました。
image.png

Eventbridge Schedulerに開始のスケジュールと停止のスケジュールが作成されています。
image.png

  • 設定値の確認
    下記のようにきちんと想定通りの設定ができていました。
    なお、インスタンスIDはマスクしていますが、Parametersに入力した値と同一の値がきちんと設定されてました。
    ※開始のスケジュールとターゲット
    スケジュール
    image.png

ターゲット
image.png

※停止のスケジュールとターゲット
スケジュール
image.png

ターゲット
image.png

  • 動作確認

最初、EC2は停止された状態で存在しています。
image.png

16:05になると指定した2つのEC2が起動しました。
起動時刻を見ると、ちゃんと16:05に起動されていることがわかります。
image.png

続いて16:15になると指定した2つのEC2が停止しました。
image.png

2つのEC2の状態遷移時刻を確認すると、ちゃんと16:15にShutdownされていることが分かります(表記がGMTなので+9時間すると16:15になります)
image.png
image.png

これで動作的にも意図通りに動くことが確認できたと思います。

試行錯誤(おまけ)

最初はMappingsを使うことで、インスタンスの個数に合わせた起動停止を行おうとしました。
理由としては、できるだけResourceの方には手を加えずにインスタンス数の増加などに対応できた方が便利だと思ったからです。
こちらの方法が実現できた場合(今回はできませんでしたが)、インスタンス数を増やす際はParametersとMappingsを少し編集するだけで済むはずでした。

以下が最初のYAMLになります。

注意
こちらはうまく機能しません

【試行錯誤段階】CFn YAMLテンプレート(折り畳んでます)
AWSTemplateFormatVersion: '2010-09-09'
Description: Scheduler Template

#下記の言語拡張を利用することで、FindMap内で<!Sub>が利用できるようになる
Transform: AWS::LanguageExtensions 
Parameters:
  CountInstances:
    Type: String
    Description: Please select the number of Instances
    AllowedValues:
      - 1Instance
      - 2Instances
      - 3Instances
      - 4Instances
      - 5Instances
  InstanceId01:
    Type: String
  InstanceId02:
    Type: String
  InstanceId03:
    Type: String
  InstanceId04:
    Type: String
  InstanceId05:
    Type: String
# ---Start Parameter--- #
  StartMinutes:
    Type: Number
    Description: Specified minutes starting instances
    ConstraintDescription: Please enter a number of 0-59
    MinValue: 0
    MaxValue: 59
  StartHours:
    Description: Specified hours starting instances
    ConstraintDescription: Please enter a number of 0-23
    Type: Number
    MinValue: 0
    MaxValue: 23
  StartDayOfWeek:
    Type: String
    Description: The day of the week starting instances
    ConstraintDescription: Please enter the day of the week
    AllowedPattern: (SUN|MON|TUE|WED|THU|FRI|SAT)((,|-)(SUN|MON|TUE|WED|THU|FRI|SAT))*
# ---Stop Parameter--- #
  StopMinutes:
    Type: Number
    Description: Specified minutes starting instances
    ConstraintDescription: Please enter a number of 0-59
    MinValue: 0
    MaxValue: 59
  StopHours:
    Description: Specified hours starting instances
    ConstraintDescription: Please enter a number of 0-23
    Type: Number
    MinValue: 0
    MaxValue: 23
  StopDayOfWeek:
    Type: String
    Description: The day of the week starting instances
    ConstraintDescription: Please enter the day of the week
    AllowedPattern: (SUN|MON|TUE|WED|THU|FRI|SAT)((,|-)(SUN|MON|TUE|WED|THU|FRI|SAT))*

Mappings:
#下記の<Input>の部分がインスタンスの数ごとに、変わることでEventbridge Schedulerを適用するターゲットを変化させる想定
  1Instance:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\" ] }"
  2Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\" ] }"
  3Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\",\"${InstanceId03}\" ] }"
  4Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\",\"${InstanceId03}\",\"${InstanceId04}\" ] }"
  5Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\",\"${InstanceId03}\",\"${InstanceId04}\",\"${InstanceId05}\" ] }"

Resources:
# ------------------------------------------------------------ #
#  IAM Role for EC2 Schedule
# ------------------------------------------------------------ #
  EC2ScheduleRole:
    Type: AWS::IAM::Role
    Properties: 
      RoleName: eventbridge-schedule-ec2-role
      Description: IAM Role for Start and Stop EC2
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "scheduler.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: AllowCntrolInstances
          PolicyDocument: 
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "ec2:StartInstances"
                  - "ec2:StopInstances"
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*
# ------------------------------------------------------------ #
#  EventBridge Scheduler for EC2
# ------------------------------------------------------------ #
  EC2ScheduleGroup:
    Type: AWS::Scheduler::ScheduleGroup
    Properties: 
      Name: "ec2-schedule-group"

  EC2StartSchedule:
    Type: AWS::Scheduler::Schedule
    Properties: 
      Description: "Start EC2 Instance"
      FlexibleTimeWindow: 
        Mode: "OFF"
      GroupName: !Ref EC2ScheduleGroup
      Name: "ec2-start-schedule"
      ScheduleExpression: !Sub "cron(${StartMinutes} ${StartHours} ? * ${StartDayOfWeek} *)"
      ScheduleExpressionTimezone: "Asia/Tokyo"
      State: "ENABLED"
      Target: 
        Arn: "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
# 下記のFindMap関数でMappingsのどの値を取得するか決まる
# 構文)!FindMap [ Mappingsの1つ目のキー(今回はParametersのCountInstancesを参照する), Mappingsの2つ目のキー(EventTargetの部分), Mappingsの3つ目のキー(Inputの部分)]
        Input: !FindInMap [ !Ref CountInstances, EventTarget, !Sub Input ]
        RoleArn: !GetAtt EC2ScheduleRole.Arn

  EC2StopSchedule:
    Type: AWS::Scheduler::Schedule
    Properties: 
      Description: "Stop EC2 Instance"
      FlexibleTimeWindow: 
        Mode: "OFF"
      GroupName: !Ref EC2ScheduleGroup
      Name: "ec2-stop-schedule"
      ScheduleExpression: !Sub "cron(${StopMinutes} ${StopHours} ? * ${StopDayOfWeek} *)"
      ScheduleExpressionTimezone: "Asia/Tokyo"
      State: "ENABLED"
      Target: 
        Arn: "arn:aws:scheduler:::aws-sdk:ec2:stopInstances"
         Input: !FindInMap [ !Ref CountInstances, EventTarget, !Sub Input ]
        RoleArn: !GetAtt EC2ScheduleRole.Arn
ポイントになりそうな部分をピックアップしていきます。

言語拡張というものを適用することで、FindInMap内で使える関数が増えるということだったので、これでMappingsの変数を!Subで変えられないかな、と思い試してみました。

下記を入れることで!FindInMap内で!Subを使っても構文エラーにならなくなりました。

Transform: AWS::LanguageExtensions 

!FindInMap内で!Subを使いたかった理由として、Mappingsの中で関数を使うことが出来ないので、Mappingsから持ってきた値に!Subを使えないかと思ったからです。
なお、!Subで下記の${InstanceId0X}の部分にParametersの値を代入してもらう想定でした。

Mappings
~
  1Instance:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\" ] }"
  2Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\" ] }"
  3Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\",\"${InstanceId03}\" ] }"
  4Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\",\"${InstanceId03}\",\"${InstanceId04}\" ] }"
  5Instances:
    EventTarget:
      Input: "{ \"InstanceIds\": [ \"${InstanceId01}\",\"${InstanceId02}\",\"${InstanceId03}\",\"${InstanceId04}\",\"${InstanceId05}\" ] }"

下記のInputのところでJSON方式でターゲットを入れるのですが、ここでMappingsから持ってきた値の変数を!Subで置き換える予定でした…

Resources:
~
~
  EC2StartSchedule:
    ~
    ~
    Target:
       Input: !FindInMap [ !Ref CountInstances, EventTarget, !Sub Input ]

しかし、結果としては変数が置き換わることなくEventbridge Schedulerのターゲットは下記のようになっており、Parametersで入力した値に置き換わっていませんでした。
補足:Parametersの部分は1Instanceで実施しました。

{ "InstanceIds": [ "${InstanceId01}"]}

この後もMappingsを利用したまま、どうにかできないかと試行錯誤しましたが、自分が試した限りできませんでした。(こうしたらできるなどありましたら、ご指摘ください。)

まとめ

今回は汎用的に検証環境等で自動起動停止をするEventbridge SchedulerをCFnで作成しましたが、もう少しEC2の枠の増加等がやりやすい方法はないかなあとは思いました。
今回のYAMLでも意図通りに起動停止はできたので、とりあえず簡単にEC2の自動起動停止を導入したいという要件は達成できたかと思います。

参考になれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0