その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

docker composeでワンショットタスクを実行する

TL;DR

docker compose up --abort-on-container-exit

[追記]

docker compose run --rm ${service_name} で良かった......
[追記ここまで]




例えばなんらかのテストを実行する時、テスト用にDB等のストレージコンポーネントを用意してそいつに対し読み書きすることでend to endのテストを模擬しているというようなことがあると思います。
かつてはテストケースごとにデータベースプロセスを上げそれに対してread/writeを実行、ということもよくしていましたが *1、最近ではテスト起動時にストレージコンテナを立ち上げてそれを読み書きしている事も多くなってきました。


さて、そのような時に「テスト起動するにあたってはコンポーネントAとコンポーネントBを事前にマシン上で立ち上げておいてください」みたいなのはいかにも面倒くさいので、こういう時に便利なツールとしてパッと思い浮ぶのはdocker composeでしょう。
docker compose自体については本記事では説明しませんが、本記事ではそういった「ストレージのような永続的なライフサイクルを持つコンテナ」と「テストランナーのような一時的なライフサイクルのコンテナ」を同時に起動させ、テストランナーの実行が終了したらすべて店仕舞いさせるというワンショットタスクをdocker composeで実行する方法について記します。

version: '3.9'

services:
  redis:
    image: redis:7.0.4
    container_name: redis
    ports:
      - "6379:6379"
    network_mode: host
  app-test:
    image: app-test-env:latest
    container_name: app-test
    command: npm test
    volumes:
      - ./:/app
    working_dir: /app
    network_mode: host
    depends_on:
      - redis

上記の docker-compose.yml では redis というRedisコンテナと、app-testというテストランナーコンテナを同時に実行するという定義をしています。


まずapp-test はテストの実行にあたってRedisを利用するので depends_onredis を指定することでredisコンテナの起動を先に行うようにします。
なお、これはあくまでredisコンテナの起動を先に行うというだけで「Redisが6379番ポートで実際にlistenしてくれるまで待つ」ということをしてくれるわけではありません。なのでドキュメントにも「実際に使えるようになるまで待つ必要がある場合は適切な方法でやるように」と書かれていますので注意しましょう *2: https://docs.docker.com/compose/startup-order/

ネットワークの設定は適宜書き換えてください。この例では簡単のためにredisコンテナの6379ポートをホストの6379ポートに公開し、redisとapp-testの両方のnetwork_modeをhostモードにすることでホストネットワークを介して6379ポートを通じて通信できるようにしてあります。


と、ひとまずこのようにしておくと、docker compose up を実行することでテストの実行は可能となります。
しかし、テストランナーの実行が終了してもredisコンテナの実行は継続するため明示的にSIGINT等を送ってあげないとこのdocker compose upは終了しなくなります。healthcheckなどを適切に設定するとこのへん上手くやれるはずですが、ワンショットのタスクにそこまでやるのも凝りすぎでは?


ということで、docker compose up --abort-on-container-exit というふうに --abort-on-container-exit を付与して実行すると、いずれかのコンテナがexitした時にそのexit codeを引き継いでdocker composeを終了してくれるようになります。

--abort-on-container-exit   Stops all containers if any container was stopped. Incompatible with -d

[追記]
冒頭にも書いたように、こうしておいて docker compose run --rm app-test と実行すると良いです。
[追記ここまで]

良かった良かった、これで簡単にワンショットのタスクをdocker composeで実行できましたね。
ちなみに、exitしたら全体をabortさせたいコンテナを PID=1 にすればexitした際にすべてを終わらせてくれるのではないか、と思って app-test に対し init: true を指定してみましたがこれは効きませんでした。

小ネタ

docker compose runの時はそのserviceのログしか出てこないので問題無いので大丈夫なんですが (つまり何も問題が無い)、docker compose upの場合はすべてのサービスのログが流れてきます。その際、今回のようなケースだとRedisのログが見れてもあまりうれしいことは無いので抑制したい。というわけで

...
  redis:
    image: redis:7.0.4
    logging:
      driver: none
...

このようにlogging driverを none に設定するのですがこれは期待通りに動きません。

詳しくはこのissueに書かれているのですが、docker compose upのログは実行中のコンテナに実際にattachしたものが表示されるのであって、logging driverの設定とは別とのことです。
つまり、ここでredisコンテナに対してnoneを設定すると、docker compose upのログには表示される一方、logging driverを使っているロガー、例えば docker logs ${container_id} 等には表示されなくなるという挙動をするようです。

なので、起動オプション --attach でログを見たいコンテナを指定すると、docker compose upのログにはそれだけが表示されるようになります。今回の例だと --attach app-test などとすると良いでしょう。