docker compose の dockerfile_inline の変数エスケープ

Pocket

Docker Compose v2.17.3 から、 dockerfile_inline 構文compose.yaml の中に Dockerfile をインライン記述できるようになった。
compose.yaml ファイル一つで複数のコンテナを立ち上げられるので、地味に便利。

さて、この機能における変数の扱いについての理解に関して…、
突然だが Docker クイズ!!

以下の compose.yaml をビルドしたら、 /out.txt には何が出力されているだろうか?

services:
  inline:
    build:
      context: .
      dockerfile_inline: |
        FROM alpine
        ARG arg3="dockerfile"
        ARG argDupl="dockerfile"
        RUN  export arg2=buildcontainer \
          && export argDupl=buildcontainer \
          && cat <<EOS > /out.txt
        --------
        arg2=$arg2
        arg3=$arg3
        arg2=\$arg2
        arg3=\$arg3
        --------
        EOS
        RUN cat /out.txt

docker compose --progress=plain build --no-cache オプションを付けてビルドし、 /out.txt のダンプがどうなるか見てみよう。
果たして、正解は…?

ドロドロドロドロドロドロ (ドラムロール音)

 

\デーン!!!/

#6 [inline 3/3] RUN cat /out.txt
#6 0.496 --------
#6 0.496 arg2=
#6 0.496 arg3=
#6 0.496 arg2=arg3=--------
#6 DONE 0.5s

正解できただろうか?

解説

解説というほど複雑な話ではないが、もし間違えたなら compose.yaml 自身にも環境変数の展開機能があることを見落としていたのかもしれない。
.env ファイルに変数を設定する、よく使うアレだ。

dockerfile_inline 構文内の Dockerfile で変数を使う場合、以下のように 3種類 の変数展開を考慮する必要がある。

  • compose.yaml
  • Dockerfile
  • ヒアドキュメントを実行するシェル

このうち、 RUN 構文内については Dockerfile レイヤーでの変数展開はされず、ビルドコンテナ内の環境変数経由で変数が渡される。
このため、 compose.yaml と ヒアドキュメント の変数展開だけ気をつければ良い。

やっかいなエスケープの問題

まず、以下の出力結果を見てみよう。

#6 0.403 arg2=arg3=--------

なぜこのような出力になってしまうのか。その理由は、

services:
  inline:
    build:
      context: .
      dockerfile_inline: |
        FROM alpine
        RUN  cat <<EOS > /out.txt
        arg2=\$arg2
        arg3=\$arg3
        --------
        EOS
        RUN cat /out.txt

この部分が compose.yaml の変数展開により

RUN  export arg2=buildcontainer \
  && export argDupl=buildcontainer \
  && cat <<EOS > /out.txt
arg2=\
arg3=\
--------
EOS
RUN cat /out.txt

こう展開され、 末尾の \ がヒアドキュメント内の改行のエスケープと解釈されてしまったためだ。

正しいエスケープ方法

この問題を解決するには、変数展開のレイヤーごとに適切なエスケープ方法を使う必要がある。
具体的には:

  • compose.yaml 内の変数展開は $$ でエスケープ
  • Dockerfile の変数は \$ でエスケープ(今回の例では関係しない)
  • ヒアドキュメントのシェルは \$ でエスケープ

以下の例で確認してみよう。

services:
  inline:
    build:
      context: .
      dockerfile_inline: |
        FROM alpine
        ARG arg3="dockerfile"
        ARG argDupl="dockerfile"
        RUN  export arg2=buildcontainer \
          && export argDupl=buildcontainer \
          && cat <<EOS > /out.txt
        ========
        arg1=$arg1
        arg2=$$arg2
        arg3=$$arg3
        argDupl=$$argDupl
        escaped=\$$ESCAPED
        ========
        EOS
        RUN cat /out.txt

bash での実行

$ arg1=hostenv docker compose --progress=plain build --no-cache

PowerShell での実行

PS> $env:arg1='hostenv'; docker compose --progress=plain build --no-cache

出力:

...
#6 [inline 3/3] RUN cat /out.txt
#6 0.407 ========
#6 0.407 arg1=hostenv
#6 0.407 arg2=buildcontainer
#6 0.407 arg3=dockerfile
#6 0.407 argDupl=buildcontainer
#6 0.407 escaped=$ESCAPED
#6 0.407 ========
#6 DONE 0.4s
...

上記のように、

  • $arg で、ホストマシンや .env ファイルの環境変数
  • $$arg で、 Dockerfile の変数 および ビルドコンテナのシェル変数
  • \$$arg で、どちらもエスケープされた "$arg" という文字列

がそれぞれ展開されることがわかる。

$argDupl を見ればわかるように、 Dockerfile の変数よりも、ビルドコンテナ内で定義された変数のほうが優先される。

ヒアドキュメントの変数展開は <<'EOS' などとすることで変数やコマンドの置換は行われなくなるが、compose.yaml 内の変数展開は当然回避されない。
このため、

services:
  inline:
    build:
      context: .
      dockerfile_inline: |
        RUN  cat <<'EOS' > /out.txt
        foobar $$hoge
        EOS

$$ でのエスケープは回避できないとにも注意だ。

まとめ

Docker Compose の dockerfile_inline の変数展開について重要な点をまとめると:

  • 変数展開は3つのレイヤーで行われる
    • compose.yaml の変数展開
    • Dockerfile の変数展開
    • シェルの変数展開(ヒアドキュメント)
  • 変数の記述方法と展開結果
    • $var - ホストマシンや .env ファイルの環境変数が展開される
    • $$var - Dockerfile の変数やビルドコンテナのシェル変数が展開される
    • \$$var - 文字列 "$var" として扱われる
  • ビルドコンテナで設定された環境変数は Dockerfile での設定より優先度が高い

これらの挙動を理解しておけば、 dockerfile_inline をもっと使いこなせるはずだ。

コメントを残す

メールアドレスが公開されることはありません。

This site uses Akismet to reduce spam. Learn how your comment data is processed.