起動タイプがEC2であるECSを高速化させるためのtips

# 概要

* 今回は、AWSの**起動タイプがEC2である**[Amazon Elastic Container Service - ECS](https://aws.amazon.com/jp/ecs/)をより高速化させるためのtipsについて記事にしました。

* 例えば、タスクの起動を高速化したい・・・だったり、クラスタの自動スケーリングを高速化したい・・・だったり、デプロイを高速化したい・・・といった場合に一読していただけると参考になるかもしれません。
* また、下記に記載されている設定方法や値については全てAWSの公式ドキュメントにあるものを載せています。それぞれのtipsごとにサマライズした公式ドキュメントのリンクを記載しておりますので、詳細を知りたい場合はリンクよりご参照いただけますと幸いです。

# ECSを高速化させるtips

## Amazon Elastic Container Service (ECS)とは何か

* Amazon Elastic Container Service (ECS)はコンテナアプリケーションをAWS上で簡単に実行、スケールすることができるスケーラブルなコンテナ管理サービスです。
* 詳細について知りたい場合はここでは言及しない為、AWS Black Beltなどをご参照いただければ幸いです。

https://aws.amazon.com/jp/blogs/news/webinar-bb-amazon-ecs-2020/

## タスクの起動を高速化

* まず初めにECSのタスクがプロビジョニングされる時の挙動から話をしていきます。

**①: ECSタスクが手動もしくは自動で起動すると、タスクが作成され、PROVISIONINGのフェーズとなります。**
**②: その後、タスクに使用可能なリソースができるまで、タスクは保留状態(PENDINGフェーズ)のままになります。**
**③: タスクが起動され、RUNNINGのフェーズに移行する前にACTIVATINGというフェーズを通過します。このフェーズでは、たとえば、複数のElasticLoadBalancing(ELB)ターゲットグループを使用するように設定されたサービスの一部であるタスクの場合に、ターゲットグループの登録をこの状態の間に行います。**
**④: 最後にRUNNINGフェーズとなり、タスクは正常に実行されます。**

![スクリーンショット 2022-06-21 13.58.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/208c7dac-0b3e-2550-1e0c-bf80e9af095d.png)

画像引用元 - https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-lifecycle.html

* ECSにおいてのタスクはサービススケジューラの挙動によって変化していきます。

* サービススケジューラというのは、サービスの状態を管理するためのもので、指定したスケジューリング戦略を実行し、起動に失敗したタスクを再スケジュールしたりします。
* どんな時に失敗になるかというと、例えば基盤となるインフラに障害が発生した場合、サービススケジューラはタスクを再スケジュールすることができます。

**サービススケジューラの主な仕事は、アプリケーションが常に必要な数のタスクを実行していることを確認することにあります。**

=> なので、サービス構成で指定したタスクの数、自動スケーリングされる時の指定した数に基づいています。
* またサービススケジューラの挙動としては、非同期ワークフローを使用してタスクをバッチで起動します。

ここまで説明した上で、じゃあどうやってタスクの起動を高速化していくのかという話になります。

### コンテナイメージのキャッシュ

* 起動タイプがEC2の場合、ECSエージェントがタスクを開始すると、リモートレジストリからDockerイメージをプルしてから、ローカルコピーをキャッシュします。

![スクリーンショット 2022-06-21 15.20.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/1b411014-6349-0449-35f4-5cbbe73baf77.png)

画像引用元 - https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/pull-behavior.html


* **このimageのpullの部分の時間短縮のために下記の設定のどちらかを入れます。**

  * **ECS_IMAGE_PULL_BEHAVIOR：once**
  => イメージは、同じコンテナインスタンスの前のタスクによってプルされなかった場合、もしくはキャッシュされたイメージが自動イメージクリーンアッププロセスによって削除された場合にのみリモートでプルされます。
それ以外の場合は、インスタンスにキャッシュされたイメージが使用されます。

  * **ECS_IMAGE_PULL_BEHAVIOR：prefer-cached**
  => キャッシュされたイメージがない場合、イメージはリモートでプルされます。
それ以外の場合は、インスタンスにキャッシュされたイメージが使用されます。
キャッシュされたイメージが削除されないようにするために、コンテナの自動イメージクリーンアップは無効になっています。

### 最適なインスタンスタイプを選択する

* 正しいインスタンスタイプの選択は、タスクで構成するリソース予約（CPU、メモリ、ENI、GPUなど）に基づいているため、性能に影響します。

### サービスを細かく分ける

* サービススケジューラの話を上記でしたのですが、ここで言っているのは、多数のタスクを伴う大規模なサービスをECSでやるのではなく、タスク数が多くなるのであれば、少ないタスクを伴う小規模なサービスとしてアプリケーションを設計することで高速化すべきだということです。

* 例えを上げるなら、1000個のタスクを持つ単一のサービスを作成するのではなく、それぞれ100個のタスクを持つ10個のサービスがあると、サービススケジューラがすべてのサービスのタスクプロビジョニングを並行して開始するため、展開速度が大幅に向上するといったことです。


### ロードバランサーのヘルスチェック

* ヘルスチェックには、展開速度に影響を与える2つのオプションがあります。
  * 1つはAmazonECSタスク定義用です。
  * もう1つはロードバランサー用です。

* デフォルトでは、ロードバランサーは、ターゲットが正常(Healthy)であると認識する前に、5回の正常性チェックに成功する必要があります。
* また、このチェックも30秒間隔で行われるので、合計で2分30秒かかる計算になります。（5*30/60）

=> **つまりECSでデプロイされたロードバランサを利用するコンテナが正常(Healthy)となるまでには、少なくともデフォルトで2分30秒以上の時間がかかると言えます.**

実際の設定値
* **HealthCheckIntervalSeconds：30秒（デフォルト）**
* **HealthyThresholdCount：5（デフォルト）**

#### AWSが公式のドキュメントで推奨している値

* じゃあ、この値どうすれば良いのとなるのですが、AWSの公式ドキュメントではサービスが起動して10秒以内に安定する場合は、オプションを次の値に設定して、ロードバランサーが合計10秒待機するように設定することを推奨しています。

* **HealthCheckIntervalSeconds：5**
* **HealthyThresholdCount：2**


![スクリーンショット 2022-06-21 16.22.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/3c55de8b-a8e6-ac90-8a87-04b4f04b3547.png)

画像引用元 - https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/load-balancer-healthcheck.html

### ロードバランサのコネクションドレイ

* コンテナで実行されているサービスとそれを利用しようとするクライアントとは持続的な接続を維持します。
* この挙動により、クライアントからの後続のリクエストが既存の接続を再利用できるようになります。

* 下記の図は、ロードバランサーにコンテナへのトラフィックを停止するように指示した場合のものです。
* クライアントがコネクションを閉じたかどうかを定期的にチェックします。
* ECSエージェントはロードバランサーを監視し、このコネクションが閉じられたことをロードバランサーが報告するのを待ちます。
* この時、コネクションは一定時間が経過後すると強制的に切断されるのですが、強制的な接続の切断までに一定の待機時間を設けています。

![スクリーンショット 2022-06-21 16.30.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/79b643c4-b25e-4e9f-2c27-cb92abc3afd7.png)

画像引用元 - https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/load-balancer-connection-draining.html

=> なので、ロードバランサーの設定を変更し、展開を高速化することができます。

ちなみに上記のロードバランサから登録解除前にロードバランサが待機する秒数設定は、下記のようになっています。
* **deregistration_delay.timeout_seconds：300秒（デフォルト）**


=> AWSの公式ドキュメントでは、この時間を応答時間が1秒未満のサービスである場合は、ロードバランサーがクライアントとバックエンドサービス間の接続を切断するまで5秒だけ待機するよう変更することを推奨しています。

* **deregistration_delay.timeout_seconds：5**

### SIGTERMの応答性

* 次の図は、ECSがタスクを終了する方法を示しています。
* ロードバランサによるコネクションドレインが終わると、ECSは最初にSIGTERMシグナルをタスクに送信して、アプリケーションを終了し、シャットダウンする必要があることを通知します。

* また、アプリケーションがSIGTERMを無視する場合(このシグナルを無視してしまうアプリケーションやアプリケーションフレームワークが存在するため)、ECSはプロセスを終了するためにSIGTERMシグナルの送信した後に一定時間待機します。
* ただ、待機時間を過ぎるとアプリケーションプロセスを強制的に停止するためにSIGKILLシグナルを送信します。
* この時の待機時間が30秒とデフォルトで設定されています。

![スクリーンショット 2022-06-21 16.41.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/98c89d65-746e-a6c6-5ac7-e5a80e90a771.png)


下記が、SIGKILLシグナル送信までの待機時間のデフォルトになります。
* **ECS_CONTAINER_STOP_TIMEOUT：30（デフォルト）**

=> AWSの公式ドキュメントでは、アプリケーションに1秒以上かかる場合は、値に2を掛けて、その数値を値として使用することを推奨しています。

## クラスタの自動スケーリングを高速化

* 次は、クラスタの自動スケーリングを高速化する方法について述べていきます。
* 起動タイプをEC2でECSを実行している場合、Amazon ECS Cluster Auto Scaling（CAS）を利用して、Amazon EC2 Auto Scalingグループ（ASG）のスケーリングを管理しているかと思います。
* CASを使用すると、ASGを自動的にスケーリングするようにECSを設定することができます。
* また、各クラスタには、1つ以上の[ECSキャパシティプロバイダー](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cluster-capacity-providers.html)があり、キャパシティプロバイダー名、Auto Scalingグループ、マネージドスケーリングおよびマネージド終了保護の設定で構成されています。
* ECSキャパシティプロバイダーは、アプリケーションの要求を満たすのに十分なコンテナインスタンスがあることを確認することにより、クラスター内のインフラを管理するために使用されます。

### ECSキャパシティプロバイダーに関するtips

* ECSキャパシティプロバイダーは、アプリケーションの要求を満たすために最終的にコンテナインスタンスを拡大/縮小します。 
* ちなみにECSが起動するインスタンスの最小数は、**デフォルトで1**に設定されています。
* なので、保留中のタスクを配置するために**複数のインスタンスが必要な場合**は、スケールアウトに時間がかかる可能性があります。

=> そのため、ECSキャパシティプロバイダの設定にある**minimumScalingStepSize** (ECSが一度にスケールインまたはスケールアウトするコンテナインスタンスの最小数)を増やすことができます。

=> また、**maximumScalingStepSize** (ECSが一度にスケールインまたはスケールアウトするコンテナインスタンスの最大数)が低すぎると、一度にスケールインまたはスケールアウトされるコンテナインスタンスの数が制限され、デプロイメントの速度が低下する可能性もあります。

**ちなみにECSキャパシティプロバイダの設定変更をする場合は、[API - UpdateCapacityProvider](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateCapacityProvider.html)からしかできないよう制限されている。** 

### インスタンスのウォームアップ期間の変更

* このインスタンスのウォームアップ期間というのはAutoScalingグループのCloudWatchメトリクスに反映されるまでの時間を指します。
* そして、このインスタンスのウォームアップ期間の値を変更することができます。defaultは300秒になっているので、
defaultより低い値に構成し、応答性の高いスケーリングをさせることができます。

## デプロイを高速化

* ここでは、デプロイを高速化させるためのtipsを紹介していきます。
* デプロイ速度の改善を行う際に考慮するポイントは、あるサービス更新のロールアウトが完了するまでにコンテナオーケストレータが行うステップがどのくらいあるかに依存します。
* また、理想的なオーケストレータの動きとしては、下記のようなステップになります。

  * 既存のコンテナを実行したまま、新しいコンテナを起動
  * 新しいコンテナが正常であることを確認
  * 古いコンテナを停止

=> ただ、デプロイメント構成とクラスター内の予約されていない空き領域(vCPU、メモリなど)によっては、すべての古いタスクを新しいタスクに置き換えるために、これを複数回実行する必要がある場合があります。


* 今回はECSのサービスをローリングデプロイする際に使用される**minimumHealthyPercent**と **maximumPercent**のデフォルト値を変更することでの高速化を見ていきたいと思います。

**それぞれのデフォルトの設定値**

* **minimumHealthyPercent：100％（デフォルト）**

=> この設定は、デプロイ中に状態を維持する必要があるサービスのタスク数の下限を表ます。
これは、desiredCountのパーセンテージとして表され、最も近い整数に切り上げられます。

* **maximumPercent：200％（デフォルト）**

=> この設定は、デプロイ中にRUNNINGまたはPENDINGの状態で許可されるサービスのタスク数の上限を表します。
こちらもdesiredCountのパーセンテージとして表され、最も近い整数に切り捨てられます。

なので、低稼働なアプリケーションのデプロイを高速化する場合であればこの設定値を変更することを公式のドキュメントでは推奨しています。

* minimumHealthyPercent: 50%
* maximumPercent: 200%

=> ただ、これは常に高い稼働状況にあるアプリケーションの場合にはデプロイ処理中であっても必要なタスク数が維持されていなければ可用性やレイテンシに悪影響を及ぼす可能性があるので、注意が必要です。

これらの値を使用した具体的なローリングデプロイの流れは下記の公式ドキュメントをご参照いただけると分かりやすいと思います。

https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/service-options.html

# 参考

https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/task.html

https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/capacity-cluster-speed-up-ec2.html

https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/deployment.html

https://toris.io/2021/04/speeding-up-amazon-ecs-container-deployments/