ecsへのアプリケーションデプロイはecs-cliが最も優れていると思いつつも、結構わかりにくいツールであるのは間違いないです。その解説をするのですが、この投稿ははっきり言って上級者向けで

  • docker-compose かなり使いこなせる
  • AWS ECS の基礎がわかっている

ことを前提にしています。上記を説明しだすとキリがないので、今は「ちょっと、何言ってるかわからないですね〜」な人も、ecsを使うならば近い将来役立つと思うので、あとで読むなどしておいてもらえればと思います。

ecs-cliのコンセプト

まずは ecs-cliがどんなものかを、これもある程度上級者向けに雰囲気を掴んでもらうように伝えるとこんなかんじ

  • docker-compose でローカル実行するよね普通
  • docker-compose.yml の内容って ecsのタスク定義 と似てるよね
  • じゃあ、docker-compose.ymlをヒントに ecsのタスク定義 を作ってついでにタスク実行・サービス定義までできるようにしたろ

できるけど、やらせないこと

ecs-cliはかなりの機能を持っています。ですが、大事なことなので最初に書きますが 全部の機能を使う必要はありません ほしいところだけつまみ食いできます。具体的にこの場では触れないこととして
ecs clusterそのものの構築 (コンテナインスタンスの起動とか)
があります。すでに定義済みの ecs クラスタを使うことは可能です。が、むしろクラスタの作成から ecs-cliを使うことは稀です。AWS公式ドキュメントのチュートリアルはだいたいクラスタ作成から入っていますが、本運用でクラスタから作り直すはそんなに無いし、というかecs-cliでそれ(デプロイの度にクラスタから新設する)をやるのは結構厄介なので、無視します。

やること・やらせたいこと

  • docker-build と ECRへのpush
  • ecs タスク定義の作成 & 更新
  • ecs タスクの実行
  • ecs サービスの定義
  • ecs サービスの更新

冒頭で機能はつまみ食いできると書いたものの、結局ほとんどecs-cliにやらせます。が、繰り返しますが、もちろん一部だけ使うもできますので、パーツごとに読んでもらって良いです。

docker-build と ECRへのpush

正確にはこのタスクにおけるecs-cliの役割はECRへのpushのみです、が ecs-cliが利用する docker-comose.yml に buildとタグ付けまでを任せることができます。これが後々の ecsのタスク定義 まで連結できます、ここが美味しい。
まず、実際のアプリ・システムで docker build が発生しないことはほぼ無いはずです。docker build は必須でかつ、そこでECRにpushした docker imageはすぐにタスク定義で参照することになります。
具体的にはdocker-compose.ymlはこのように書きます

version: "3"

services:
  web:
    build: .
    image: [aws-account-id].dkr.ecr.[ecr-region].amazonaws.com/web:1.0
    ports:
      - 80

Dockerファイルはあくまで参考に、皆さんの必要なアプリに合わせてください、docker-compose.ymlと同じ場所に配置します。

FROM nginx

RUN echo 'hello world this version is 1.0' > /usr/share/nginx/html/index.html

これで docker-compose build をすると [aws-account-id].dkr.ecr.[ecr-region].amazonaws.com/web:1.0 というタグがついたイメージが ローカルにできますecs-cliではなく docker-compose の機能です。これにより buildした imageにタグを付けて、かつ後々タスク定義にそのタグを使い回せます
引き続き ecs-cli でpushします

ecs-cli push --region <your ecr region>  --cluster-config <cluster config name> [aws-account-id].dkr.ecr.[ecr-region].amazonaws.com/web:1.0

docker コマンドでpushした場合、ECRへのpushの場合は docker login が必要ですが、これが不要になります。さらにECRのリポジトリ自体も先に作っておく必要がありません、pushしたときに無かったら勝手に作ってくれます。これだけでも便利。

お前は ecs-cli push の忖度を無視しているぞ! いえ、わざとです。

version: "3"

services:
  web:
    build: .
    image: web:1.0

として、docker-compose build ののち

ecs-cli push --region <your ecr region>  --cluster-config <cluster config name> web:1.0

とした場合、ecs-cliが忖度して、ECRのリポジトリ名にあったtagでpushしてくれます。なので、わざわざ docker-compose.ymlの image: で完全なtagをつけなくていいんですが、こうしておかないと後述のタスク定義を作成するときに、この世におそらく存在しない web:1.0 というECRではない、dockerHubのイメージを指すことになります。
このあたりは -f オプションで docker-compose.ymlを複数指定&マージや、環境変数を compose自体に吸わせたりと、いろいろ回避する方法はあるのですが、リポジトリにECRを使う場合は、ECRのURLをあえて抽象化しないほうが、事故のリスクも減らせるのでいいかなと思っています。

ecs タスク定義の作成 & 更新

