背景
ECSにデプロイしたAPIサーバの役割を果たすタスクが一定時間ごとに落ちて毎回ECS agentによって立ち上げられるという事象を観測した。大元の原因を特定するのに手間取ったため、メモとして残しておく。
状況整理
ECSはALBのターゲットとして登録してしていたので、ALBによるヘルスチェックが行われている。また、ECSのタスク定義でコンテナヘルスチェックも行なっている。
このような場合、以下のような現象が観測される。
- ALBのヘルスチェックが失敗している
- ECSのコンテナヘルスチェックが失敗している
- サーバプロセスがexitしている
これらの現象は相互に影響を与え合う。例えば、サーバプロセスがexitするとECSのコンテナヘルスチェックやALBのヘルスチェックが失敗する。また、ネットワークの設定ミスがあった場合、サーバプロセスは動いているのにALBによるヘルスチェックが失敗して、ALBがECS agentにコンテナを落とすことを命令し、サーバプロセスがSIGTERMを受け取ってexitするかもしれない。さらに、ECSのコンテナヘルスチェックの設定ミスで、サーバプロセスは動いているのにコンテナヘルスチェックが失敗して、サーバプロセスにSIGTERMが送られるかもしれない。
すなわち、大元の原因を突き止めるにはちょっとした工夫が必要である。
絞り込み1
まず重要なヒントは、コンテナが起動されて落ちるまでの時間が毎回一定かどうか、ということである。
一定でなければ、サーバプロセスが自らexitしている可能性が高い。その場合は、コンテナからCloudWatch logsなどにログを送信してエラーがおきていないかを確認する必要がある。これは比較的わかりやすい。
コンテナが起動されて落ちるまでの時間が毎回一定である場合は、さらに絞り込みを行う。
絞り込み2
コンテナが起動されて落ちるまでの時間が毎回一定であれば、(サーバプロセスは正常であるが)ネットワークやヘルスチェックの設定にミスがある可能性が大きい。
最初にやるべきことは、サーバプログラムへのリクエストのログをCloudWatch logsなどに送信してログを見れる状態にすることである。このリクエストログにはUser-Agentが含まれていなければならない。
User-Agentを含むリクエストログが見れる状態にできたら、タスクを再実行してみる。
ECSに設定したヘルスチェックコマンドがcurlを使ったものであれば、User-Agentの部分がcurlであるログが存在するはずである。
同じようにALBによるヘルスチェックが正しく行われていれば、リクエストログのUser-Agentが"ELB-HealthChecker"となっているログが存在するはずである。両方のヘルスチェックが設定されていた場合は、それぞれに対して以下の検証を行う。
もし、設定したヘルスチェックコマンドに対応するUser-Agent値がログに含まれていれば、ネットワークの設定は正しいがヘルスチェックの設定ミスが原因でサーバプロセスにSIGTERMが送られている可能性が高い。この場合、ヘルスチェックコマンドの設定は正しいことが保証されている。ヘルスチェックの設定にはコマンド以外にも、タイムアウトやインターバルやリトライ回数といったパラメータがある。ここでは、それらのパラメータ設定が間違っていることを疑う。ここで大事なのは、各ヘルスチェックの設定において、最短でUnhealthyになるまでの時間を求めることである。例えば、ECSのコンテナヘルスチェックでタイムアウトが20s、インターバルが100s, リトライが3回、スタートピリオドが30sという設定がなされていたとき、最短でUnhealthyになるのはインターバル100sのヘルスチェックが3回連続失敗する場合なので、(100 * (3 - 1)) + 20 + 30で250sである。もし、ECSのコンテナヘルスチェクとALBのヘルスチェックの設定が異っていれば、コンテナが起動して落ちるまでの時間と比較することで、どちらのヘルスチェックの設定を直すべきかがわかる。ここまでわかれば解決は容易いだろう。
一方、設定したヘルスチェックコマンドに対応するUser-Agent値がログに含まれていなければ、さらに絞り込みを行う。
絞り込み3
ヘルスチェックがなされた痕跡がリクエストログに残っていない場合、2つの可能性がある。
- ネットワークの設定ミスで、ヘルスチェックのリクエストがコンテナまで到達していない
- ネットワークの設定は正しいが、ヘルスチェックコマンドの設定が間違っている。
2の場合は、大抵の場合、以下のいずれかに該当するであろう。
- リクエストが送られるパスに対する応答が200以外のコードを返す。
- リクエストが送られるポート番号が間違っている。
- ECSのヘルスチェックコマンドでcurlを使っているが、curlがコンテナにインストールされていない。(alpineにはcurlが含まれていない。)
- ECSのヘルスチェックコマンドの設定形式が間違っている。
4のケースはデバックがしづらいがAWS::ECS::TaskDefinition HealthCheckやDocker公式ドキュメント CreateContainerを慎重に読み込むことでミスを見つけることは可能である。
これらに当てはまらない場合は、1.ネットワークの設定ミス、を疑う。ネットワークの設定ミスには、SubnetのNetworkACL設定や、ECS ServiceのSecurity group設定、タスク定義のport mapping設定など、様々な設定値が関係してくるので、一つ一つの設定を慎重に疑う必要がある。また、ネットワーク設定のデバッグは、踏み台サーバからリクエストを送ってみるなどの手段が有効である。
まとめ
上記の流れで大抵のケースは捕捉できると思う。