👾

docker buildのsecretオプションでハマった話

2024/11/21に公開

はじめに

こんにちは。都内でソフトウェアエンジニアをしているtomoriです。

最近 docker buildsecret オプションでハマったことがあったので、備忘録として記事にします。

(どちらかというと secret オプションというよりは docker build そのものでハマっていたような気もしますが……)

ちなみに secret オプションのことは、「要件的にこういうの欲しいなぁー」と思って探していたらたまたま見つけました。

先に結論

諸々の経緯を端折って先に結論を書きます。

Build secrets を使用してイメージをビルドする際には、以下の3点に注意してください。

  1. BuildKit を使用する

    • Docker Desktop, Docker Engine v23.0 以降では、BuildKit がデフォルトのビルダーとして有効になっています。特別な設定は不要です。
    • もし v23.0 より古いエンジンを使用している場合は、以下のように環境変数を設定して BuildKit を明示的に有効化してください。
      $ DOCKER_BUILDKIT=1 docker build .
      
  2. Buildx を最新バージョンにアップデートする

    • $ docker buildx version で現在のバージョンを確認し、v0.6.0 以上であることを確認してください。
    • v0.6.0 より古い場合は、最新バージョンにアップデートしてください。
  3. syntax ディレクティブを使用し、最新の BuildKit フロントエンドを利用する

    • Dockerfile の先頭に以下の1行を追加することで BuildKit の最新フロントエンドを使用して Dockerfile を解析できます。
      # syntax=docker/dockerfile:1
      

secretオプションとは

Docker には Build secrets という概念があります。

Build secrets とは、コンテナイメージをビルドするプロセスの一部で使用される、パスワードやAPIキーなどの機密情報のことを指します。

A build secret is any piece of sensitive information, such as a password or API token, consumed as part of your application's build process.

https://docs.docker.com/build/building/secrets/

