Amazon ECS のスケーリングのスケジュール設定の出力ツールを開発した。

本記事は [オルトプラス Advent Calendar 2023](https://qiita.com/advent-calendar/2023/altplus2023) の12/25の記事です。

## はじめに

こんにちは、オルトプラスのSREエンジニアの佐藤健太です。普段は弊社で使用しているAWSサービスの設定変更やメトリクス改善業務を中心に業務を行なっています。休日はAWSオンライン学習オンラインコミュニティに参加し、[AWS認定資格 ソリューションアーキテクトアソシエイトの教科書](https://www.amazon.co.jp/-/en/CloudTech%E6%9B%B8%E7%B1%8D%E4%BD%9C%E6%88%90%E5%A7%94%E5%93%A1%E4%BC%9A-ebook/dp/B0BCPNZ9GJ/ref=sr_1_1?qid=1702710376&refinements=p_27%3A%E3%81%8F%E3%82%8D%E3%81%8B%E3%82%8F%E3%81%93%E3%81%86%E3%81%B8%E3%81%84&s=digital-text&sr=1-1) や[AWS Amplifyのハンズオン本](https://www.amazon.co.jp/-/en/%E7%9F%B3%E4%BA%95-%E6%B9%A7-ebook/dp/B0CH7LLX2H/ref=sr_1_1?crid=1PEOJJXU3SXL5&keywords=Amplify+%E4%BD%90%E8%97%A4%E5%81%A5%E5%A4%AA&qid=1702710575&s=digital-text&sprefix=amplify+%E4%BD%90%E8%97%A4%E5%81%A5%E5%A4%AA%2Cdigital-text%2C175&sr=1-1)の共著活動をしています。

今回はAWSサービスの設定変更業務で簡単に確認できない部分を見える化した話です〜

## 概要
- Amazon ECSを運用している上でスパイク的な負荷が来るとわかっているときに、ECSタスクのオートスケーリングのスケジュール設定を行うことがあります。
- ECSオートスケーリングのスケジュール設定はAWSコンソール上ではできず、AWS コマンドラインインターフェイス（AWS CLI）でしか行えません。
- さらに登録したスケジュール設定を確認する時のAWS CLIのコマンドに対するレスポンスはJson形式のため確認しづらいという問題があります。
- そこでECSオートスケーリングのスケジュール設定をほしい情報だけ、他の設定と比べれるように表形式で出力できるツールを確認しました。

## 詳細
### オートスケーリングについて
最初にこの記事で頻出するオートスケーリングという言葉について簡単に説明します。
ITサービスを提供するサーバーに高負荷がかかった時にサーバー台数を増やして負荷を分散させます。この台数を増やすことをスケールアウトといいます。
一方でサーバー台数を減らすことをスケールインといいます。スケールインは負荷が下がってきて減らせることを確認してから実施します。
AWS ECSでは、タスク(=サーバー）の増減を自動的に判断・実施する機能としてオートスケーリングを提供しています。
タスク数の最小値(MinCapacity)と最大値(MaxCapacity)を設定することができます。

### 負荷とオートスケーリングについて
サービスにかかる負荷の上昇の仕方は二種類あります。
時間経過に伴い徐々に上昇していく通常の負荷については、CPU使用率やメモリ使用率の監視メトリクスを[ターゲット追跡スケーリングポリシー](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-autoscaling-targettracking.html)などで徐々にオートスケーリングさせる運用が有効になります。
イメージでいいますと負荷が高くなった時から数分〜十数分かけてECSのタスク数を増やしていく感じです。

一方でゲームサービスにおいて、ガチャなどの更新やイベントが始まる時間帯はユーザーが一気に集まり急激に負荷が上昇する（スパイク）事象については、上記のスケーリングポリシーでは対応が間に合いません。最初の急上昇する負荷に対応してほしいのに、数分後からECSのタスク数が増えても遅いのです。
一方で、ガチャやイベントの施策はこちらから提供するものなのでスパイク負荷が来るタイミング自体は予想できるものになります。

![chart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591721/3c89e306-2809-729a-25e1-b1daf3e34389.png)


### オートスケーリングのスケジューリングについて
あらかじめ来ることが分かっているスパイク負荷については、その負荷が始まる少し前の時間帯からECSのタスク数を多めに用意できるようにオートスケーリングのスケジュール設定を行っております。
例えば、通常10台で稼働しているところを100台まで増やしてスパイク負荷に備えるといったイメージです。

### オートスケーリングのスケジューリング設定の確認について
実際にスケジューリングされた設定を確認してみましょう
通常のAWSサービスの設定や設定内容の確認はAWSコンソールのWebブラウザ上で行えるのですが、今回のECSタスクのスケジュール設定や設定内容の確認は以下のような[AWS CLI](https://aws.amazon.com/jp/cli/)コマンドを実行した時のレスポンスでしかできないものとなっています。（ここ重要です）

以下が実行した時のコマンドプロンプトの様子です。

```
$ aws application-autoscaling describe-scheduled-actions \
  --service-namespace ecs \
  --profile <aws_credential>
# レスポンス
{
  "ScheduledActions": [
    {
      "ScheduledActionName": "api-20231201-2100",
      "ScheduledActionARN": "<ScheduledActionARN>",
      "ServiceNamespace": "ecs",
      "Schedule": "cron(0 12 01 12 ? *)",
      "ResourceId": "<ResourceId>",
      "ScalableDimension": "ecs:service:DesiredCount",
      "ScalableTargetAction": {
        "MinCapacity": 10,
        "MaxCapacity": 100
      },
      "CreationTime": "2023-12-01T20:00:00.479000+12:00"
    },
 },
}
```
通常AWSコンソールでこういった情報を確認するときは、AWS側が情報を整理してわかりやすく表示してくれています。
ただ今回のようにブラウザで確認することができない情報については、私たちはこのレスポンスをみて設定しているかどうか確認しなければなりません。

登録スケジュールが一個だとまだいいですが、数が増えると確認が煩雑になります。
また実務上で設定の登録漏れがないかを確認したいときはServiceNamespaceなど同じ情報や、ScheduledActionARNなど長い情報は必要ありません。
運用上困ったので情報をスリム化して出力させるツールが欲しくなりました。

### 情報のスリム化と一覧化ができるツールの開発

そこで運用上の知りたい情報だけ抜き出して一覧化するスクリプトツールを開発しました。
今回はvenvの設定とpip3インストールコマンドを共有するだけで他の人にも使えるのでpythonの言語を選択しました。
スクリプトの中では[AWS SDK for Pythonのboto3](https://aws.amazon.com/jp/sdk-for-python/)というモジュールを使って、コールしたAPIのレスポンスの情報の必要なものだけ抽出し出力させてます。

```
import argparse
import boto3
from botocore.exceptions import ProfileNotFound
from prettytable import PrettyTable

# 引数処理
parser = argparse.ArgumentParser(description='スクリプトの説明')
parser.add_argument('account', type=str, help='awsアカウント account1/account2')

args = parser.parse_args()
account = args.account

# ~/.aws/credentialsに登録している情報を指定する
aws_profiles = {
    "account1": "account1",
    "account2": "account2"
}

# accountに基づいて対応するAWSプロファイルとECSリソースIDを取得する
aws_profile = aws_profiles.get(account)

def show_schedule_list():
    # セッションを作成し、指定したプロファイルを使用する
    try:
        session = boto3.Session(profile_name=aws_profile)
        autoscaling_client = session.client('application-autoscaling')
    except ProfileNotFound:
        print(f"Error: AWS CLI profile '{aws_profile}' not found.")
        exit(1)

    # スケジュールされたアクションの情報を取得
    try:
        paginator = autoscaling_client.get_paginator('describe_scheduled_actions')
        response_iterator = paginator.paginate(ServiceNamespace='ecs')

    except Exception as e:
        print(f"Error: {e}")
        exit(1)

    # 必要な情報を抽出
    ## 全てのページにわたって結果を処理
    data = []
    for response in response_iterator:
        scheduled_actions = response.get("ScheduledActions", [])
        for action in scheduled_actions:
            scheduled_action_name = action.get("ScheduledActionName", "")
            schedule = action.get("Schedule", "")
            # ScalableTargetsセクションからMinCapacityとMaxCapacityを取得
            min_capacity = action.get("ScalableTargetAction", {}).get("MinCapacity", "")
            max_capacity = action.get("ScalableTargetAction", {}).get("MaxCapacity", "")
            new_data = {
                "ScheduledActionName": scheduled_action_name, 
                "Schedule": schedule, 
                "MinCapacity": min_capacity, 
                "MaxCapacity": max_capacity
            }
            # PrettyTableに直接入れることもできるが、並び替えしてみやすくしたいのでデータ配列を作る
            data.append(new_data)

    # ScheduledActionNameでデータをソート
    sorted_data = sorted(data, key=lambda x: x["ScheduledActionName"])

    # テーブルのヘッダーを指定
    table = PrettyTable(["ScheduledActionName", "Schedule", "MinCapacity", "MaxCapacity"])

    # ソートずみデータをテーブルに追加
    for row in sorted_data:
        table.add_row([row["ScheduledActionName"], row["Schedule"], row["MinCapacity"], row["MaxCapacity"]])

    # 全ての列を左寄せにする
    table.align = "l"

    print(table)

def main():
    show_schedule_list()

if __name__ == "__main__":
    main()
```
ScheduledActionNameの命名規則を何のECSサービスか、いつの時刻かわかるように設定しています。つまりScheduledActionNameにResourceIdの情報を持たせて表示するカラムを少なくさせようとしています。

また、PrettyTableにデータを入れる前に一旦ディクショナリー配列に入れてScheduledActionNameで並び替えをすることで、同じ種類のスケジュール設定ごとにまとまって確認できるようにしています。
これはAWSのAPIは設定しているスケジュールを順序がバラバラの状態で返してくるためです。

### スクリプトを実行してみる

1つの設定が一行で表されるようになり、登録されたスケジュール設定が格段に見やすくなりました！
毎回の設定業務で最後に確認作業をするときに目が滑らなくなり、ミス削減につながりました。
※ ScheduleカラムはUTCのタイムゾーンのcron式で表示されているので、ScheduledActionNameの時間と9時間ずれています。

```
$ python3 show_ecs_schdule.py stg                                      
+----------------------+-----------------------+-------------+-------------+
| ScheduledActionName  | Schedule              | MinCapacity | MaxCapacity |
+----------------------+-----------------------+-------------+-------------+
| api-20231201-2100    | cron(0 12 01 12 ? *)  | 10          | 100         |
| api-20231202-2000    | cron(0 11 02 12 ? *)  | 100         | 300         |
| api-20231203-2100    | cron(0 12 03 12 ? *)  | 150         | 300         |
| api-20231204-2200    | cron(0 13 04 12 ? *)  | 180         | 300         |
(省略)
```

## 最後に
- 今回はあまりAWSで整備されておらず、かつ記事化されていないAmazon ECS のスケーリングのスケジュール設定を見やすく出力させるツールを開発しました。
- PythonだとAWS Lambdaに載せやすくPJ全体に見える化するツールも開発できそうなのでそちらを着手してみようかと思ってます。
- 登録ツールや、削除ツールも作っているので次回はそちらについて紹介したいと考えております。