この記事はGunosy Advent Calender 2024の19日目の記事です。昨日の記事はskozawaさんの「第2回インフォメーション・ヘルスAWARDに参加しました」でした。
はじめに
こんにちは、mtjuneです。サーバーサイドエンジニアとしてサービスの開発・保守を行っています。
もう7ヶ月ほど前の話になりますが、AWS OpsWorks が2024年5月にサービスを終了する*1ため、弊社ではOpsWorks上で動作していた一部のWebサーバーをコンテナオーケストレーションサービスである ECS(Elastic Container Service) に移行しました。
ECS 上では ECS タスクという単位でアプリケーションが動作しますが、この ECS タスクはデプロイ、スケールイン、Spot インスタンスの終了など、様々な要因で終了することがあります。
このとき終了処理を適切に行わないとリクエストの処理が途中で打ち切られたり、新規リクエストを受け付けられなかったりでクライアントに適切なレスポンスを返せなくなってしまいます。この終了処理をきちんと行う、というのが本記事の趣旨となります。
ECS タスクの graceful shutdown
ECS タスクが終了される場合、コンテナに終了シグナル(SIGTERM
など)が送信されます。ECS タスクのコンテナ内で動いているアプリケーションはこの終了シグナルを受けとってプロセスを終了させることになりますが、何も考えず即座に終了させると上述したように処理中のリクエストが打ち切られてしまう可能性があります。
そのため終了シグナルを受け取った後、
- 新規リクエストが送られてこなくなっている
- 受信済みのリクエストの処理が完了している
ことを保証して終了するようにしたいです(このことを graceful shutdown と言ったりします)。
また、今回のケースでは各 ECS タスクにサイドカーとして nginx コンテナを配置し、アプリケーションはこの nginx コンテナを通してリクエストを受け取るようにしています。そのためアプリケーションコンテナだけではなく、nginx コンテナも同じように graceful shutdown させる必要があります*2。
ではここから、graceful shutdown させるために今回行った設定を紹介していきます。
コンテナ間の依存関係を設定する
ECS ではコンテナ間の依存関係を指定することでコンテナの起動順を制御することができますが、終了順(各コンテナに終了シグナルを送信する順番)も制御してくれます。つまり、ここで「nginx コンテナ」を「アプリケーションコンテナ」依存するように設定すると、以下のような順番で処理が進みます
- nginx コンテナに終了シグナルを送信
- (nginx コンテナの終了後)アプリケーションコンテナに終了シグナルを送信
こうすることでアプリケーションコンテナの前段にいる nginx コンテナが必ず先に終了するため、nginx コンテナが graceful shutdown するようになれば自然とECSタスク全体も graceful shutdown するようになってくれます。
コンテナ間の依存関係は ECS タスク定義から設定できます。
nginx コンテナの graceful shutdown
次は nginx コンテナで graceful shutdown を行うことを考えます。
受信済みのリクエストの処理が完了している
受信済みのリクエストの処理が完了している
ことについては、nginx は SIGQUIT
シグナルを受けたときに残っているコネクションの処理を完了させてから終了するようになっていました。さらに nginx の公式 docker image では終了シグナルとして SIGQUIT
を使うようになっていた*3ため、このイメージをベースイメージとしたnginxコンテナを作るだけで解決してしまいました。
新規リクエストが送られてこなくなっている
次に 新規リクエストが送られてこなくなっている
ことについてですが、こちらに関しては ECS タスクへリクエストをルーティングしている側(Application Load Balancer や ECS サービスディスカバリなど)によって行うべき対応が変わると思います。
今回は ECS タスクへリクエストをルーティングするのに ECS サービスディスカバリを利用しており、TTL を 10秒に設定していたため終了シグナルを受けてから少なくとも10秒はリクエストが送られてくる状態でした。これに関しては単純にシェルスクリプトで「リクエストが送られなくなるのに十分な時間待機してから nginx に SIGQUIT を流す」処理を書いて対応することにしました。
#!/bin/sh set -e quit_after_wait() { # 20秒待機してから引数で指定されたプロセスに SIGQUIT を送る # $1: child_pid sleep 20 # nginx に SIGQUIT を送る kill -s QUIT $1 wait $1 exit $? } # nginx プロセスを開始 /usr/sbin/nginx & # nginx のプロセス番号を child_pid に保持 child_pid=$! # SIGQUIT を受け取ったときに quit_after_wait を実行するように設定 trap "quit_after_wait $child_pid" SIGQUIT wait ${child_pid} exit $?
まとめ
この記事では ECS タスクで Web サーバーを動かすにあたり、適切に graceful shutdown させるために行った設定を紹介してきました。この記事では非常にざっくりとした内容しか書いてないので、より詳しい解説を読みたい方は AWS から出ている記事を読んでもらうのが良いと思います。
明日の Gunosy Advent Calendar 2024 はサンドバーグさんの「クラウドサービスとRails 7: Master Keyの管理で何故か沼るのは自分だけ?」です。
*1:https://docs.aws.amazon.com/ja_jp/opsworks/latest/userguide/stacks-eol-faqs.html
*2:アプリケーションコンテナが動いていても、nginx コンテナが終了すると新規リクエストを受けられなくなる