🚗

EC2 自動起動・停止を Step Functions で運用する

2024/03/25に公開

AWS Lambda や自前のプログラムを一切書かずに、ノーコードで EC2 インスタンスを指定した時刻で起動したり停止する運用をします。

AWS のサービスごとの仕様や操作に慣れが必要ですが、代わりにランタイム更新の手間が無く長期運用するには打ってつけです。

記事は EC2 での作成例ですが、RDS など他のサービスへも応用ができます。

使うサービス

大まかに以下の3つを使います。全て AWS コンソール画面から行います。

  • Step Functions / ステートマシン
    • EC2 検索と実際の操作を担当します。
  • Event Bridge Scheduler
    • 定期実行でステートマシンをキックします。
  • IAM ロール + ポリシー
    • それぞれのサービスで必要なロールを準備します。

IAM ロール

早速先に必要なロールを作っておきます。

準備編 - 回避策

2024年3月現在、最近の IAM ロール作成ウィザードは選択式で制約が大きく、必要なロール画面からは一発で作らせてもらえません。回避策と言いますか、小技で準備します。

  1. ひとまずユースケースとして EC2 を指定して進めます。
  2. ポリシー未設定のまま、ロール名だけ入力したロールを作成します。
  3. 作成した IAM ロールを選択&編集モードにします。
  4. 適宜、信頼関係の JSON を修正したり、ポリシーを追加します。

※この手順で EC2 を選ぶことにあまり意味は無いです。シンプルなウィザードであれば何でも良いです。手順 4 で変更してます。

ロール: StepFunctions-EC2Management

EC2 を管理するための実行権限をステートマシンに与える IAM ロールです。

信頼関係

JSON に切り替えて、以下の内容で置き換えます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "states.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

許可 - 許可ポリシー

アタッチする AWS 管理ポリシー

X-Ray による実行分析に必要な記録を許可します。X-Ray を利用しないならアタッチ不要です。

「許可を追加」メニュー → 「ポリシーをアタッチ」を選択して、以下の AWS 管理ポリシーをアタッチします。

  • AWSXrayWriteOnlyAccess
インライン1: PushToCloudWatchLogs

Step Functions の実行状況を記録するためのポリシーです。ログの書き込みだけ許可します。

「許可を追加」メニュー → 「インラインポリシーを作成」を選択します。
JSON に切り替えて以下の内容で置き換えます。インラインポリシー名は何でも良いですが、ここでは PushToCloudWatchLogs とします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogDelivery",
                "logs:DeleteLogDelivery",
                "logs:DescribeLogGroups",
                "logs:DescribeResourcePolicies",
                "logs:GetLogDelivery",
                "logs:ListLogDeliveries",
                "logs:PutResourcePolicy",
                "logs:UpdateLogDelivery"
            ],
            "Resource": "*"
        }
    ]
}
インライン2: Management

Step Function で組んだ各タスクの実行権限です。EC2 の検索と起動・停止を許可します。
手順はインライン1と同じです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Management",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:StartInstances",
                "ec2:StopInstances"
            ],
            "Resource": "*"
        }
    ]
}

ロール: Scheduler-ExecutionStateMachine

Event Bridge スケジューラーに与える IAM ロールです。ステートマシンを開始するだけのシンプルな権限だけ付けます。

信頼関係

同等に JSON を置き換えていきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "scheduler.amazonaws.com"
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "StringEquals": {
                    "aws:SourceAccount": "123456789012"
                }
            }
        }
    ]
}

許可 - 許可ポリシー

インライン1: Execution

ステートマシンの開始権限を許可します。

ついでに、スケジュールの実行失敗を通知するためのデッドレターキュー(DLQ)として SQS へのメッセージ送信権限もつけておきます。今回は DLQ 設定を省略しているので、DLQ 部分は無くても動作します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "StartExecution",
            "Effect": "Allow",
            "Action": [
                "states:StartExecution"
            ],
            "Resource": "*"
        },
        {
            "Sid": "DLQ",
            "Effect": "Allow",
            "Action": [
                "sqs:SendMessage"
            ],
            "Resource": "*"
        }
    ]
}

