AWS Step Functionsを使ったステージング自動起動と自動停止!

こんにちは。エージェンシー事業でリードプラットフォームディレクターをしています、スガタニと申します。
おや?エンジニアじゃない?と思われたかと思いますが、少し前にBX推進を行う非エンジニア部署に異動しました。
それまではエージェンシー事業のエンジニアとしてユニットマネージャーをしておりました!
希望のキャリアプランに合わせて、ジョブローテーションが選択できるのは弊社の良いところですね。

そんな私が異動前最後に行った仕事、

「「 Step Functionsを使って、AWS上のステージング環境を夜間休日停止させる! 」」

こちらの内容について紹介させていただこうと思います。
AWSのコスト削減を頑張りたい方や、Step Functionsで何か自動化したい方の参考になれば嬉しいです。

はじめに

前提

AWSのコスト削減を目的として、社外向けWEBサービスのステージング環境を夜間休日停止させたい!
ステージング環境はエンジニアがリリース前の確認に使う他、一部の社員が提供機能の確認に使っているため、日中は常時起動させたいというニーズがあります。
元々GitLab CI/CDで実装はしてあったのですが、「認証周りを気にしなくていいし、便利なリソースがたくさんあるんだから、AWSリソースへの処理はAWS内で実装したい!」という思想が個人的に強めなので、GitLab CI/CDからの引っ越しを決めました。

技術選定

引っ越しにあたって、以下の5つの選択肢を比較検討しました。
Amazon EventBridgeとLambdaはクリティカルなデメリットがあるため除外しました。残りの選択肢のなかで、ノーコード実装と可視化が可能という点が魅力的かつ、今回の実装内容にマッチしていると感じたため、Step Functionsを選択することにしました!

Step Functionsって?

一言で言うと、「AWSの各サービスを組み合わせて複雑なワークフローを構築・可視化できるサービス」です!
200以上のサービスを利用できる上、Lambda関数を呼び出して複雑な独自処理を実行させることもできるため、できることの幅がとても広いです。 各タスクは独立しており、実行履歴をログで確認できます。

Step Functionsには、状態遷移というものがあり、状態遷移の回数によって料金が変動します。
状態遷移は1つのリクエスト実行や、分岐・並列等のフロー処理が完了するごとにカウントされていきます。
1ヶ月4000回までの状態遷移が無料、以後の状態遷移ごとに0.000025USD課金と、比較的お安めの価格設定になっています。(※大量のループ処理等を走らせると高額になってしまうので注意!)

全体像

Step Functionsによるフロー実装に関わるリソースの全体像です。
インスタンスへ入らずともEC2へコマンド実行できる、AWS Systems ManagerのSendCommand APIや、今年リリースされたUser Notificationsを使用してみました!

朝の起動と夜の停止でフローが異なるので、それぞれ分けて説明していきます!

朝の自動起動フロー作成

やりたいこと

  1. RDSの起動
  2. EC2の起動
  3. WEBインスタンスのUnicorn起動
  4. AutoScalingGroupの更新
  5. Mackerelのミュート解除

Step Functions設定手順

さっそくワークフローを作成していきましょう!

1日1回、長期的に実行したいワークロードなので、タイプは標準を選びます。

1. RDSの起動

アクションを検索し、ドラッグアンドドロップで設置することができます。
マルチAZ DBクラスター構成のため、StartDBClusterを使用します。
APIパラーメーターには起動するDB識別子を指定しましょう。

▼ StartDBCluster: APIパラメーター

{
  "DbClusterIdentifier": "DBName"
}


さらに、RDSの起動が完了するまで次に進まないよう設定を追加します。
(RDSの起動より前にEC2が起動すると、DBが読めずにエラーが発生するため)
分岐と待機のフローアクションを使って、Describeでステータスを定期的に確認しに行き、「available」になったら次に進むように実装します。
DescribeDBClusterで取れたステータス情報を分岐で使用するため、結果の出力設定を入れておきましょう。
RDSは起動に時間がかかるため、Waitの待機時間は180秒に設定してみました。

▼ DescribeDBCluster: APIパラメーター

{
  "DbClusterIdentifier": "DBName"
}

▼ DescribeDBCluster: 出力(ResultSelectorを使用して結果を変換)