次にecsのタスク定義を作成します。このタスク定義は2つのファイルで内容を指定します。

  • docker-compose.yml
  • ecs-params.yml

docker-compose単体では、ecsのタスク定義のすべてを表現しきれません。なので大部分はdocker-compose.ymlから、ecs specificな部分は ecs-params.ymlで指定し、2つがくっついてタスク定義になります。
各々のファイルの詳細は公式ドキュメントを読んでください。注意点は docker-compose のリファレンスにある機能がすべて ecs-cli でも指定できるわけではない点です。

ファイルの準備ができたら、ecs-cliでタスクを定義しましょう

ecs-cli compose create --cluster-config <hoge>

compose create は 作成も更新も行います。そもそもタスク定義は更新不可能ですので、新しいバージョンが作成されます。気をつけてほしいのは、このコマンドは タスク定義の作成(新バージョンも) のみですので、これを叩いただけではデプロイされません

ecs タスクの実行

次にECS用語のタスク実行です。ECS用語のサービスとは違い、一発もので死んでもrespawnしません。

ecs-cli compose up --cluster-config <hoge>

バッチジョブ等一発ものならばこれでOKです。もしもデーモンプロセスで、サービス化するまえにタスクで試したいだけ、確認が終わり止めたくなったら。

ecs-cli compose down --cluster-config <hoge>

docker-composeのコマンドと一緒です。ECSのタスクは、一発走ると死んでしまうので startとかはできません。

ちなみにFargateを使う場合は、SGやSubnetは ecs-params.ymlで指定します。ここで参照するSG等は、先に作っておく必要があります。ecs-cli は 1タスク定義 = 1 docker-compose.yml という構成になるので、(ある程度)マイクロサービス化しているならば何個もdocker-compose.ymlを作成するはずで、ここで利用するSGが互いに依存するような(e.g. sg-a -> sg-b の 80は accept)場合は、依存関係を解消できずに作成できなくなります。なので、ecs-cli でこれらAWSリソースを作成できるようにするのはちょっと無理筋です。

ecs サービスの定義

サービスの設定は少々癖があります。

  1. どのALBにぶら下げるとかは、コマンドラインオプションでしか指定できない
  2. 新規作成更新のコマンドが違う

サービス実行時のオプション等

ecs-cliはタスク定義が中心ですので、タスクで動かすのか、サービスで動かすかは抽象化、というかどっちでもいいようにできています。サービスとタスクの大きな違いは、とくにWebなら

  • タスクはALBにぶら下げられない
  • サービスはALBにぶら下げられる

ので、どのALBにぶら下げるかは、サービス定義時にコマンドオプションで指定します。ecs-params.ymlで指定で、タスクとして実行したときは無視してくれれば良さそうなもんですが(ここは変わるかもしれない)。また、ここで指定するALBやTGも先に作っておく必要があります。サービス定義時に指定するコマンド具体的にはこんな感じ

ecs-cli compose service create \
--deployment-max-percent 200 --deployment-min-healthy-percent 50 \
--target-group-arn <target-group's ARN> \
--container-name web --container-port 80 --cluster-config <hoge>

ecs-cliには ALBもTGも作る事はできないので、ALB/TG/Lister-rules/SG(バックのEC2と兼ね合い) は予め作っておく必要があります。このあたりも含めてどうしても一括で管理したいインフラ寄りの貴方には、ここまで書いといてどんでん返しですが、terraformをおすすめします。

新規作成と更新

実際は compose service ... の解説を熟読してほしいのですが、私の運用ルールを書きます。

  1. compose service create で初期定義&タスク数0
  2. compose service scale でタスク数調整
  3. compose service up で新バージョンデプロイ

最初の create と scale は特に違和感ないと思いますが、新バージョンのデプロイのみ up を使うとしています。初期定義時にいきなり up としてもタスク数1で立ち上がるのですが、この場合タスク定義だけ更新し、事前チェックしたいだけでもいきなりデプロイしてしまうためです(文法エラー時はデプロイしません)。そのうち dry-runもできるようになりそうですが、create だけならデプロイ無し、 up ならデプロイありと意識付けしたほうが良いでしょう。なお、タスク定義がただしいかどうかをチェックしたいだけなら、 compose create でOKです。サービスは新しいタスク定義バージョンができたからと言ってそれにつられて勝手にデプロイはされません。

ecs-cliで前バージョンへ切り戻し

ecs-cliには、タスク定義のバージョンを指定してタスク・サービスを実行することができません。が、一応ecs-cliの特性を使えば前のバージョンへ切り戻すことは可能です、