Step Functions

続いて心臓部となる、対象 EC2 の特定と起動・停止をするステートマシンをそれぞれ用意していきます。

ステートマシン: ec2-start-instance

ステートマシンの開始パラメーターで開始対象の EC2 インスタンス名を指定して停止中なら起動指示をするフローを組みます。

想定する開始パラメーター

以下のような形式を受け取るステートマシンを作っていきます。定義内では $.EC2Names で文字列の配列として参照できます。

{
  "EC2Names": ["webserver1", "webserver2", "db1" ]
}

ステートマシン定義

東京リージョンでの URL
https://ap-northeast-1.console.aws.amazon.com/states/home?region=ap-northeast-1#/statemachines

  1. Step Functions ステートマシンを開きます。
  2. 「ステートマシンの作成」→「Blank」のまま「選択」をクリックします。
  3. 上部で「Code」をクリックし、JSON による編集モードに切り替えます。
  4. 以下の内容をコピーして貼り付けます。
{
  "Comment": "Start EC2 Instances",
  "StartAt": "DescribeInstances",
  "States": {
    "DescribeInstances": {
      "Type": "Task",
      "Parameters": {
        "Filters": [
          {
            "Name": "instance-state-name",
            "Values": [
              "stopped"
            ]
          },
          {
            "Name": "tag:Name",
            "Values.$": "$.EC2Names"
          }
        ]
      },
      "Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
      "Next": "Choice"
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.Reservations[0].Instances",
          "IsPresent": true,
          "Next": "Map"
        }
      ],
      "Default": "DoNothing"
    },
    "Map": {
      "Type": "Map",
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "INLINE"
        },
        "StartAt": "StartInstances",
        "States": {
          "StartInstances": {
            "Type": "Task",
            "Parameters": {
              "InstanceIds.$": "States.Array($.InstanceId)"
            },
            "Resource": "arn:aws:states:::aws-sdk:ec2:startInstances",
            "End": true,
            "TimeoutSeconds": 300
          }
        }
      },
      "ItemsPath": "$.Reservations[0].Instances",
      "End": true
    },
    "DoNothing": {
      "Type": "Pass",
      "End": true
    }
  }
}

設定

続いて上部の「設定」をクリックします。

  • ステートマシンに ec2-start-instance と付けます。
  • タイプは標準のままです。
  • アクセス許可の実行ロールをクリックして、予め作成しておいた StepFunctions-EC2management を選択します。
  • ログ記録は任意ですが、できる限り ERROR か ALL の設定をします。
    • 記録する場合は CloudWatch Logs でのログ保管期間の調整を忘れずに。
  • 追加の設定も任意ですが、X-Ray トレースの有効化を検討します。

ステートマシン: ec2-stop-instance

ステートマシンの開始パラメーターで開始対象の EC2 インスタンス名を指定して起動している EC2 に対して停止指示をするフローを組みます。

最初に通常の停止を試みます。停止失敗もしくは 5 分間応答が無ければ強制停止して確実に停止状態に移行させます。強制停止にエスカレートするのは異常動作なので特別な通知タスクを仕込むのが望ましいですが、ここでは省略します。

想定する開始パラメーター

起動用と同じです。

ステートマシン定義

