ECS の Enhanced Container Dependency Management でタスク終了時のコンテナの依存関係を考慮する

先日次のような記事を書きました。
ECS タスクの終了時にコンテナの依存関係が考慮されない問題を解決するコマンドを作った

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

その約 2 週間後に ecs-agent v1.26.0 がリリースされ、コンテナの依存関係を記述できるようになりました。
cf. Amazon ECS Introduces Enhanced Container Dependency Management
Pull Request はこちら https://github.com/aws/amazon-ecs-agent/pull/1904

ecswrap が約 2 週間でお役御免になったのは悲しいですが、とても嬉しい変更ですね!
というわけで、ecswrap でやっていたようなことを ECS タスクの定義で実現するにはどうすれば良いか解説します。

タスクの例

ecswrap の example を例として使います。

  • logger コンテナ(メインコンテナ)
    • 1 秒間隔で fluentd にイベントをポストするコンテナ。SIGTERM を受け取ってからも 5 秒間ポストし続ける。
  • fluntd コンテナ(サイドカーコンテナ)
    • イベントを受け取ったらそのデータを/fluentd/log/data.*.log に出力する
    • fluentd コンテナは SIGTERM を受け取ってから最大 10 秒間 logger コンテナが終了するのを待つ (10 秒は後述する ECSWRAP_STOP_WAIT_TIMEOUT のデフォルト値)

fluentd コンテナの Dockerfile は次のように、ecswrap 経由で fluentd を起動するようにしています。

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"]

ECS_CONTAINER_STOP_TIMEOUT はデフォルトの 30 秒とします。

この場合、次のように fluentd コンテナの環境変数に ECSWRAP_LINKED_CONTAINERS を指定することで、logger コンテナが終了するまで fluentd コンテナの fluentd プロセスに SIGTERM が送られないようにしていました。

{
  "name": "fluentd",
  "image": "$fluentd_repo_uri",
  "essential": true,
  "environment": [{"name": "ECSWRAP_LINKED_CONTAINERS", "value": "logger"}]
}

cf. example/run_example_ecs_task.sh#L30-L51

ecswrap には ECSWRAP_STOP_WAIT_TIMEOUT という環境変数があり、ECSWRAP_LINKED_CONTAINERS で指定したコンテナがこの時間内に終了しなかった場合に子プロセスに SIGTERM を送ります。デフォルトは 10 秒です。これは、上記の例だと fluentd コンテナが logger コンテナの終了を待ち続けて ECS_CONTAINER_STOP_TIMEOUT 経過後に SIGKILL が送られることを防ぐためです。そうしないと、fluentd のバッファに溜まっていたデータが消失してしまいます。

logger が Sidekiq アプリケーションの場合、ECSWRAP_STOP_WAIT_TIMEOUT は Sidekiq のタイムアウトより大きく、ECS_CONTAINER_STOP_TIMEOUT より小さくしなければなりません。

Enhanced Container Dependency Management を使う例

前述の例に相当する書き方は次のようになります。Container Dependency パラメータと Container Timeouts パラメータを指定します。

aws ecs register-task-definition --cli-input-json "$(cat <<JSON
{
  "family": "$TASK_FAMILY",
  "containerDefinitions": [
    {
      "name": "logger",
      "image": "$logger_repo_uri",
      "essential": true,
      "links": ["fluentd:fluentd"],
      "dependsOn": [
        { "containerName": "fluentd", "condition": "START" }
      ],
      "stopTimeout": 10
    },
    {
      "name": "fluentd",
      "image": "$fluentd_repo_uri",
      "essential": true
    }
  ],
  "cpu": "128",
  "memory": "256"
}
JSON
)"

ecswrap を使った例では fluentd コンテナの方に環境変数を指定していましたが、enhanced container dependency management では逆になるのが注意点です。dependsOn で自身の依存しているコンテナを指定すると、タスク開始時には先にそちらのコンテナが起動し、終了時には先に自身が停止するようになります。

stopTimeoutECS_CONTAINER_STOP_TIMEOUT 相当の値をコンテナごとに指定するためのパラメータです。上記の例だと、10 秒経っても logger コンテナが終了しない場合、logger コンテナには SIGKILL が送られます。stopTimeout のデフォルト値は ECS_CONTAINER_STOP_TIMEOUT です。
cf. agent/engine/docker_task_engine.go#L1099-L1102

stopTimeoutstopContainer が実行されてからのタイムアウトなので、ecswrap のようにメインコンテナのタイムアウト時間と ECS_CONTAINER_STOP_TIMEOUT の両方を考慮した上でタイムアウト時間を決めないという煩わしさはありません。一方、ECS タスクを停止しようとしてから全てのコンテナが終了するまでのタイムアウト時間を指定することはできません。

ECS タスク停止時のフローを図にすると次のようなイメージです。


Show source

10 sec、30 sec はそれぞれ logger コンテナ、fluentd コンテナの stopTimeout です。

まとめ

  • dependsOn を指定すると、タスク起動時のコンテナ開始順序だけでなく終了時のコンテナ停止順序も考慮される
  • コンテナそれぞれに stopTimeout を指定できるようになった
  • stopTimeout は ECS タスクを終了しようとしてからの経過時間ではなく、コンテナを停止しようとしてからの経過時間

dependsOn には START 以外に HEALTHY 等も指定できるので、色々試してみると良いと思います!
cf. Container Dependency