{
  "DbClusterStatus.$": "$.DbClusters[0].Status"
}

2. EC2起動

RDSと同様にEC2も完全起動させてから次へ進むようにフローを作成します。
さっきとは違い対象が複数インスタンスになるため、DescribeInstancesで取得したRunning状態のインスタンス数で分岐を作成します。

また、オートスケーリングによりインスタンスIDは変動するため、DescribeInstancesで停止状態のインスタンスIDを取得して、StartInstancesに渡します。
同アクションがあるので、状態名を分かりやすく書き換えておきましょう。

▼ DescribeRunningInstances: APIパラメーター

{
  "Filters": [
    {
      "Name": "tag:Name",
      "Values": [
        "staging-web-*"
        ]
    },
    {
      "Name": "instance-state-name",
      "Values": [
        "running"
      ]
    }
  ]
}

▼ DescribeRunningInstances: 出力(ResultSelectorを使用して結果を変換)

{
  "StagingRunningInstanceCount.$": "States.ArrayLength($.Reservations)"
}

▼ DescribeStopInstances: APIパラメーター

{
  "Filters": [
    {
      "Name": "tag:Name",
      "Values": [
        "staging-web-*"
        ]
    },
    {
      "Name": "instance-state-name",
      "Values": [
        "stopped"
      ]
    }
  ]
}

▼ DescribeStopInstances: 出力(ResultSelectorを使用して結果を変換)

{
  "StagingStopInstanceIds.$": "$.Reservations[*].Instances[0].InstanceId"
}

▼ StartInstances: APIパラメーター

{
  "InstanceIds.$": "$.StagingStopInstanceIds"
}

3. WEBインスタンスのUnicorn起動

WEBインスタンスにUnicorn自動起動設定が入っていなかったので、SSMのSendCommandで起動してみます。(※本来はユーザーデータやAMIに設定を入れておくのが良いと思います)

SendCommandで指定するインスタンスIDはDescribeInstancesで取得しておきましょう。

▼ DescribeWebInstances: APIパラメーター

{
  "Filters": [
    {
      "Name": "tag:Name",
      "Values": [
        "staging-web-*"
        ]
    }
  ]
}

▼ DescribeWebInstances: 出力(ResultSelectorを使用して結果を変換)

{
  "StagingWebInstanceIds.$": "$.Reservations[*].Instances[0].InstanceId"
}

▼ SendCommand: APIパラメーター

{
  "InstanceIds.$": "$.StagingWebInstanceIds",
  "DocumentName": "AWS-RunShellScript",
  "Parameters": {
    "commands": [
      "ここに必要なコマンドを入力"
    ]
  }
}

4. AutoScalingGroupの更新

AutoScalingGroupに紐づけられているインスタンスのライフサイクルを、Standby→InServiceに変更します。 DescribeAutoScalingGroupsでインスタンスIDとライフサイクルを取得して、Standbyだった場合にExitStandbyで更新をかけていきます。

▼ DescribeAutoScalingGroups: APIパラメーター

{
  "AutoScalingGroupNames": [
    "StagingAutoScalingGroupName"
  ]
}

▼ DescribeAutoScalingGroups: 出力(ResultSelectorを使用して結果を変換)

{
  "StagingInstanceIds.$": "$.AutoScalingGroups[0].Instances[0].InstanceId",
  "StagingInstanceStatus.$": "$.AutoScalingGroups[0].Instances[0].LifecycleState"
}

▼ ExitStandby: APIパラメーター

{
  "AutoScalingGroupName": "StagingAutoScalingGroupName",
  "InstanceIds.$": "$.StagingInstanceIds"
}


次にAutoScalingGroupのキャパシティを変更します。(最大キャパシティは4の状態)

  • 希望するキャパシティ:0→2
  • 最小キャパシティ  :0→2

▼ UpdateAutoScalingGroup: APIパラメーター

{
  "AutoScalingGroupName": "StagingAutoScalingGroupName",
  "MinSize": 2,
  "DesiredCapacity": 2
}

5. Mackerelのミュート解除

Mackerelで外形監視をしており、EC2停止中はエラー通知が飛ばないようミュートしているため解除します。
MackerelのAPIをAPI Gatewayに登録し、Invokeアクションによって実行することで実現させます。
また、API KeyはSSMパラメーターストアに保存しておいたので、GetParameterで取得してきます。