{
  "Comment": "Stop EC2 Instances",
  "StartAt": "DescribeInstances",
  "States": {
    "DescribeInstances": {
      "Type": "Task",
      "Parameters": {
        "Filters": [
          {
            "Name": "instance-state-name",
            "Values": [
              "running"
            ]
          },
          {
            "Name": "tag:Name",
            "Values.$": "$.EC2Names"
          }
        ]
      },
      "Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
      "Next": "Choice"
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.Reservations[0].Instances",
          "IsPresent": true,
          "Next": "Map"
        }
      ],
      "Default": "DoNothing"
    },
    "Map": {
      "Type": "Map",
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "INLINE"
        },
        "StartAt": "StopInstances",
        "States": {
          "StopInstances": {
            "Type": "Task",
            "Parameters": {
              "InstanceIds.$": "States.Array($.InstanceId)",
              "Force": false
            },
            "Resource": "arn:aws:states:::aws-sdk:ec2:stopInstances",
            "Catch": [
              {
                "ErrorEquals": [
                  "States.ALL"
                ],
                "Next": "ForceStopInstances"
              }
            ],
            "TimeoutSeconds": 300,
            "End": true
          },
          "ForceStopInstances": {
            "Type": "Task",
            "Parameters": {
              "InstanceIds.$": "States.Array($.InstanceId)",
              "Force": true
            },
            "Resource": "arn:aws:states:::aws-sdk:ec2:stopInstances",
            "End": true,
            "TimeoutSeconds": 120
          }
        }
      },
      "ItemsPath": "$.Reservations[0].Instances",
      "End": true
    },
    "DoNothing": {
      "Type": "Pass",
      "End": true
    }
  }
}

設定

ステートマシンの名前を ec2-stop-instance にします。それ以外は起動のステートマシンと同じ設定をします。

Step Functions テスト

出来れば、この段階で作成した ec2-start-instanceec2-stop-instance が期待通りに動作するかテストしておきます。実際に EC2 が起動・停止するので、タイミングは計画的にします。

  1. ステートマシン ec2-start-instance を選択して開きます。
  2. 「実行を開始」をクリックします。
  3. 「入力 - オプション」の欄に以下を参考に入力します。
  4. 「実行を開始」をクリックします。
{
  "EC2Name": ["server1", "server2"]
}

状態が進んで成功すれば OK です。

トラブルシュート

失敗したら、実行履歴からステートの状態やエラーメッセージ、実際のタスク入力された JSON が確認できるのでそれを見つつ解消します。

良くあるのが、IAM ロールの設定ミスでアクションの実行を拒否されるケースです。エラーメッセージの中にどのアクションが失敗したか記録があるので見比べてチェックします。

Event Bridge スケジューラー

起動は平日・土日で分けつつ、停止は一律指定の合計3個のスケジュールを組んでみます。

作り方は公式にもあるのを参考にします。
https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-create-rule-schedule.html

スケジュール: AutoStartUp-Weekday

スケジュールの詳細の指定

ここでは平日(月曜~金曜)の朝 8:00 に起動する設定にします。

タイムゾーン - (UTC+09:00) Asia/Tokyo

  • スケジュールの種類 - cron ベースのスケジュール
  • cron 式 - 分 時間 日付 月 曜日 年
cron( 0 8 ? * MON-FRI * )

曜日の指定がハイフン繋がりで範囲指定な事に注意します。
トリガー式の例が表示されて、タイムゾーンを含めて想定の時刻と一致している事をチェックしておきます。

ターゲットの選択

起動用のステートマシンと入力パラメーターを設定します。

  • ターゲット API - AWS Step Functions / StartExecutionを選択
  • ステートマシン - ec2-start-instance を選択
  • 入力 - 以下を参考に EC2 サーバー名を設定します。
{
  "EC2Names": ["webserver1", "webserver2", "db1"]
}

