compose.yamlはマージができるし、YAMLのtagでその挙動をコントロールできる

このエントリは、はてなエンジニア - Qiita Advent Calendar 2024 - Qiitaの13日目の記事です。昨日は、 id:maiyama4 さんのSwiftUI の Text がなぜか省略されてしまう問題とそのレイアウトプロセスのデバッグ - maiyama4's blogでした。

みなさんCompose使っていますか? ここで言うComposeはdocker composeとかnerdctl composeとかで呼べるコンテナ複数立ち上げくんのことです。 Docker Composeはcompose specとして標準化されている仕様のリファレンス実装であり、他のランタイムからもComposeを使うことができます。なので今回はまとめてComposeと呼ぶこととします。

Composeファイルの複数指定

おなじみのcompose.yamlファイルですが、Compose実行時にファイルを指定することができます。しかも複数指定することができます。

さてこのときにcompose.yamlとcompose.another.yamlに同じキーの値が存在したときはどうなるでしょう*1。

# compose.yaml
services:
  app:
    image: nginx:latest


# compose.another.yaml
services:
  app:
    image: ubuntu:latest

このようにして呼び出してみましょう。 docker compose configコマンドを使うと、実際に使用されるcomposeファイルが分かるようです。やってみましょう。

$ docker compose -f compose.yaml -f compose.another.yaml config

name: ymse
services:
  app:
    image: ubuntu:latest
    networks: # 自動付加
      default: null
networks: # 自動付加
  default:
    name: ymse_default

nginxと指定していたimageがubuntuと上書きされていることがわかりますね。

試してみるとこのルールは他にもenvironmentなどでも適用されることがわかります。 やってみましょう。

# compose.yaml
services:
  app:
    image: nginx:latest
    environment:
      foo: '1'
      bar: '2'

# compose.another.yaml
services:
  app:
    image: ubuntu:latest
    environment:
      foo: override

こんな感じにyamlを書いて実行してみる。

$ docker compose -f compose.yaml -f compose.another.yaml config

name: ymse
services:
  app:
    environment:
      bar: "2"
      foo: override
    image: ubuntu:latest
    networks:
      default: null
networks:
  default:
    name: ymse_default

確かにcompose.another.yamlで指定したservices.app.environment.fooのみが上書きされていることがわかりますね。

ということがComposeの仕様であるcompose-specに記述されています。

github.com

無で上書きしたい

さてこのcompose-specのmergingのセクションですが眺めてみると、特定のキーの値をresetすることができるみたいです。確かに生活していれば特定環境のみで値をセットしたいこともある。

github.com

# https://github.com/compose-spec/compose-spec/blob/e9dc800c027a513254bbf15ee05893a8105a6afb/13-merge.md#reset-value より引用。
# file Aを追記するなど一部改変。

# file A
services:
app:
    image: myapp
    ports:
      - "8080:80"
    environment:
      FOO: BAR             

# file B
services:
  app:
    image: myapp
    ports: !reset []
    environment:
      FOO: !reset null

上書きしたい値の頭に!resetと付けると無で上書きできるようです。 実際にどんなcomposeファイルが生成されるのか見てみましょう。

$ docker compose -f compose.yaml -f compose.another.yaml config

name: ymse
services:
  app:
    image: myapp
    networks:
      default: null
networks:
  default:
    name: ymse_default

実際にportsとenvironmentがオミットされていることがわかりますね。

YAML Tagで実現するリセット

さておもむろに!resetを頭に付けるとマージしたときにうまくやってくれるよ。と紹介しましたが、この!resetの正体はなんなのでしょうか。

これはYAMLのtagと呼ばれるものです。 YAMLの仕様にもしっかりと明記されている文法で、!1つで始まるタグはアプリケーション固有のローカルタグと呼ばれています。

https://yaml.org/spec/1.2.2/#24-tags https://yaml.org/spec/1.2.2/#3212-tags

ローカルタグはパースするアプリケーション側で自由に扱うことができます。

例えばRubyのYAMLパーサであるlibrary yamlではローカルタグを付けることにより、パースしたあとにRubyのどのクラスで扱うかを指定できます。

docs.ruby-lang.org

developers.bookwalker.jp

docker/composeが参照している compose-spec/compose-go の中でも実際にYAMLのタグをパースしてresetならよしなにするコードが存在しています。

github.com

このコードのおかげで上書きができるのですね。

まとめ

compose-specを眺めていたら見つけた、あまり知られていなさそうな仕様について調べたらYAMLのタグに流れ着きました。 知らない仕様を知ることは面白いですね。

以上、はてなエンジニア - Qiita Advent Calendar 2024 - Qiita13日目担当の id:ymseでした。14日目の担当は id:utgwkk さんです

*1:蛇足だが、compose.override.yml という名前にすると実行時に明に指定しなくても使われる。compose.yamlと一緒ですね。