複数のdocker-compose.ymlをマージして1つのYAMLにする方法です。
複数のアプリケーションが協調して動くようなシステム*1を開発していて、各レポジトリにdocker-compose.ymlが存在している状況を想定します。
ローカルで複数アプリを協調して稼働させるにはアプリケーションAのrepositoryに移動してdocker-compose up
し、次はBに移動して…というのをやっていたのですが、どうせdocker-compose
するのならレポジトリ構成に依存せず必要なアプリケーション全てに対してまとめて操作したいのでYAMLを統合してまとめて管理できるようにしました。
操作とはup
, down
, build
を始めとした諸々です。
docker-composeの関連知識
以下の知識を利用します。
docker-compose
には環境変数COMPOSE_FILE
で複数のファイルを渡すことができ、YAMLの内容はマージされるdocker-compose -f <filename>
でもできる
docker-compose config
コマンドでcompose fileを検証、評価できる- マージ結果を確認できる
COMPOSE_FILE=./auth_api.yml:./payment_api.yml:./payment_frontend.yml \ docker-compose config > ./docker-compose.yml
複数ファイル指定時のマージは一番最初に指定したファイルに対して後続のファイルの設定をマージしていく挙動になります。
- キーが無い場合:追加
- キーがある場合:後で指定したファイルの内容で上書き
- リストの場合:リストの要素を追加
主に、基底となるファイルを環境ごとに拡張して使いまわしたいときに使える (docker-compose.local.yml
でローカル用の設定を足すなど)
具体的なやり方
以下のようにrepository群が並んでいるとします。
# ディレクトリ構成 . └── repos ├── auth_api │ └── docker-compose.yml ├── payment_api │ └── docker-compose.yml └── payment_frontend └── docker-compose.yml
これらを愚直にCOMPOSE_FILE=./repos/auth_api/docker-compose.yml:...
と繋げていってもconfigコマンドは成功した風に完了します。
しかし、YAMLファイル内に記述した相対パスが各repositoryのルート基準ではなくCOMPOSE_FILE
envに渡した最初のファイルのパス基準で評価されてしまいます。
具体例として、repos/payment_api/docker-compose.yml
内に以下のような相対パスを書いていたとします。
services: payment_api: build: .
ここでCOMPOSE_FILE=./repos/auth_api/docker-compose.yml:./repos/payment_api/docker-compose.yml
というコマンドを実行すると、この相対パスは意図しないディレクトリとして評価されてしまいます。
services: payment_api: build: context: /absolute/path/to/repos/auth_api
これを避けるためには最終形態の特大YAMLを生成する前に各repositoryのYAMLを個別に評価し、YAML内の相対パスを絶対パス表現に変換する必要があります。パスを予め静的に決定することはできないので動的に生成します。
#!/usr/bin/env ruby require 'yaml' require 'pathname' require 'fileutils' root_dir = Pathname.new(__dir__) repos = %w[ auth_api payment_api payment_frontend ].map {|r| root_dir.join('repos', r) } FileUtils.mkdir_p "tmp/config" tmp_config_filenames = repos.each_with_object([]) do |repo, memo| compose_path = repo.join('docker-compose.yml') if compose_path.exist? memo << root_dir.join("tmp/config/#{repo.basename}.yml").tap do |tmp_config_filename| system "COMPOSE_FILE=#{compose_path} docker-compose config > #{tmp_config_filename}" end end end system "COMPOSE_FILE=#{tmp_config_filenames.join(':')} docker-compose config > ./docker-compose.yml"
見た通り、各repositoryのYAMLをdocker-compose config
で評価したものを一時ディレクトリに吐き出します。ここで生成されたYAMLたちの中ではパスはすべて絶対パスに変換されています。
この一時ディレクトリのファイルをCOMPOSE_FILE
に与え、再度docker-compose config
を実行するとパス問題が解決された大統一YAMLが生成されます。
場合によってはnetworksの調整やservice nameやport衝突やらが発生したりすると思いますが、各repositoryのYAMLを修正したり大統一YAMLをちょっと修正するようにスクリプトを直せばなんとかなります。なる。
これで勝利です。レポジトリが別れていてもすべてのserviceをまとめてdocker-compose
で操作することができます。
補遺
docker-compose
実行時のファイルを指定したエイリアスを登録しておくと各レポジトリ内で操作するときも大統一YAMLを参照できて便利です。
alias xc='docker-compose -f /Users/ohbarye/path/to/unified/docker-compose.yml'
また、対象のレポジトリ群を管理するレポジトリを作っておくと上述のようなスクリプトを置いたり、全レポジトリをまたぐ横断的な"なにか"をするときに非常に便利です*2。
最近の事例だと、共通のGitHub Actionsをまとめて突っ込んだり、master branchをまとめてmain branchに変更するときに役立ちました。
ohbarye Advent Calendar 2020の2日目の記事でした。