従来、ビルド時の情報を引き渡す方法としては、ビルド引数(ARG環境変数(ENV が一般的でした。

しかし、これらの方法では、機密情報が最終的なイメージに含まれたり、ビルドキャッシュやビルドログに残ってしまうリスクがあります。

そのため、ビルドプロセスに対して機密情報を引き渡す方法としては適していません。

Build arguments and environment variables are inappropriate for passing secrets to your build, because they persist in the final image. Instead, you should use secret mounts or SSH mounts, which expose secrets to your builds securely.

この課題を解決するために登場したのが Build secrets です。

docker build コマンドで --secret オプションを指定することで Build secrets を使用することができ、ビルドプロセス中に限り、機密情報を一時的かつ安全に引き渡すことが可能になります。

ハマったこと

イメージのビルド環境として AWS CodeBuild を使用しており、以下のように secret オプションを使って、ビルドプロセス内の環境変数にシークレットをマウントする設定を行っていました。

CodeBuild の環境には aws/codebuild/amazonlinux2-x86_64-standard:5.0 を指定しています。

※ 以下は簡略化した例です。

Dockerfile
RUN --mount=type=secret,id=xxx,env=XXX \
    --mount=type=secret,id=xxx,env=XXX \
    aws s3 cp ...
buildspec.yml
...
phases:
  build:
    commands:
      - |
        docker build \
          --secret id=xxx,env=XXX \
          --secret id=xxx,env=XXX \
          -t "${IMAGE_NAME}:${IMAGE_TAG}" \
          -f "${DOCKERFILE_PATH}" \
          "${BUILD_CONTEXT}"
...

ローカル環境では同様の Dockerfile とコマンドで正常にビルドできることを確認していたのですが、 CodeBuild 環境下では以下のエラーが発生しました。

Dockerfile:25
--------------------
  24 |     
  25 | >>> RUN --mount=type=secret,id=xxx,env=XXX \
  26 | >>>     --mount=type=secret,id=xxx,env=XXX \
  27 | >>>     aws s3 cp ...
  28 |     
--------------------
ERROR: failed to solve: unexpected key 'env' in 'env=XXX'

試したこと1

「あれ?環境変数へのマウントってもしかすると最近追加された機能なのかな?」と思い、 docker build のリリースノートを確認したところ、 secret オプションの env サポートは v0.6.0 (2021年のリリース)で追加されていることが確認できました。

  • Allow secrets from environment variables. docker/buildx#488

https://docs.docker.com/build/release-notes/#enhancements-7

念のため buildspec.yml に以下のコマンドを追加して Buildx のバージョンを確認しました。

$ docker buildx version

その結果、CodeBuild で使用している Buildx のバージョンは v0.14.1 であり、バージョン的には問題ないことがわかりました。

[Container] 2024/11/19 10:00:00.000000 Running command docker buildx version
github.com/docker/buildx v0.14.1 59582a88fca7858dbe1886fd1556b2a0d79e43a3

さらに、出力されたコミットハッシュ時点でのソースを確認したところ、こちらにもリリースノートに貼られていたものと同様の実装があり、やはり問題ないことが判明(52, 53行目)。

https://github.com/tonistiigi/buildx/blob/59582a88fca7858dbe1886fd1556b2a0d79e43a3/util/buildflags/secrets.go#L42-L56

試したこと2

Buildx のバージョンは問題なさそうだったため、「BuildKit を使用できていないのでは?」と考え、次はその部分を見直すことにしました。

先ほど同様に buildspec.yml に以下を追加し、Docker Engine のバージョンを確認しました。

$ docker --version

結果、Docker Engine のバージョンは v26.1.4 であり、デフォルトで BuildKit が使用されるはずだという結論に至りました。

(厳密には $ docker --version はエンジンのバージョンを確認するコマンドではないのですが、大体同じ値になるのでその辺は割愛)

[Container] 2024/11/19 10:10:00.000000 Running command docker --version
Docker version 26.1.4, build 5650f9b

公式ドキュメントにも v23.0 以降の Docker Desktop, Docker Engine はデフォルトのビルダーとして BuildKit が使用されることが明記されています。

BuildKit is the default builder for users on Docker Desktop and Docker Engine v23.0 and later.

https://docs.docker.com/build/buildkit/#getting-started

念のため、以下のように buildspec.yml を変更し、BuildKit を明示的に有効化しました。

buildspec.yml
 phases:
   build:
     commands:
       - |
-        docker build \
+        DOCKER_BUILDKIT=1 docker build \
           --secret id=xxx,env=XXX \
           --secret id=xxx,env=XXX \
           -t "${IMAGE_NAME}:${IMAGE_TAG}" \
           -f "${DOCKERFILE_PATH}" \
           "${BUILD_CONTEXT}"

特に何もしなくても BuildKit は使用できているはずなので、当然これでもエラーは解消されず途方に暮れます。

解決方法

問題を解決するために調査を続けていたところ、約2ヶ月前に投稿された、同様の問題を訴える issue を発見しました。

https://github.com/docker/docs/issues/20935

この issue の内容を追っていくと、どうやら Dockerfile 内で Build secrets を環境変数としてマウントする env オプションは、2024/09/10 にリリースされた BuildKit v1.10.0 で初めてサポートされたことがわかりました。

Build secrets can now be mounted as environment variables using the env=VARIABLE option. moby/buildkit#5215

https://docs.docker.com/build/buildkit/dockerfile-release-notes/#1100

さらに、この BuildKit バージョンは Docker Engine v27.3.0 以降で含まれることが明記されています。

CodeBuild 環境内の Docker Engine は v26.1.4 だったため、このバージョンには含まれていないリリースであることがわかります。


つまり、CodeBuild 環境で発生していたエラーは、 Dockerfile の解析を担当する BuildKit フロントエンドのバージョンが古かったこと が原因だったようです。

コマンドオプション解析を行う Buildx は --secretenv に対応していた一方で、Dockerfile の解析を行う BuildKit フロントエンドの方が --secretenv に対応していないバージョンだった、ということになります。

CodeBuild 環境内の Docker Engine をアップデートすることは現実的ではないため、 syntax ディレクティブを利用して最新の BuildKit フロントエンドを明示的に指定すること で解決しました。

Dockerfile の先頭に以下の1行を追加することでsyntax ディレクティブを利用できます。

Dockerfile
# syntax=docker/dockerfile:1

また、特定のバージョンを使用したい場合には、以下のようにバージョンを固定することも可能です。

Dockerfile
# syntax=docker/dockerfile:1.10.0

数時間振り回された末に、これで無事にエラーが解消されました。

あとがき

docker build ノコトナニモワカッテナカッタンダナ。

今振り返ると、今回の問題は docker build について以下のポイントを理解していたかどうかで、問題解決の勘所を掴めるかどうかが大きく変わるものだったなと感じています。

  • docker build はビルドクライアントである Buildx とビルドバックエンドである BuildKit によって構成されていること。
  • docker build コマンド実行時のオプション解析は Buildx が担当し、Dockerfile の解析は BuildKit が担当しているということ。
  • BuildKit が Dockerfile を解析する際、BuildKit フロントエンドというコンポーネントを使用して解析を行っていること。
  • BuildKit フロントエンドはデフォルトで BuildKit に組み込まれたものが使用されるが、 syntax ディレクティブを利用することで外部の(最新の)ものを使用できること。

エラー直面当初は、自分もこのあたりの仕組みを全く理解しておらず、issue を見つけて解決した後に「で、結局何が原因で何をして解決したんだ?そもそも自分は何を知らないから苦労したんだ?」と考えることに。

その後、ドキュメント等をあさって色々調べた結果、ようやく自分の頭で整理して理解できました。

普段何気なく使用していただけに、Docker って奥が深いなぁと改めて実感した問題でした。

これを機に docker build に対する解像度を上げることにも繋がったので、なんやかんやよかったと思っています。

内容的には若干マニアックだったかもしれませんが、本記事が同じような問題に直面している誰かの助けになれば嬉しいです。

参考リンク

Discussion