docker composeでワンショットタスクを実行する
TL;DR
docker compose up --abort-on-container-exit
[追記]
docker compose run だとredis serviceがstopしないからダメ的な話?
— r7kamura (@r7kamura) 2022年9月19日
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_on
で redis
を指定することで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
などとすると良いでしょう。
*1:e.g. Test::Mysqld
*2:かつて似たようなことをブログに書いていたことを思い出しました: 相手のサーバにHTTP(S)で接続できるかどうかを確認するときにリトライしながらやりたいんですけどって時 - その手の平は尻もつかめるさ