ecs-cli のタスク定義は、良くも悪くも docker-compose.yml および ecs-params.yml の主張をきちんと受け入れます。例えば

  • タスク定義 hoge:10 で指定していた imageのタグが ver-10
  • hoge:11 新たに imageタグ ver-11 を push しタスク定義でも ver-11 を参照した
  • その後 ver-11(hoge:11) にバグがあったことに気づき、ver-10(hoge:10) に戻したいとなった
  • docker-compose.yml で指定している image: のtag を ver-10 にもどした(ほかはいじっていない)
  • この状態で ecs-cli compose service up とすると、 hoge:10 で再デプロイされる なんと hoge:12が作成されずに hoge:10が再利用される

この機能は賛否両論あると思います。素直にECSのapiを使えば、サービスの hoge:11 への参照を hoge:10に変えればいいだけだからです。ですが、よく考えてみたら hoge:10 が tag:ver-10 だというのはきれいに連番で運用していたらそうなるだけで、タスク定義のバージョンだけの情報では、そこで使っているdocker-imageのtagはなんなのかは正確にわかりません。
今現時点で動いているタスク定義のバージョンは常に最新という鉄の掟を守れればよいですが、切り戻したあとに忘れず最新版=つまり問題があったバージョンを削除を徹底するなど、機械=ECSというシステムに人間を歩み寄らせる必要があるということです。

これに対してecs-cliはお前の書いた docker-compose.yml (& ecs-params.yml) の内容、全部尊重したる。タスク定義の番号は使い回すか、新設するかは俺に任せろ、お前の言う通りにやったるわと言わんばかりに人間の主張をecsに合わせこませます。

私の考えとしては、タスク定義のバージョン番号だけ見ても、それがいつのdocker-imageを指しているかわからない。だったらいっその事タスク定義の番号のことは人間は意識せず(ecsの都合なんか知らない)にecs-cliに忖度させればいいと思います。
ただし、ecs-cliもdocker本体の進化、それに付随してecs自体の進化をどんどん吸収して新バージョンが頻繁にでます。よってそのecs-cliのバージョンアップには要注意で、都度タスク定義のバージョンの動きは確認したほうが良いでしょう。

latestタグの扱いをどうするか

latestというタグは特殊で、docker-imageのタグを省略した場合は latest が指定されているとみなします。この latestに依存すれば、タスク定義の書き換えは常に不要で、最新のdocker-imageを指し続けることができます。よって、通常オペレーションでは、 docker push ののち compose service up--force-deployment と指定すれば良いです。が、こうしてしまうと切り戻しが煩雑です。旧バージョンの docker-imageにlatestタグを付け替えて force-deployment とすればよいのですが、latestタグしかつけていない場合は、明確な印がないので、pushした日付から「多分この時期Pushしたバージョンはこうだったはず」と推測するしかありません。
これも賛否両論だと思いますが、latestタグによる運用は、切り戻しのことを考えるとあまりおすすめしません。latestを使うにしても、latest以外のユニークなタグをすべてのイメージに合わせてつけておくべきです。

ローカル実行との兼ね合い

docker-compose は -f オプションを書くことで、composeファイルをマージした状態で実行できます。環境変数なども使えるのですが、私は -f をつかって、ECS向けとローカル実行向けを切り替えるのが良いと思います。docker-compose.yml は ECS向け、 docker-compose-local.yml はローカル実行向けとした場合

docker-compose -f docker-compose.yml -f docker-compose-local.yml up 等
version: "3"

services:
  nginx:
    image: nginx
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "/ecs/test"
        awslogs-stream-prefix: "nginx"
version: "3"

services:
  nginx:
    logging:
      driver: json-file

これで走らせると、ecs向けでは logdriver は awslogs(cloiudwatch logs) でローカル実行ならば普通のjson-file (docker-compose logs ngixで読める) となります。
これをうまく使うと、ecs向けは DBは RDSをつかう、ローカル実行は、おなじ docker network に mysqlのコンテナを立ち上げる、ということもできます。もちろん ymlファイルを複数指定せずに、大本から2つに割ることもできますが、割ってしまうと 1枚の docker-compose.yml で 2度美味しい ecs-cliの利点が薄まります。
これ以外にも docker-compose の機能として、違うyamlを吸わせたり、環境変数を docker-compose.yml自体に吸わせたりする方法がたくさんあるので、自分にあったものを探して選んでください。

その他の注意点

タスク定義で指定している docker-imageが実在するかの裏取りはしてくれない

docker push忘れを予防するには、私達でなにか確認プロセスの準備が必要です。

ecs-cli logs の対象は cloudwatch logs (awslogs) のみ

Fargate以外なら ecs-agentからローカルログ(json-file)が取得できてもよさそうですがダメなようです。私としては、docker-compose logs をガンガン使うひとで且つecs-cli使うなら 「awslogsにはとりあえず投げとけ」を推奨します。

元記事はこちら

運用ベースのecs-cli利用手引き