はじめに

AWS 利用料削減のため、EC2 インスタンスを指定の時間で停止・起動させる必要がある場合、どのような方式で実装するのがベストでしょうか?

今回、私が担当するお客様の案件にてこの要件があったため、ベストな方式を検討しました。結論として、SSM メンテナンスウィンドウ(AWS Systems Manager Maintenance Windows)を採用し、リソースグループとタグで対象インスタンスを管理、CloudFormation でコード管理する構成にしました。

この記事では、方式の比較から実際の構築手順、やってみてわかった注意点までをまとめます。

EC2 自動起動・停止の方式比較

EC2 の自動起動・停止を実現する方法は主に以下の 4 つがあります。

方式 追加コスト 対象の管理方法 IaC 管理 祝日対応 導入の手軽さ
SSM メンテナンスウィンドウ 無し タグ(リソースグループ経由) ×
EventBridge + Lambda Lambda 実行分 タグ(Lambda 内で直接指定可) ○(要コード実装)
Instance Scheduler(AWS ソリューション) DynamoDB + Lambda タグ
Auto Scaling Scheduled Actions 無し ASG の台数制御 × ×(ASG 前提)

EventBridge + Lambda は柔軟性が高いですが、Lambda 関数のコードを書いてメンテナンスする必要があります。Instance Scheduler は AWS 公式のソリューションで祝日対応もできますが、裏で DynamoDB や Lambda が動いていて、トラブルシュート時に追いにくいです。Auto Scaling Scheduled Actions は ASG(Auto Scaling グループ)の利用が前提なので既存の単体 EC2 には使えません。また、台数を 0 にすることで停止を実現する仕組みのため、インスタンスは停止ではなく終了(Terminate)されます。

SSM メンテナンスウィンドウは、Lambda 不要で追加コストもかからず、CloudFormation でそのまま管理できます。祝日対応ができないという弱点はありますが、「平日の業務時間帯だけ起動しておきたい」というシンプルな要件であれば十分です。

今回の案件では、以下の要件がありました。

  • Lambda 等のコードの運用保守コストをかけたくない
  • お客様から起動・停止時刻の変更依頼が度々あるため、IaC で管理してすぐに対応できるようにしたい
  • インスタンスのリストア等で対象インスタンスが変わることがあるため、インスタンス ID ではなくタグで対象を管理したい

これらを踏まえて、SSM メンテナンスウィンドウ + リソースグループ(タグベース)+ CloudFormation の構成を採用しました。

SSM メンテナンスウィンドウの仕組み

SSM メンテナンスウィンドウは、以下の 3 つの要素で構成されています。

  • メンテナンスウィンドウ:スケジュール(cron 式)と実行枠(Duration)を定義する
  • ターゲット:処理対象のインスタンスを定義する
  • タスク:実行する処理(起動や停止)を定義する

EC2 の起動・停止には、AWS が用意している SSM ドキュメント AWS-StartEC2InstanceAWS-StopEC2Instance をタスクとして使います。

Duration について

メンテナンスウィンドウには Duration(実行枠)という設定があります。

スケジュールで指定した時刻は「この時刻ちょうどに実行される」という意味ではなく、「この時刻から Duration 時間の枠内で実行される」という仕様です。例えば、スケジュールが 8:30 で Duration が 2 時間の場合、タスクは 8:30〜10:30 の枠内で実行されます。

ただ、実際に試してみたところ、スケジュール時刻の直後に実行されていたので、基本的には指定した時刻に実行されると思って大丈夫です。「仕様上はそうなっている」という認識だけ持っておけば問題ないと思います。

祝日対応について

SSM メンテナンスウィンドウのスケジュールは cron 式で定義するため、「月〜金」のような曜日指定はできますが、祝日を除外することはできません。祝日にもインスタンスが起動してしまうのが許容できない場合は、EventBridge + Lambda など別の方式を検討する必要があります。

タグで対象インスタンスを管理したい → リソースグループを経由する

SSM メンテナンスウィンドウでインスタンスを起動・停止する場合、ターゲットの指定方法は以下の 2 つです。

  • ResourceType: INSTANCE でインスタンス ID を直接指定する
  • ResourceType: RESOURCE_GROUP でリソースグループを指定する

「タグが付いたインスタンスを自動的に対象にしたい」と思って調べたのですが、AUTOMATION タスク(AWS-StartEC2Instance / AWS-StopEC2Instance)では、ターゲットにタグを直接指定することができませんでした。Run Command タスクであれば Key=tag:タグキー の形式でタグ指定ができるのですが、AUTOMATION タスクではこの指定方法が使えないようです。

参考:RegisterTargetWithMaintenanceWindow – AWS Systems Manager API Reference

リソースグループで回避する

そこで使うのが AWS Resource Groups です。

Resource Groups では、タグベースのリソースグループを作成できます。例えば「AUTO_STARTSTOP: enabled というタグが付いた EC2 インスタンス」をグループとして定義しておけば、メンテナンスウィンドウのターゲットにそのリソースグループを指定するだけで、タグが付いたインスタンスが自動的に処理対象になります。

