2019-04-14

ecs-deployを使ったAmazon ECSへのデプロイの裏側

ecs-deploy というデプロイツールがあり、これを使うと簡単に Amazon ECS へデプロイができる。
https://github.com/silinternational/ecs-deploy

Dockerイメージを更新するだけであれば以下のように、コマンド一発でデプロイをいい感じにやってくれる。

ecs-deploy --cluster my-cluster --service-name my-service --image registry.gitlab.com/mogulla3/ecs-sample-app:xxe93fce82a4e787dc81427a4a469debaa07dbb4

ecs-deploy は内部的には AWS公式のCLI を使っているので、CLIを駆使してデプロイを実行していると想像できるが、具体的に何をやっているのかをこの記事では追ってみたい。

なおここでは ecs-deploy まわりについてしか触れていないが、一緒に検証をした mosuke5 先生も同じ作業内容で別視点の記事を書いているのでお供にどうぞ。
https://blog.mosuke.tech/entry/2019/04/13/aws-ecs-deploy/

前提

  • AWS ECS を Fargate 起動タイプを使って実行しているアプリケーションが対象
  • デプロイとして具体的にやりたいことはDockerイメージの更新のみ
  • デプロイタイプとして rolling update を選択
  • ecs-deploy は 2019/04/13時点のmasterブランチのバージョンを使用 (ハッシュ値 : 2470057351d205b8a59b1067cc23cb2243a7fae8

全体的な流れ

最初にコマンド実行時の全体的な流れをざっと確認した。大きく分けて、次の5ステップになっている。

  1. 現在のECSタスク定義を取得する
  2. 現在のECSタスク定義をベースに、新しいタスク定義を作成する
  3. 新タスク定義をAWS上に登録する
  4. 新タスク定義を使うよう、ECSを更新をする
  5. デプロイの最終確認をする

以下、それぞれのステップを細かく追っていく。

現在のECSタスク定義を取得する

現在のタスク定義を取得するために、最初に aws ecs describe-services を実行してタスク定義のARNを取得する

aws ecs describe-services --service <SERVICE_NAME> --cluster <CLUSTER_NAME> | jq -r .services[0].taskDefinition

ARNとはAmazonリソースネームの略で、AWS上のリソースを一意に識別するためのもの。詳細は こちら を参照されたし。
具体的には arn:aws:ecs:us-east-1:... のような単なる文字列だ。

タスク定義のARNを取得したら、実際の値、つまりJSONフォーマットで書かれた定義情報を aws ecs describe-task-definition を使って取得する。

aws ecs describe-task-definition --task-def <取得したタスク定義のARN>

現在のECSタスク定義をベースに、新しいタスク定義を作成する

次に、これからデプロイする新しいタスク定義を作成する。これは、現在のタスク定義をベースに一部を書き換えたり、不要な要素を削る形で作られる。

最初に、タスク定義の中に含まれるイメージ情報を sed を使って書き換える。変更後の値は ecs-deploy コマンド実行時に --image オプションに渡した値となる。

{
    "taskDefinition": {
        "containerDefinitions": [
            {
                // ここが書き換わる
                "image": "registry.gitlab.com/mogulla3/ecs-sample-app:xxe93fce82a4e787dc81427a4a469debaa07dbb4",
                ...
            },
            ...
        ],
        ...
    }
}

imageの部分を書き換えた後、必要となるデータだけを抽出する。 ここは起動タイプとしてFargateを選択しているかどうか等によって抽出する値が変わってくるが、自分の設定の場合は以下のようなタスク定義が作られていた。

{
    "family": "task-definition-name",
    "volumes": [],
    "containerDefinitions": [
        {
            "name": "custom",
            "image": "registry.gitlab.com/mogulla3/ecs-sample-app:xxe93fce82a4e787dc81427a4a469debaa07dbb4",
            "repositoryCredentials": {
                "credentialsParameter": "arn:aws:secretsmanager:.."
            },
            "cpu": 256,
            "links": [],
            "portMappings": [
                {
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "entryPoint": [],
            "command": [],
            "environment": [],
            "mountPoints": [],
            "volumesFrom": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/task-definition-name",
                    "awslogs-region": "us-east-1",
                    "awslogs-stream-prefix": "ecs"
                }
            }
        }
    ],
    "placementConstraints": [],
    "networkMode": "awsvpc",
    "executionRoleArn": "arn:aws:iam::...",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512"
}

新しく作成したタスク定義をAWS上に登録する

ここまでで新しいタスク定義が完成した。しかしこれをいきなり使うのではなく、最初にAWS上に登録する必要がある(らしい)。
よって、次のように aws ecs register-task-definition を使って登録する。JSONデータの新タスク定義をそのまま引数として渡して登録できる。

aws ecs register-task-definition --cli-output-json "新しく作成したタスク定義のJSON"

このコマンドの返り値として、新しいタスク定義のJSONデータが返ってくるので、ここからARNだけを変数に保持して次に進む。このステップはこれだけだ。

新タスク定義を使うよう、ECSを更新をする

ここまでで下準備が整った。いよいよイメージの更新を実行していく。これは aws ecs update-service を使って行われる。

aws ecs update-service --cluster my-cluster --service my-service --task-definition <新タスク定義のARN>

さらにここで終わらず、続けて aws ecs describe-services を使い desiredCount というパラメータを取得している。

aws ecs describe-services --cluster my-cluster --service my-service | jq '.services[]|.desiredCount

desiredCount とはタスク定義を反映したインスタンスの実行数のこと。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service_definition_parameters.html

ecs-deployは気が利いていて、新しいタスク定義で作られた実行中インスタンスの数がこの値になるまでsleepをはさみつつ確認をしてくれる(ただしタイムアウト時間はある)。

このとき、aws ecs list-tasksaws ecs describe-tasks コマンドが使われている。

aws ecs list-tasks --cluster my-cluster --service-name my-service --desired-status RUNNING
aws ecs describe-tasks --cluster my-cluster --tasks RUNNING_TASKS

デプロイの最終確認をする

最後にデプロイの完了を確認を行う。具体的には以下コマンドで取得する deployments の数が1つになることを確認している。

aws ecs describe-services --services my-service--cluster my-cluster | jq "[.services[].deployments[]] | length"

ECSはデプロイした後、旧タスクと新タスクがしばらく混在した状態になる。実際にWebサーバのコンテナをデプロイしているときに、ブラウザからアクセスを繰り返すと旧タスクと新タスクのレスポンスがランダムで返ってくることを確認している。

おそらくロードバランサーなどの設定も絡んでくるところだが、要するに新旧タスクが混在した状態から新タスクだけが稼働している状態になるまでの確認をしてくれているわけだ。

自分が最初に試したときは90秒の待機時間を超えても2つの deployment が残った状態で確認処理がタイムアウトしてしまっていた。これはECSの前段に作られたロードバランサーが旧タスクの登録解除を行う際に300秒近く待機することが起因していたようで、ロードバランサーの設定を変えることで成功するようになった。

参考 : https://qiita.com/naomichi-y/items/d933867127f27524686a

まとめ

ecs-deploy を使ったECSアプリケーションのデプロイの裏側を追った。
コマンド一発でいい感じにやってくるその裏側では AWS CLI、jq、sed などのコマンドを使って結構色々なことをやってくれていた。

この記事では書ききれなかった処理やコマンドラインオプションも色々あったので、詳細が気になった方は読んでみるといいかもしれない。ShellScriptだけで書かれていて、かつ700行くらいしか無いので個人的には読みやすかった。