ECS タスクの終了時にコンテナの依存関係が考慮されない問題を解決するコマンドを作った

ECS タスクは、起動時にはコンテナの依存関係を考慮するのに終了時は全く考慮しません。次のコメントからもよくわかります。

A container can always stop, die, or reach whatever other state it wants regardless of what dependencies it has

cf. agent/engine/dependencygraph/graph.go#L179-L180

これの何が問題かというと、fluentd のようなデータ収集用のサイドカーコンテナが動いている場合に、メインのコンテナよりも先にサイドカーコンテナが停止してデータが消失するということが起きてしまいます。

このことは 2 年半前から issue に上がっているんですが、未だに解決されていません。
cf. Termination order of linked containers · Issue #474 · aws/amazon-ecs-agent

最近ようやく動き出したみたいですが、各コンテナのどの状態に依存しているかを細かく指定できるようにしたり、今まで ECS_CONTAINER_STOP_TIMEOUT でホスト単位でしか指定できなかった docker のタイムアウト時間も指定できるようにしたりと、色々検討しているようなので根本解決されるのはまだ先と思われます。

というわけで、終了の依存関係を指定できるコマンドを作成しました。
https://github.com/abicky/ecswrap

最近 Go のコードを読む機会が増えたこともあって、不慣れではありますが Go で書いてみました。

使い方

example に載せているとおりですが、fluentd の場合は次のような Dockerfile を書けばよいです。net パッケージを使っているので、CGO_ENABLED=0-tags netgo を指定しないとダイナミックリンクになり、alpine ベースの image で起動に失敗します。1

FROM golang:1.11.5 AS builder

ENV GOPATH /go

RUN go get -d github.com/abicky/ecswrap \
  && cd $GOPATH/src/github.com/abicky/ecswrap \
  && CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s"

FROM fluent/fluentd:v1.3.3-1.0

COPY --from=builder /go/src/github.com/abicky/ecswrap/ecswrap /usr/local/bin/
ENTRYPOINT ["tini", "--", "ecswrap", "-v", "--", "/bin/entrypoint.sh"]
CMD ["fluentd"]

docker image をビルドして ECR にプッシュしたら、タスク定義でその docker image を使うようにし、先に停止すべきコンテナの名前を環境変数 ECSWRAP_LINKED_CONTAINERS にカンマ区切りで指定します。

{
  "family": "$TASK_FAMILY",
  "containerDefinitions": [
    {
      "name": "logger",
      "image": "$logger_repo_uri",
      "essential": true,
      "links": ["fluentd:fluentd"]
    },
    {
      "name": "fluentd",
      "image": "$fluentd_repo_uri",
      "essential": true,
      "environment": [{"name": "ECSWRAP_LINKED_CONTAINERS", "value": "logger"}]
    }
  ],
  "cpu": "128",
  "memory": "256"
}

なお、デフォルトだと ECSWRAP_LINKED_CONTAINERS に指定したコンテナが 10 秒以内に停止しなかった場合はタイムアウトとなり、そのタイミングで受け取ったシグナルを子プロセスに送ります。この時間は ECSWRAP_STOP_WAIT_TIMEOUT を指定することで調整することができます。ただし、ECS_CONTAINER_STOP_TIMEOUT 以上の値を指定すると、先に ECS_CONTAINER_STOP_TIMEOUT に到達して SIGKILL で殺される可能性があるので注意してください。

仕組み

Go に慣れている人であれば 1 分ぐらいで読める規模のコードですが次のように動作します。

  1. exec.Command で指定されたコマンドを実行して子プロセスを作成
  2. 特定のシグナルを受け取るか、子プロセスが終了するのを待機
    • 受け取るシグナルは環境変数やコマンドラインオプションで指定できるようにした方が良いかも
  3. 特定のシグナルのうち、SIGQUIT, SIGINT, SIGTERM 以外のシグナルを受け取ったら、同じシグナルを子プロセスに送る
  4. SIGQUIT, SIGINT, SIGTERM を受け取ったら、ECSWRAP_LINKED_CONTAINERS で指定されたコンテナが終了するのを待ってから同じシグナルを子プロセスに送る
    • コンテナの状態は task metadata version 3 endpoint を 1 秒間隔で叩いて確認
    • これらのシグナルも環境変数やコマンドラインオプションで指定できるようにした方が良いかも

[ECS] [Proposal]: Container Ordering · Issue #123 · aws/containers-roadmap が導入されるまでの役目となるでしょうが、同じような悩みを抱えている人のお役に立てば幸いです。