この方式の良いところは、インスタンスの追加・削除がタグの付け外しだけで済むことです。新しいインスタンスを起動・停止の対象に加えたい場合は、そのインスタンスにタグを付けるだけで済みます。CloudFormation テンプレートやメンテナンスウィンドウの設定を変更する必要はありません。逆に、一時的に対象から外したい場合はタグを削除するだけで対応できます。

CloudFormation での実装手順

ここからは、実際に CloudFormation で構築する手順を説明します。

前提条件

  • SSM メンテナンスウィンドウ用の IAM ロールが作成済みであること
  • IAM ロールに以下の 2 つのマネージドポリシーがアタッチされていること
    • AmazonSSMAutomationRole
    • AmazonSSMMaintenanceWindowRole
  • IAM ロールの信頼ポリシーで ssm.amazonaws.comsts:AssumeRole できること

SSM メンテナンスウィンドウはタスク実行時にこの IAM ロールを引き受けて(AssumeRole して)動作します。そのため、信頼ポリシーで SSM サービスからの AssumeRole を許可しておく必要があります。

AmazonSSMMaintenanceWindowRole には resource-groups:ListGroupsresource-groups:ListGroupResourcestag:GetResources の権限が含まれているので、リソースグループをターゲットにする場合でも追加のポリシーは不要です。

手順 1:対象インスタンスにタグを付与

起動・停止の対象にしたい EC2 インスタンスにタグを付けます。

EC2 コンソールでインスタンスを選択し、「タグ」タブ → 「タグを管理」から以下のタグを追加します。

Key Value
MY_SERVER_AUTOMATION_STARTSTOP enabled

タグの Key と Value は任意ですが、何の用途のタグかわかりやすい名前にしておくと良いです。

手順 2:CloudFormation テンプレートの作成

以下のテンプレートで、リソースグループ、メンテナンスウィンドウ(起動用・停止用)、ターゲット、タスクをまとめて作成します。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'SSM Maintenance Windows for EC2 Start/Stop with Resource Group'

Parameters:
  ResourceGroupName:
    Type: String
    Default: MyServerResourceGroup
    Description: Resource Group name

  TagKey:
    Type: String
    Default: MY_SERVER_AUTOMATION_STARTSTOP
    Description: Tag key to identify target EC2 instances

  TagValue:
    Type: String
    Default: enabled
    Description: Tag value to identify target EC2 instances

  ServiceRoleArn:
    Type: String
    Description: ARN of the IAM Role for SSM Maintenance Window

Resources:
  # --- リソースグループ ---
  # 指定タグが付いた EC2 インスタンスを自動的にグループ化
  ServerResourceGroup:
    Type: AWS::ResourceGroups::Group
    Properties:
      Name: !Ref ResourceGroupName
      Description: Resource group for target EC2 instances
      ResourceQuery:
        Type: TAG_FILTERS_1_0
        Query:
          ResourceTypeFilters:
            - AWS::EC2::Instance
          TagFilters:
            - Key: !Ref TagKey
              Values:
                - !Ref TagValue

  # --- 起動用メンテナンスウィンドウ(平日 08:30 JST)---
  EC2StartMaintenanceWindow:
    Type: AWS::SSM::MaintenanceWindow
    Properties:
      Name: MW-MyServer-Start
      Description: Start EC2 instances
      Schedule: "cron(30 8 ? * MON-FRI *)"
      ScheduleTimezone: "Asia/Tokyo"
      Duration: 2
      Cutoff: 0
      AllowUnassociatedTargets: true

  EC2StartMaintenanceTarget:
    Type: AWS::SSM::MaintenanceWindowTarget
    Properties:
      WindowId: !Ref EC2StartMaintenanceWindow
      Name: Target-MyServer
      ResourceType: RESOURCE_GROUP
      Targets:
        - Key: resource-groups:Name
          Values:
            - !Ref ResourceGroupName

  EC2StartWindowTask:
    Type: AWS::SSM::MaintenanceWindowTask
    Properties:
      Name: Task-MyServer-Start
      WindowId: !Ref EC2StartMaintenanceWindow
      Targets:
        - Key: WindowTargetIds
          Values:
            - !Ref EC2StartMaintenanceTarget
      TaskArn: AWS-StartEC2Instance
      TaskType: AUTOMATION
      TaskInvocationParameters:
        MaintenanceWindowAutomationParameters:
          DocumentVersion: $DEFAULT
          Parameters:
            InstanceId:
              - '{{RESOURCE_ID}}'
      Priority: 1
      MaxConcurrency: "100%"
      MaxErrors: "100%"
      ServiceRoleArn: !Ref ServiceRoleArn

  # --- 停止用メンテナンスウィンドウ(平日 20:00 JST)---
  EC2StopMaintenanceWindow:
    Type: AWS::SSM::MaintenanceWindow
    Properties:
      Name: MW-MyServer-Stop
      Description: Stop EC2 instances
      Schedule: "cron(0 20 ? * MON-FRI *)"
      ScheduleTimezone: "Asia/Tokyo"
      Duration: 2
      Cutoff: 0
      AllowUnassociatedTargets: true

  EC2StopMaintenanceTarget:
    Type: AWS::SSM::MaintenanceWindowTarget
    Properties:
      WindowId: !Ref EC2StopMaintenanceWindow
      Name: Target-MyServer
      ResourceType: RESOURCE_GROUP
      Targets:
        - Key: resource-groups:Name
          Values:
            - !Ref ResourceGroupName

  EC2StopWindowTask:
    Type: AWS::SSM::MaintenanceWindowTask
    Properties:
      Name: Task-MyServer-Stop
      WindowId: !Ref EC2StopMaintenanceWindow
      Targets:
        - Key: WindowTargetIds
          Values:
            - !Ref EC2StopMaintenanceTarget
      TaskArn: AWS-StopEC2Instance
      TaskType: AUTOMATION
      TaskInvocationParameters:
        MaintenanceWindowAutomationParameters:
          DocumentVersion: $DEFAULT
          Parameters:
            InstanceId:
              - '{{RESOURCE_ID}}'
      Priority: 1
      MaxConcurrency: "100%"
      MaxErrors: "100%"
      ServiceRoleArn: !Ref ServiceRoleArn

