はじめに
今回は検証環境等の停止忘れによる課金を抑えるために、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テンプレートを選んで、スタックを作成していきます。
下記のスタックの詳細の画面で起動停止するEC2のインスタンスIDと、起動停止する「分」、「時間」、「曜日」を指定します。
今回は下記のように設定しています
- EC2インスタンスは2台(InstanceId01とInstanceId02のみ入力)を対象としています
- 起動時間は
16:05
- 停止時間は
16:15
- 曜日は起動停止ともに
MON-FRI
の平日を対象としています
そのあとは特に何も変えずに「次へ」で進みます。
レビュー画面で「AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。」の注意にチェックを入れて、「送信」を押下します。
Eventbridge Schedulerに開始のスケジュールと停止のスケジュールが作成されています。
-
設定値の確認
下記のようにきちんと想定通りの設定ができていました。
なお、インスタンスIDはマスクしていますが、Parametersに入力した値と同一の値がきちんと設定されてました。
※開始のスケジュールとターゲット
スケジュール
- 動作確認
16:05になると指定した2つのEC2が起動しました。
起動時刻を見ると、ちゃんと16:05に起動されていることがわかります。
続いて16:15になると指定した2つのEC2が停止しました。
2つのEC2の状態遷移時刻を確認すると、ちゃんと16:15にShutdownされていることが分かります(表記がGMTなので+9時間すると16:15になります)
これで動作的にも意図通りに動くことが確認できたと思います。
試行錯誤(おまけ)
最初は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の自動起動停止を導入したいという要件は達成できたかと思います。
参考になれば幸いです。