SQSのメッセージ数に応じてFargateタスクをスケーリングさせる

## はじめに
株式会社LITALICO SREグループの鈴木です。
この記事は [LITALICO Engineers Advent Calendar 2021 その2](https://qiita.com/advent-calendar/2021/litalico)の22日目の記事です。
LITALICOのB2B向けサービスではECS Fargate上でアプリケーションコンテナの管理を行っています。
とある新機能を開発するにあたって、SQS内のキュー数に応じてFargate上のタスクをスケーリングさせる要件があり、Autoscailingのサービスについて調べながら実装してみたので、自身の備忘録も兼ねて書こうと思います。

## 大まかな構成と要件
![スクリーンショット 2021-12-21 7.34.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2341357/7a237f9a-06c2-22cb-1ea4-7478d902b6a2.png)
今回の要件に関わるインフラ部分のリソース構成は上記の通りです。
(実際にはDB接続や他アプリとの連携部分などがありますが今回は省略しています)
赤く囲った箇所が本記事の設定を入れる箇所です。

現在開発中の新機能では日中や夜間のバッチ処理を行う要件があり、バッチ処理はLaravelのQueueで実装されていて、キューサービスにはAWS SQSを使っています。
バッチ処理の実行トリガーとなるジョブメッセージがSQSに投入されると、Fargate上のworkerタスクがジョブメッセージに応じてデータを処理します。

今回オートスケーリングさせる対象はこのworkerタスクです。workerタスクはキューにメッセージが入っていない時は起動させる必要はありません。また、夜間と日中のバッチ処理では処理するメッセージ数がかなり異なります。夜間に1000件以上のジョブを流し、数時間以内に処理をする必要があるのに対し、日中は１日数回APIからコールしてジョブを実行する程度です。そのため、夜間はタスク数を増加する必要がありますが、日中はAPIコールのタイミングで1タスク起動していれば十分です。

時間帯で指定してタスクを増減させる方法でも要件を実現できそうではあるのですが、キューのメッセージ数をベースにスケーリングさせることができればより効率的だと考えました。
今回は下記条件を満たせるようにタスクの数を増減させたいと思います。

- `キュー内のメッセージ数=0`の場合、タスクの数は0にする
- `1<=キュー内のメッセージ数<=500`の場合、タスクを1つ起動する
- `500<キュー内のメッセージ`の場合は、タスクを4つにスケールアウトさせる
-  スケールアウトしたタスクは、`キュー内のメッセージ=0`になったら一定の秒数ごとに1タスクずつスケールインする(最終的にタスク数は0になる)

実際に本番利用する際は、動作検証して想定している挙動になるように各パラメータを微調整していく必要があります。

## 設定方法


### オートスケーリングターゲット
まず、`register-scalable-target`コマンドでFargateタスクをAutoScailingのターゲットに登録する必要があります。コマンドの詳細は下記リンクをご参照ください。

https://docs.aws.amazon.com/cli/latest/reference/application-autoscaling/register-scalable-target.html

注意点として、`min-capacity`と`max-capacity`のオプションは必ず設定する必要があります。
そうしないと、タスクの必要数(`DesiredCount`)が変更されないため、後述するスケールイン/アウトのポリシーやアラームでのトリガーを設定しても、タスクの追加が実行されません。

```
aws application-autoscaling register-scalable-target \
    --service-namespace ecs \
    --scalable-dimension ecs:service:DesiredCount \ #スケーリング対象
    --resource-id service/sample-cluster/sample-service \ #対象となるECSサービス
    --min-capacity 0 \ #最小タスク数
    --max-capacity 4 #最大タスク数 
```

### オートスケーリングポリシー

ECS サービスのオートスケーリングでは以下3つのタイプがサポートされています。

- **スケジュールに基づくスケーリング**

日付や時刻に基づいてスケーリングを行いたい場合に使用します。

- **ターゲット追跡スケーリングポリシー**

指定したメトリクスが指定した数値になるようにスケーリングを行うポリシーです。今回の例で言えば、SQSキューのメッセージ数がターゲット値になり、その平均値や最大値をもとにECSのタスク数を増減させることができます。
例えばキュー内メッセージの平均数が10個となるように指定した場合、

メッセージ数が20個 -> スケールアウト
メッセージ数が0個 -> スケールイン
というような振る舞いをしてくれるように自動でスケールイン/アウトの設定を入れてくれます。

CloudWatchアラームの作成やしきい値設定まで自動でよしなにやってくれるのが便利です。

- **ステップスケーリングポリシー**

手動で1つ以上のしきい値を設定し、その値を元にスケーリングを行いたい場合に使用します。
適切な値を設定するためにはテストを繰り返してパラメータを調整する必要がありますが、挙動を細かくコントロールできるのが長所です。

AWSはターゲット追跡ポリシーを使うことを推奨しているようです。
そのせいか、ステップスケーリングポリシーはマネジメントコンソール上からの設定がサポートされておらず(2021/12/20現在)、AWS CLI上から設定する必要があります。
> Amazon ECS サービスの Auto Scaling は Application Auto Scaling ステップスケーリングポリシーの使用をサポートしていますが、代わりにターゲット追跡ポリシーを使用することをお勧めします。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-autoscaling-stepscaling.html

また、前述の通り、ターゲット追跡スケーリングポリシーは自動でCloudWatchアラームを作成してくれるのですが、このアラームの編集は非推奨とされています。スケーリングの挙動を細かくチューニングすることは想定されていないようです。

>ターゲット追跡スケーリングポリシーのために Service Auto Scaling が管理する CloudWatch アラームを編集または削除しないでください。スケーリングポリシーを削除するときに、Service Auto Scaling はアラームを自動的に削除します。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-autoscaling-targettracking.html

今回の例では、メッセージ数とタスクの関係が要件で決まっているため、 **ステップスケーリングポリシー** を利用します。
ポリシーをCLI上から追加するには`put-scaling-policy`コマンドを使います。

https://docs.aws.amazon.com/cli/latest/reference/application-autoscaling/put-scaling-policy.html

#### スケールアウトポリシー
要件の通り、SQSの個数に応じたタスクの増加数(ScalingAdjustment)を設定していきます。
**注意点として、下限値(MetricIntervalLowerBound)は「-より大きい」、上限値(MetricIntervalUpperBound)は「以下」で指定する必要があります。**
スケーリングが実行されるまでの待機時間を60秒とします。
基準となるメトリクス値の統計情報(MetricAggregationType)は`Average | Maximum | Minimum`から選ぶことができます。


```high-policy-config.json
{
  "AdjustmentType": "ExactCapacity",
  "StepAdjustments": [
    {
      "MetricIntervalLowerBound": 0,　#0より大きい時にスケーリングが開始する
      "MetricIntervalUpperBound": 500,　#500以下までは同じ状態
      "ScalingAdjustment": 1 #スケールアウトするタスク数
    },
{
      "MetricIntervalLowerBound": 500,#500より大きくなるとスケーリングが開始する
      "ScalingAdjustment": 3
}
  ],
  "Cooldown": 60,#スケールアウトが実行されてから次のスケールアウトが実行されるまでの時間
  "MetricAggregationType": "Average" #メッセージ数の平均値を用いる
}
```
上記のポリシーを`put-scaling-policy`コマンドで追加します。

```
aws application-autoscaling put-scaling-policy --service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id  service/sample-cluster/sample-service \
--policy-name sqs-queue-step-high-scaling-policy \
--policy-type StepScaling \
--step-scaling-policy-configuration file://high-policy-config.json
```



#### スケールインポリシー
同様に、スケールインポリシーも作成します。キュー数が0の時に、120秒ごとに1つずつタスクをシャットダウンするように設定します。

```low-policy-config.json
{
  "AdjustmentType": "ChangeInCapacity",
  "StepAdjustments": [
    {
      "MetricIntervalUpperBound": 0, 
      "ScalingAdjustment": -1
    }
  ],
  "Cooldown": 120, #スケールインが実行されてから次のスケールインが実行されるまでの時間
  "MetricAggregationType": "Average"
}
```

```
aws application-autoscaling put-scaling-policy --service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id  service/sample-cluster/sample-service \
--policy-name sqs-queue-step-low-scaling-policy \
--policy-type StepScaling \
--step-scaling-policy-configuration file://low-policy-config.json

```

### スケールアウトアラーム
ポリシーが定義できたので、スケーリングのトリガーとなるCloudWatchアラームを設定します。
ステップスケーリングポリシーを利用する場合、手動でCloudwatchアラームを追加する必要があるため、`put-metric-alarm`コマンドを利用して設定します。

https://docs.aws.amazon.com/cli/latest/reference/cloudwatch/put-metric-alarm.html


config内の`AlarmActions`で先ほど設定したスケーリングポリシーを指定することにより、アラーム発生をトリガーにしてオートスケーリングを実行することができます。

```high-alarm-config.json
{
  "AlarmName": "sample-sqs-high-alarm",
  "AlarmDescription": "high alarm policy for sample-queue",
  "TreatMissingData": "notBreaching",
  "ActionsEnabled": true,
  "OKActions": [],
  "AlarmActions": [

 "arn:aws:autoscaling:ap-northeast-1:<AWS_ACCOUNT_ID>:scalingPolicy:xxxxxxxxxx:resource/ecs/service/default/sample-service:policyName/sqs-queue-step-high-scaling-policy" #スケールアウトポリシーのarn

  ],
  "MetricName": "ApproximateNumberOfMessagesVisible",
  "Namespace": "AWS/SQS",
  "Statistic": "Sum",
  "Dimensions": [
    {
      "Name": "QueueName",
      "Value": "sample-queue"
    }
  ],
  "Period": 60,
  "Unit": "Count",
  "DatapointsToAlarm": 1,
  "EvaluationPeriods": 1,
  "Threshold": 1,
  "ComparisonOperator": "GreaterThanOrEqualToThreshold" #「-以上」でアラート発報
}
```

```
aws cloudwatch put-metric-alarm --cli-input-json file://high-alarm-config.json
```

### スケールインアラーム

```low-alarm-config.json
{
  "AlarmName": "sample-sqs-low-alarm",
  "AlarmDescription": "low alarm policy for sample-queue",
  "TreatMissingData": "notBreaching",
  "ActionsEnabled": true,
  "OKActions": [],
  "AlarmActions": [
 "arn:aws:autoscaling:ap-northeast-1:<AWS_ACCOUNT_ID>:scalingPolicy:xxxxxxxxxx:resource/ecs/service/default/sample-service:policyName/sqs-queue-step-low-scaling-policy" #スケールインポリシーのarn
     ],
  "MetricName": "ApproximateNumberOfMessagesVisible",
  "Namespace": "AWS/SQS",
  "Statistic": "Sum",
  "Dimensions": [
    {
      "Name": "QueueName",
      "Value": "sample-queue"
    }
  ],
  "Period": 60,
  "Unit": "Count",
  "DatapointsToAlarm": 1,
  "EvaluationPeriods": 1,
  "Threshold": 1,
  "ComparisonOperator": "LessThanThreshold"　#「-より少ない」でアラート発報
}
```

```
aws cloudwatch put-metric-alarm --cli-input-json file://low-alarm-config.json
```

これでautoscailingの設定は完了です。

## おわりに
個人的にAutoscailingの設定項目はややこしいと感じることが多く、整理して理解するのが大変だったのですが、なんとか実装できてよかったです。この記事がどなたかのお役に立てば幸いです。

明日は@nobuhide-saitoさんの記事です!お楽しみに!


## 参考
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-auto-scaling.html

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html