Outputs:
  ResourceGroupName:
    Value: !Ref ServerResourceGroup
    Description: Resource Group Name
  StartMaintenanceWindowId:
    Value: !Ref EC2StartMaintenanceWindow
    Description: Maintenance Window ID for Start
  StopMaintenanceWindowId:
    Value: !Ref EC2StopMaintenanceWindow
    Description: Maintenance Window ID for Stop

テンプレートのポイントをいくつか補足します。

  • ResourceQuery.Type: TAG_FILTERS_1_0 でタグベースのリソースグループを作成しています。ResourceTypeFiltersAWS::EC2::Instance を指定することで、EC2 インスタンスだけがグループに含まれるようにしています
  • ターゲットの ResourceTypeRESOURCE_GROUP を指定し、Key: resource-groups:Name でリソースグループ名を参照しています
  • タスクの InstanceId パラメータに {{RESOURCE_ID}} を指定しています。これはリソースグループ内の各インスタンス ID に自動展開されるプレースホルダーです
  • 起動用と停止用でメンテナンスウィンドウを分けています。Duration 枠が重ならないように、起動は 8:30(枠:8:30〜10:30)、停止は 20:00(枠:20:00〜22:00)としています

手順 3:スタックのデプロイ

CloudFormation コンソールからスタックを作成します。

まず、CloudFormation コンソールを開き「スタックの作成」→「新しいリソースを使用」を選択します。

「テンプレートファイルのアップロード」で上記の yml ファイルをアップロードし、「次へ」をクリックします。

スタック名を入力します(例:ssm-maintenance-window-myserver-stack)。

パラメータの入力画面が表示されるので、以下を確認・入力します。

  • ResourceGroupName:リソースグループ名(デフォルトのままでも OK)
  • ServiceRoleArn:SSM メンテナンスウィンドウ用 IAM ロールの ARN
  • TagKey:手順 1 で付けたタグの Key
  • TagValue:手順 1 で付けたタグの Value

「スタックオプションの設定」画面と「レビュー」画面はデフォルトのまま進めて、最後に「送信」をクリックします。

スタックのステータスが CREATE_COMPLETE になれば完了です。

手順 4:リソースグループの確認

Resource Groups コンソール(AWS Resource Groups & Tag Editor)を開き、作成したリソースグループを確認します。

「グループリソース」タブに、手順 1 でタグを付けた EC2 インスタンスが表示されていれば OK です。

手順 5:実行結果の確認

メンテナンスウィンドウのスケジュール時刻が来たら、Systems Manager コンソール → メンテナンスウィンドウ → 該当ウィンドウの「履歴」タブで実行結果を確認できます。ステータスが 成功 になっていれば、起動・停止が正常に実行されています。
今回は検証のため、インスタンスを事前に手動で停止した状態にしておき、起動用メンテナンスウィンドウが実行されてインスタンスが起動されるかを確認しました。また、スケジュール時刻はテンプレートに記載した 8:30 / 20:00 ではなく、検証用に変更して実行しています。

EC2 コンソールでもインスタンスの状態(実行中 / 停止済み)を確認してみてください。
今回は起動のジョブを実行したので実行中になっていることが確認できました。

おわりに

SSM メンテナンスウィンドウを使った EC2 の自動起動・停止を、リソースグループと CloudFormation で管理する方法を試してみました。

正直なところ、AUTOMATION タスクでタグを直接指定できないのは驚きましたが、リソースグループを経由すれば結果的にタグベースで管理できるので、運用上は問題ありませんでした。むしろ CloudFormation でリソースグループごとコード管理できるので、設定の見通しが良くなったと感じています。

祝日対応ができない点は割り切りが必要ですが、「Lambda を書かずにタグベースで起動・停止対象を管理したい」という要件であれば、この構成はなかなか使いやすいです。

同じような要件で悩んでいる方の参考になれば幸いです。