設定

  • スケジュール完了後のアクション - NONE
  • 再試行ポリシーとデッドレターキュー - 任意
  • アクセス許可 - `Scheduler-ExecutionStateMachine

スケジュール: AutoStartUp-Weekend

スケジュールの詳細の指定

ここでは土日の 20:00 に起動する設定にします。

  • タイムゾーン - (UTC+09:00) Asia/Tokyo
  • スケジュールの種類 - cron ベースのスケジュール
  • cron 式 - 分 時間 日付 月 曜日 年
cron( 0 20 ? * SAT,SUN * )

曜日の指定がカンマ区切りで個別指定な事に注意します。
トリガー式の例が表示されて、タイムゾーンを含めて想定の時刻と一致している事をチェックしておきます。

ターゲットの選択

AutoStartUp-Weekday と同じです。

設定

AutoStartUp-Weekday と同じです。

スケジュール: AutoShutdown-Daily

スケジュールの詳細の指定

ここでは全日一律で深夜 2:00 に停止する設定にします。

  • タイムゾーン - (UTC+09:00) Asia/Tokyo
  • スケジュールの種類 - cron ベースのスケジュール
  • cron 式 - 分 時間 日付 月 曜日 年
cron( 0 2 * * ? * )

トリガー式の例が表示されて、タイムゾーンを含めて想定の時刻と一致している事をチェックしておきます。

ターゲットの選択

停止用のステートマシンと入力パラメーターを設定します。

  • ターゲット API - AWS Step Functions / StartExecutionを選択
  • ステートマシン - ec2-stop-instance を選択
  • 入力 - 以下を参考に EC2 サーバー名を設定します。
{
  "EC2Names": ["webserver1", "webserver2", "db1"]
}

設定

  • スケジュール完了後のアクション - NONE
  • 再試行ポリシーとデッドレターキュー - 任意
  • アクセス許可 - Scheduler-ExecutionStateMachine

応用

ある程度柔軟に EC2 サーバーを指定できる例としてサーバー名を利用しました。仕掛けとして、ステートマシン中の ec2:DescribeInstances に渡して対象を絞る条件に放り込んでいます。このフィルターを変えればよいので、独自タグでの一括指定などもできます。

詳細は API リファレンスで。
https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html

特定のタグ名で制御する

タグが付いていればタグの値は何でもよく、該当する EC2 に対して処理する場合です。
例えばタグに E2EServer: MasterTestDevTeam: FooBar DevTeam: Hoge などが付いた EC2 を対象にしたい運用の設定例(抜粋)です。

各ステートマシン - DescribeInstances ステートのフィルター部分
"Filters": [
  {
    "Name": "instance-state-name",
    "Values": [
      "running"
    ]
  },
  {
    // こんな風に変える
    "Name": "tag-key",
    "Values.$": "$.TagNames"
  }
]
各スケジュール - 入力設定

対象にしたいタグ名を列挙する。

{
  "TagNames": ["E2EServer", "DevTeam"]
}

汎用化のメリット

今回のように、ステートマシンの入力パラメーターを活用して汎用化すると複数のサービスからステートマシンを呼び出して共有・横展開する事が容易になります。もちろんステートマシンだけでも独立して実行できるので、手動で実施する EC2 起動・停止する保守作業にも使えます。

絞り込み検索 → 該当有無を判断する Choice 分岐 → 対象群に対して Map 並列処理」のフローは大変便利で広く応用できるので、手元にサンプルとして1つ作成しておくと動くリファレンスとして良いです。

補足

IAM ロールのアクションを限りなく絞っているとはいえ、対象リソースは限定して無いのでオーバー気味です。厳密にするならリソース名を明示する必要がありますが、汎用化と相反する所なのでバランスが大切です。

終わりに

長文の記事にお付き合いいただき、ありがとうございます。

長々とあたかも業務で使ってるような雰囲気を醸してますが、実はプライベートな Minecraft サーバー運用のガチ設定から抜粋しました。日中のお仕事中は遊べませんからね、節約ですw


何気に Event Bridge スケジューラーの繰り返し設定で、タイムゾーンが付いて直接日本時間で指定できるのが大きかったです。頭の中で 9 時間の時差計算しなくて済むのは精神的に楽でした。

今回は利用しませんでしたが、スケジュール完了後のアクションに自動でスケジュールを削除する DELETE 設定が増えたのもかなり有用ですね。例として、API 経由で 1 回だけ実行するスケジュールを組みたいアプリ機能の実装と抜群に相性が良いでしょう。こちらも活用例を記事にしたいと思います。

それではまた!

コラボスタイル Developers

Discussion