キャパシティ編もよかったらどうぞ
はじめに
Amazon ECS と AWS Fargate を使ってサービスを提供しているときに、コストを最適化する方法があります。一例として、次のコスト最適化の方法を挙げます。
- Fargate Spot : 空きキャパシティを利用
- Savings Plans : 1年 or 3年の期間契約
上記の2個は Fargate の購入方法が異なります。Fargate Spot は通常の価格と比べて最大 70% OFF の価格で利用できます。EC2 スポットインスタンスと同様の概念になっていて、Fargate Spot も AWS の空きキャパシティを利用します。Fargate Spot の空きキャパシティを確保できるかぎり、タスクとしてコンテナ群を動かすことが出来ますが、空きキャパシティが確保できなくなった場合にタスクが中断する可能性があります。タスクで動かしているアプリケーションは、中断する2分前に中断通知(SIGTERM
)を受け取ることが出来ます。SIGTERM
を受け取り、正しく処理が完了できるようなアプリケーションは Fargate Spot に向いていると言えます。
Savings Plans は、1年 or 3年の期間を指定した購入方法です。指定した期間の利用を約束する購入方法となっており、途中で使わなくなっても料金が発生する仕組みです。その代わり、通常料金と比べて最大 50% の割引で利用できます。こちらは、アプリケーションの変更が不要で、購入方法の変更だけでコスト最適化が出来る特徴があります。状況に合わせて、Fargate Spot と Savings Plans を使い分けするといいと思います。
Fargate Spot の詳細な説明は、以下の参考リソースも合わせてご確認ください。
参考リソース
- https://aws.amazon.com/jp/blogs/news/aws-fargate-spot-now-generally-available/
- https://d1.awsstatic.com/webinars/jp/pdf/services/20190306_AWS-Blackbelt-EC2Spot.pdf
今回の記事では、Fargate Spot を利用するときの「中断」について、考慮しなければならない点を整理していきます。また、考慮点と合わせて、設定方法も紹介していきます。
Fargate Spot の中断で考慮するべき内容
Fargate Spot は前述のとおり、キャパシティが確保できなくなったときに中断されます。24時間365日、いつでも中断される可能性があるので、それに備えておく必要があります。中断に備えて、次の点を考慮しておくと良いでしょう。
- アプリケーションが中断 (
SIGTERM
) を受け取って、アプリケーションの停止やログの出力をおこなう - 何秒前に中断通知を受け取るか、タスク定義で
stopTimeout
の指定を行う -
中断するタスクを、ALB からデタッチするサービスアップデートにより不要となりました - ALB の Deregistration Delay を2分未満にする
それぞれの内容についてみていきます。
アプリケーションが中断 (SIGTERM
) を受けとる
まず、Fargate Spot の中断が発生した時に何が起こるか説明します。AWS Blog から引用した次の画像を基に説明します。
引用 : https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/
Fargate Spot で空きキャパシティが確保できなくなった時に、AWS はアプリケーションに通知を送ります。タスク定義のパラメータの中にあるstopTimeout
を指定することで、どれくらい前に通知をしてくれるか変更できます。stopTimeout
は最大 120 秒なので、中断まで2分の猶予があると言えます。
どのように通知されるかというと、コンテナのエントリプロセス (通常は PID 1) に SIGTERM
シグナルが送られます。SIGTERM
を受け取ったアプリケーションは、猶予時間の中で安全にアプリケーションを終了出来ます。中断される前の処理としては、次の点を考慮すると良いと思います。
- アプリケーションの安全な停止 (新規受付停止、途中の処理の完了)
- バッファしているログの出力・退避
- 長い処理の中断や、チェックポイントの書き出し
SIGTERM
が送られてきてから、stopTimeout
で指定した秒数が経過すると SIGKILL
が送られます。これによって、アプリケーションが中断される形です。なお、SIGTERM
のシグナルを無視していても、SIGKILL
で強制終了されます。
アプリケーション側でどのように SIGTERM
を受け取れるのか、サンプルアプリケーションのコードが AWS Blog に書かれています。このあたりを確認してみると良いでしょう。
AWS Blog : https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/
Go 言語の例を AWS Blog から取り上げてみると、こんな感じです。SIGTERM
を受け取ったあとのロジックをプログラム内で組み立てられます。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
//registers the channel
signal.Notify(sigs, syscall.SIGTERM)
go func() {
sig := <-sigs
fmt.Println("Caught SIGTERM, shutting down")
// Finish any outstanding requests, then...
done <- true
}()
fmt.Println("Starting application")
// Main logic goes here
<-done
fmt.Println("exiting")
}
また、コンテナイメージのエントリーポイントをどのように構成しているのかも重要なポイントです。コンテナの ENTRYPOINT
を /bin/sh -c my-app
に設定している例を見ていきます。この例では、以下の理由から my-app は正常にシャットダウンしません。
- sh がエントリプロセスとして実行され、さらに my-app プロセスを生成します。これは sh の子プロセスとして実行されます。
- sh は、コンテナが停止したときに SIGTERM シグナルを受信しますが、my-app に渡されず、このシグナルに対してアクションを実行するように設定されていません。
- その後コンテナが SIGKILL シグナルを受信すると、sh とすべての子プロセスが直ちに終了します。
このあたりの対処方法が AWS Blog に記載されています。合わせて参照してください。
URL : https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/
stopTimeout を 120秒にする
前述のとおり、Fargate Spot で中断する際に、事前に通知されます。どれくらい前に通知されるのかという猶予時間を、タスク定義の stopTimeout
で指定できます。デフォルトは空白となっており、30秒になります。最大 120 秒まで伸ばせるので、基本的には伸ばすのが良いと思います。
Target Group から中断する Task をデタッチする (不要)
Fargate Spot が中断する際に、ELB のデタッチをする必要がありましたが、2023 年 2 月 13 日サービスアップデートにより不要になりました。Fargate Spot で SIGTERM が送信される前に、ELB の登録解除がされるようになりました。サービスアップデートは以下の URL をご確認ください。
https://aws.amazon.com/jp/about-aws/whats-new/2023/02/amazon-elastic-container-service-accuracy-service-load-balancing/
Fargate Spot を利用している場合、中断通知が来たときに、ALB に紐づく Target Group から Task をデタッチするのをお勧めします。以下の AWS Blog に次の記載があります。
引用 : https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/
スポットインスタンスの自動ドレイン機能は EC2 スポットインスタンスで機能しますが、
FARGATE_SPOT
として実行されるタスクでは、ロードバランサーのターゲットグループから登録解除されてからタスクがSTOPPED
状態に移行するという保証はありません。望ましくないエラーを回避するために、必要な API を呼び出して、ロードバランサーのターゲットグループからタスクを登録解除することをお勧めします。これを処理する方法はいくつかあります。
SIGTERM
ハンドラーの中で、DeregisterTargets API を呼び出して、ターゲットグループからタスクを登録解除します。この方法では、AWS SDK をアプリケーションに組み込む必要があります。もしくは、ロードバランサーからタスクを登録解除するロジックを含むサイドカーコンテナを実行することもできます。- タスク状態変更イベントでトリガーされる Lambda 関数を実装します。GitHub の例を参考にできます。イベントの
stopCode
がTerminationNotice
のときに、Lambda 関数でロードバランサーからタスクを登録解除するようにします。
EC2 のスポットインスタンスの場合は、自動的に ALB からデタッチしてくれる機能がありますが、Fargate Spot の場合はこの機能が実行されないことがあるので、別途実行する必要があります。
GitHub の aws-samples で公開されているものを利用すると、簡単に環境にデプロイできます。
URL : https://github.com/aws-samples/ecs-fargate-drain-function
このデプロイ方法は後ほど紹介していきます。
Target Group の Deregistration Delay を2分未満にする
Target Group から Task をデタッチする際の、Deregistration Delay
を考慮する必要があります。Deregistration Delay
は、Target Group から安全にデタッチするための機能です。Target Group からデタッチするときに、その Task は一定時間 Draining
の状態になります。Draining
状態のタスクは、新規リクエストを停止する一方、既存のコネクションを維持しつつ閉じられることを待機する機能です。いきなりデタッチすると途中のリクエストが破棄されてしまうので、安全に待機するための機能ですね。
この Deregistration Delay
がデフォルトだと 5分間となっています。Fargate Spot の stopTimeout
の最大は2分なので、デフォルトのままだと先に Task が終了してしまう形になります。Deregistration Delay
を2分未満の値に設定するのがおすすめです。
考慮点を設定してみる
上記の考慮点を気にしながら、実際に ECS と Fargate Spot でコンテナを動かしてみましょう!
Go言語で SIGTERM を受け取るプログラムを作成
まず、Fargate Spot 上で動かすプログラムを準備します。今回は、Go 言語のプログラムにします。AWS Blog のサンプルコートを参考にしました。
URL : https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/
ポイントは次の通りです。
- Go 言語で Web サーバーを構成
- SIGTERM を受け付けて、ログにその旨を出力する
package main
import (
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 好きな文字を入れる
fmt.Fprintf(w, "こんにちは、Fargate Spot!\n")
fmt.Fprintf(w, "\n")
println("INFO : HTTP アクセスを受け付けました")
}
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
// SIGTERM を処理するためにチャンネルを登録
signal.Notify(sigs, syscall.SIGTERM)
go func() {
// SIGTERM を受け取ったときに行う処理
sig := <-sigs
fmt.Println("Caught SIGTERM, shutting down")
// インスタンスが停止する前に行うべき処理をする。この例では標準出力に出力するのみ。
println("SIGTERM を受け付けました : " + sig.String())
// os.Exit(0)
done <- true
}()
http.HandleFunc("/", handler) // ハンドラを登録してウェブページを表示させる
http.ListenAndServe(":8080", nil)
}
Go のプログラムをビルドします。
go build -o app
ECR に Docker Image を Push
作成したプログラムを Docker Image 化して、ECR に Push します
FROM public.ecr.aws/docker/library/golang:1.16
RUN mkdir /workdir
COPY app /workdir
EXPOSE 8080
CMD ["/workdir/app"]
Docker Image をビルドします
docker image build -t xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-spot-testapp:0.0.2 .
ECR にプッシュします
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-spot-testapp:0.0.2
Tast Definition 作成
Fargate Spot の通知をどれくらい前にしてくれるのか、stopTimeout
の設定をしていきます。今回の手順では、既に存在する Task Definition を更新する手順ですが、新規作成でも基本的な画面構成は同じです。(New ECS Experience の画面では出てこないので、一時的に古いページを使います)
Create new revision を選択します
対象のコンテナを選択します
Stop Timeout を 120 にして、Update します
Create を押します
Fargate Spot で ECS Service の作成
新たに作成した Task Definition を使って、Service を作成します。
Capacity Provider strategy から FARGATE_SPOT を選択します。これで、Fargate Spot が使われるようになります。
Task Definition を利用
新たに ALB の作成
Security Group などのネットワーキングを指定
Service が作成されました
対象の Task の詳細画面を見ると、FARGATE_SPOT
と書かれており、正常に設定できていることがわかります。
Target Group の Deregistration delay の設定変更
考慮点で書いた通り、Target Group から Task をデタッチするときに、安全に取り外すための待機時間が設定されています。これをデフォルトの 300 秒から、120秒未満に設定するのが良いです。今回は、115 秒で設定してみます。
ECS Service に紐づいている Target Group を Edit します。
Deregistration Delay
を変更して Save を押します
更新されました
中断通知を受け取って、ALB からデタッチする Lambda Function を作成 (不要)
サービスアップデートにより不要となりました。
考慮点で書いた通り、Fargate Spot の中断通知をトリガーに ALB からデタッチする処理が必要です。今回の記事では、簡単に設定できる、EventBridge と Lambda の組み合わせで行います。aws-samples として GitHub に公開されている内容を基本的にはそのまま利用できるため、比較的簡単に作成できます。
以下の URL で公開されています
URL : https://github.com/aws-samples/ecs-fargate-drain-function
上記の Repository を clone します
git clone https://github.com/aws-samples/ecs-fargate-drain-function.git
clone してきたディレクトリに移動します
cd ecs-fargate-drain-function/
clone してきたディレクトリに Dockerfile があるため、まずはこれを build します
docker build -t aws-samples/aws-lambda-deregister-targets-fargate-spot .
Makefile も一緒に用意されているので、make compile に定義されている内容を実行します。go build
などを行っているようです。
make compile
Makefile で定義されている内容を呼びだして、terraform plan を実行します。
- 新たにデプロイするリソースを表示
- 引数に、対象のリージョンや、アクセスキーを指定
make awsDefaultRegion=ap-northeast-1 awsAccessKey=xxxxx awsSecretKey=yyyy plan
新たにデプロイされるリソース群が表示されます。いくつか大事なものをピックアップします。
- EventBridge Rule
- Lambda Function
- SQS DeadLetter Queue
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_cloudwatch_event_rule.fargate-spot-rule will be created
+ resource "aws_cloudwatch_event_rule" "fargate-spot-rule" {
+ arn = (known after apply)
+ description = "Capture Fargate Spot tasks that are going to be shutdown."
+ event_bus_name = "default"
+ event_pattern = jsonencode(
{
+ detail = {
+ clusterArn = [
+ "*",
]
}
+ detail-type = [
+ "ECS Task State Change",
]
+ source = [
+ "aws.ecs",
]
}
)
+ id = (known after apply)
+ is_enabled = true
+ name = "tf-deregister-targets-fargate-spot-rule"
+ name_prefix = (known after apply)
+ tags_all = (known after apply)
}
# aws_cloudwatch_event_target.rule_target_lambda_deregister will be created
+ resource "aws_cloudwatch_event_target" "rule_target_lambda_deregister" {
+ arn = (known after apply)
+ event_bus_name = "default"
+ id = (known after apply)
+ rule = "tf-deregister-targets-fargate-spot-rule"
+ target_id = "aws-lambda-deregister-target-go"
}
# aws_iam_role.iam_for_lambda will be created
+ resource "aws_iam_role" "iam_for_lambda" {
+ arn = (known after apply)
+ assume_role_policy = jsonencode(
{
+ Statement = [
+ {
+ Action = [
+ "sts:AssumeRole",
]
+ Effect = "Allow"
+ Principal = {
+ Service = "lambda.amazonaws.com"
}
},
]
+ Version = "2012-10-17"
}
)
+ create_date = (known after apply)
+ force_detach_policies = false
+ id = (known after apply)
+ managed_policy_arns = (known after apply)
+ max_session_duration = 3600
+ name = "tf-role-deregister-target-fargate-spot"
+ name_prefix = (known after apply)
+ path = "/"
+ tags_all = (known after apply)
+ unique_id = (known after apply)
+ inline_policy {
+ name = (known after apply)
+ policy = (known after apply)
}
}
# aws_iam_role_policy.deregister_policy will be created
+ resource "aws_iam_role_policy" "deregister_policy" {
+ id = (known after apply)
+ name = "tf-policy-deregister-target-fargate-spot"
+ policy = (known after apply)
+ role = (known after apply)
}
# aws_lambda_function.lambda_deregister_targets_fargate_spot will be created
+ resource "aws_lambda_function" "lambda_deregister_targets_fargate_spot" {
+ architectures = (known after apply)
+ arn = (known after apply)
+ filename = "build/aws-lambda-deregister-target-go.zip"
+ function_name = "tf_deregister_targets_fargate_spot"
+ handler = "aws-lambda-deregister-target-go"
+ id = (known after apply)
+ invoke_arn = (known after apply)
+ last_modified = (known after apply)
+ memory_size = 128
+ package_type = "Zip"
+ publish = false
+ qualified_arn = (known after apply)
+ reserved_concurrent_executions = -1
+ role = (known after apply)
+ runtime = "go1.x"
+ signing_job_arn = (known after apply)
+ signing_profile_version_arn = (known after apply)
+ source_code_hash = "3Tsbu8NGlvr09uXoizEwof39r2atqL6aP20b56IkXF4="
+ source_code_size = (known after apply)
+ tags_all = (known after apply)
+ timeout = 10
+ version = (known after apply)
+ dead_letter_config {
+ target_arn = (known after apply)
}
+ ephemeral_storage {
+ size = (known after apply)
}
+ tracing_config {
+ mode = (known after apply)
}
}
# aws_lambda_permission.allow_cloudwatch_to_call_deregister_lambda will be created
+ resource "aws_lambda_permission" "allow_cloudwatch_to_call_deregister_lambda" {
+ action = "lambda:InvokeFunction"
+ function_name = "tf_deregister_targets_fargate_spot"
+ id = (known after apply)
+ principal = "events.amazonaws.com"
+ source_arn = (known after apply)
+ statement_id = "AllowExecutionFromCloudWatch"
}
# aws_sqs_queue.deadletter_queue_for_deregister_lambda will be created
+ resource "aws_sqs_queue" "deadletter_queue_for_deregister_lambda" {
+ arn = (known after apply)
+ content_based_deduplication = false
+ deduplication_scope = (known after apply)
+ delay_seconds = 0
+ fifo_queue = false
+ fifo_throughput_limit = (known after apply)
+ id = (known after apply)
+ kms_data_key_reuse_period_seconds = (known after apply)
+ max_message_size = 262144
+ message_retention_seconds = 1209600
+ name = "tf-deadelteer-queue-failed-deregister"
+ name_prefix = (known after apply)
+ policy = (known after apply)
+ receive_wait_time_seconds = 10
+ tags_all = (known after apply)
+ url = (known after apply)
+ visibility_timeout_seconds = 30
}
実際にデプロイを行います。AWS 上にリソースが生成されます
make awsDefaultRegion=ap-northeast-1 awsAccessKey=xxxx awsSecretKey=yyy apply
自動生成されたリソース群をいくつか確認していきましょう。
EventBridge
Rule
- 全てのクラスターで、ECS Task に何かしらの State 変化をトリガーにしている
全ての ECS クラスター上で、Task の State が変化したことトリガーにして、Lambda Function を起動
Lambda
Lambda Function が生成されている
この Function の中身は、こちらのプログラムとなっている
- Lambda Function が受け取った Event を、CloudWatch Logs に出力
- Event の中に、
TerminationNotice
が含まれていると処理を行う。含まれていないと無視する。 - Service 名と TargetGroup 名を取得して、ALB から登録を解除する
動作確認
EventBridge のルール設定を見ると、Fargate Spot の中断通知に限らず、手動で ECS Task を停止しても、Lambda Function が起動することが期待できます。
ただ、自分の環境だと、Terraform で Deploy しただけだと正常に動作しませんでした。そこで、手動で新たに EventBridge の Rule を設定すると正常に動作しました。
Create rule
Next
Next
Lambda Function を指定
Dead Letter Queue も Terraform をつかってデプロイされているので、指定
Create Rule を押します
その後、動作確認として ECS Task を手動停止します
Lambda の実行ログが CloudWatch Logs に出力されます。Lambda が受け取った Event を JSON 形式で出してくれています。
付録 : Spot の中断通知をメールで受け取る
ここまで設定しましたが、Fargate Spot の中断がなかなか起きないので、実際の動作確認がまで時間が掛かります (起きないとはいっても、いつでも起きる可能性があるので、それに備えておくのは重要です)
Fargate Spot の中断が発生したときに、すぐにログを確認するために通知設定をしておきましょう。次の URL を参考にしています。
参考 : https://aws.amazon.com/jp/premiumsupport/knowledge-center/fargate-spot-termination-notice/
EventBridge で Create Rule を押します。
適当に名前を入れて Next を押します
全てのクラスタを対象に、Fargate Spot の中断通知 TerminationNotice
が発生したときを条件にします。
{
"source": [
"aws.ecs"
],
"detail-type": [
"ECS Task State Change"
],
"detail": {
"stopCode": [
"TerminationNotice"
]
}
}
通知先の SNS Topic を選びます
Create rule を押します
Fargate Spot の発生をまちましょう。
参考URL
ECS のアプリケーションを正常にシャットダウンする方法
https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/
202109 AWS Black Belt Online Seminar Amazon Elastic Container Service − EC2 スポットインスタンス / Fargate Spot ことはじめ
https://www.slideshare.net/AmazonWebServicesJapan/202109-aws-black-belt-online-seminar-amazon-elastic-container-service-ec-fargate-spot
ecs-fargate-drain-function
https://github.com/aws-samples/ecs-fargate-drain-function
Handling Fargate Spot termination notices
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/fargate-capacity-providers.html#fargate-capacity-providers-termination
AWS Fargate Spot タスクで Spot 終了通知を処理するにはどうすればよいですか?
https://aws.amazon.com/jp/premiumsupport/knowledge-center/fargate-spot-termination-notice/