Go 製の API サーバーで Fargate Spot を使う

AWS Fargate で Go 製の API サーバーを動かしており、費用節約のために Fargate Spot を使おうと思った。そのために行った内容について。

実施内容は主に下記を参考にしている。

https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/

SIGTERM の処理

まず Fargate Spot を使うかどうかに関わらず停止シグナル SIGTERM の適切な処理が必要である。
今回は API サーバーを動かしているので、SIGTERM を受信したら Grateful Shutdown する。

前述の参考記事にも丁寧にサンプルコードが掲載されているが、Go 1.16 以降のシグナル処理はもっと簡単に書くことができる。

参考: https://zenn.dev/nekoshita/articles/dba0a7139854bb

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "Hello World!")
	})

	server := &http.Server{
		Addr:    ":8080",
		Handler: nil,
	}

    go func() {
		<-ctx.Done()
        fmt.Println("Caught SIGTERM, shutting down")
		ctx, cancel := context.WithTimeout(context.Background(), time.Second * 90)
		defer cancel()
		server.Shutdown(ctx)
	}()
	
	fmt.Println("Start server")
	err = server.ListenAndServe()
	if err != http.ErrServerClosed {
		log.Fatal(err)
	}
    fmt.Println("Main end")
}

なお SIGTERM 受信時に実行すべき処理の参考として、2023/2 にアップデートがあり、Fargate Spot でもロードバランサ―の登録解除→SIGTERM 受信という順番が保証されるようになった。

参考: https://aws.amazon.com/jp/about-aws/whats-new/2023/02/amazon-elastic-container-service-accuracy-service-load-balancing/

以前は順番が保証されておらず、SIGTERM 受信後に新規リクエストが来る可能性があったので、SIGTERM 受信時にロードバランサーから登録解除する必要があった。

ECS の設定

stopTimeout

タスクが使用するコンテナ定義で stopTimeout 値を必要に応じて変更する。
これはタスクが SIGTERM を受信してから SIGKILL が飛んでくるまでの時間であり、デフォルトは 30 秒である。
この値を長くとっておくことにより、終了処理中に強制終了される可能性を減らすことができる。
なお Fargate Spot の中断通知が発生してから 120 秒後に強制終了させられるので、stopTimeout は 120 秒以下にする必要がある。

deregistrationDelay

ロードバランサーを前段に置いている場合、登録解除の遅延(deregistrationDelay)設定も変更の必要がある。
これは登録解除するターゲットに未処理のリクエストやアクティブな接続がある場合に登録解除プロセスが完了するのを待つ時間で、デフォルトで 300 秒となっている。

タスクがシャットダウンされる前にターゲットグループの登録を解除する必要があるので、FARGATE_SPOT に関連付けられているターゲットグループの登録解除の遅延は 120 秒以下の値に設定する。

シャットダウンの流れ

先に貼った参考記事に書いてある通り、流れは

(1) インスタンスが DRAINING 状態になる -> この 120 秒後にインスタンスは終了する
(2) ALB が登録解除プロセスを開始 -> deregistrationDelay 待ってから登録解除
(3) ALB が登録解除を完了したらタスクが SIGTERM を受信 -> stopTimeout 待ってから SIGKILL

となっている。
前述の2023/2のアップデートの内容を踏まえると、今は Fargate Spot でもこの流れのはずである。

実際に私は当初 deregistrationDelay をデフォルトの 300 秒から変え忘れていたため、(2) の処理が終わって (3) の処理が起動する前に (1) の処理によってタスクが強制終了する状態となっていた。

したがって stopTimeout, deregistrationDelay それぞれ 120 秒を最大値として書いたが、実際には stopTimeout + deregistrationDelay が 120 秒以下になっていないと期待した動作にならないと考えられる。

CDK の実行の参考

私が作っているアプリケーションの cdk 実装は下記のようになっている。

app-stack.ts

以上