Mackerel API ドキュメント(v0)

▼ GetParameter: APIパラメーター

{
  "Name": "mackerel_api_key",
  "WithDecryption": true
}

▼ GetParameter: 出力(ResultSelectorを使用して結果を変換)

{
  "MackerelApiKey.$": "States.Array($.Parameter.Value)"
}

▼ API Gataway:Invoke: APIパラメーター

{
  "ApiEndpoint": "xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
  "Method": "PUT",
  "Headers": {
    "Content-Type": [
      "application/json"
    ],
    "X-Api-Key.$": "$.MackerelApiKey"
  },
  "Stage": "main",
  "Path": "/",
  "RequestBody": {
    "headers": [
      {
        "name": "Cache-Control",
        "value": "no-cache"
      }
    ],
    "method": "GET",
    "isMute": false,
    "certificationExpirationCritical": 15,
    "memo": "ステージングの外形監視",
    "type": "external",
    "url": "https://xxxxxxxxxxxxxxxxx",
    "maxCheckAttempts": 5,
    "service": "XXX",
    "name": "Staging External HTTPS Check",
    "responseTimeDuration": 10,
    "id": "xxxxxxxxxxx",
    "certificationExpirationWarning": 30,
    "responseTimeCritical": 5000,
    "responseTimeWarning": 2000
  },
  "AuthType": "IAM_ROLE"
}

これで自動起動のワークフロー設計は完了しました!
必要な権限を付与したIAMロールを紐づけて、名前をつけて保存します。
実際に実行して動作を確認してみましょう。

夜の自動停止フロー作成

やりたいこと

  1. Mackerelのミュート
  2. AutoScalingGroupの更新
  3. EC2の停止
  4. RDSの停止

Step Functions設定手順

自動起動と同じ要領での作成のため、詳細は割愛します!
起動とは違い、完全停止を待つ必要はなく、ステータスが変わりさえすれば次の処理を走らせることができます。そのため、Describeループは作成せずにWaitで10秒ほど待たせています。

スケジュールとエラー通知設定

最後に定期実行させるためのスケジュール設定と、失敗時の通知設定を行っておきます。

スケジュール設定

EventBridgeのスケジュールを作成します。月曜~金曜の朝8時に起動、夜23時に停止させます。
ターゲットはStep FunctionsのStartExecution、cron式は以下のように設定しました。

起動cron式

停止cron式

エラー通知設定

フローが失敗した場合にSlackへ通知させます。
今年新しく追加された「AWS User Notifications」を使用します。
あらゆる通知形はこれで簡単実装できるのでは?!というくらい便利なサービスです!

通知設定の「通知設定を作成」から新規作成を行なっていきましょう。

Step Functionsのステータスが以下いずれかの失敗値になった場合をイベントに設定します。

  • FAILED
  • TIMED_OUT
  • ABORT

別のStep Functionsワークフローがあり、通知を飛ばしたくない場合は、名前でのフィルタリングも追加できます。

今回は各ワークフロー(起動・停止)が1日1回実行で、頻繁に実行されるものではないため、集約しない設定にしました。
また、通知先はSlackを設定したChatbotを選択します。

Step Functionsを使ってみた所感

良かったところ

対応しているAWSリソースが多く、基本的になんでも実装できるため幅広い用途で使えそうだなと思いました。また、サーバーレスかつGUIで構築できるため、学習・実装コストが低い上に、可視化まで同時に行えてしまうところがとても良かったです。
今回の実装内容だと無料枠内でおさまったのも嬉しいところですね!

気になったところ

フローの途中から実行ができないため、リトライ時に重複実行しても問題ないよう考慮が必要であったり、処理の完了を待つためにはDescribeをループ処理させる必要があったりと、フローの複雑度と状態遷移回数が上がってしまう部分が気になりました。
AWS CLIのqueryオプションのようなものが使えないため、出力の高度なフィルタリングが難しく、Describeで都度必要なデータを取得する必要がある点も悩みポイントでした。

まとめ

Step Functionsを使ったステージング環境の起動停止を実装してみましたが、いかがだったでしょうか。みなさんもぜひ色々な自動化で試してみてください。
実装前にコスト面の確認はお忘れ無く〜!