Docker Bake を使って複雑なビルドオプションを管理しよう

## Bake

https://www.docker.com/ja-jp/blog/ga-launch-docker-bake/

> Docker Bake は、Compose がランタイム環境の管理を簡素化するのと同様に、Docker ビルドを効率化するオーケストレーション ツールです。 Bake を使用すると、ビルドステージとデプロイ環境を宣言型ファイルで定義できるため、複雑なビルドの管理が容易になります。 また、BuildKit の並列化および最適化機能を活用して、ビルド時間を短縮します。

> Dockerfileはイメージのビルドステップを定義するのに最適ですが、チームは多くの場合、複数のイメージをビルドし、テスト、リンティング、コード生成などのヘルパータスクを実行する必要があります。 従来、これは多数の docker build コマンドと独自のオプションや引数をジャグリングすることを意味し、退屈でエラーが発生しやすいプロセスでした。

これまで BuildKit を用いてイメージをビルドする場合、下記のような複雑な引数をたくさん渡す必要がありました。

```sh
$ docker buildx build \
  --push \
  --cache-from="type=registry,ref=foo/myapp:latest" \
  --cache-to="type=registry,ref=foo/myapp:latest,mode=max,image-manifest=true,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true,push=true" \
  --tag foo/myapp:v1.0.0 \
  --tag foo/myapp:latest \
  --file ./main.Dockerfile \
  .
```

イメージをビルドするためにこのような引数を、 monorepo で様々なターゲットに対して行うのは非効率です。 CI などの bash スクリプトの中で管理することになると思いますが、煩雑ですね。

また、ローカル開発環境では docker compose で管理しているのに、本番環境はファイル管理されていないという一貫性のなさも問題です。

これらを解決するために、 `docker buildx bake` コマンドが生まれました。

```sh
$ docker buildx bake
```

これだけで自動的に bake に必要なファイルを読み込み、ビルドを行います。

このコマンドは Docker Desktop では 4.38.0 から(執筆段階での最新は 4.42.0)、 Docker Engine では 23.0 から(DOCKER_BUILDKIT=1 環境変数が必要)利用可能となっています。 BuildKit のバージョンは最新がおススメです。

Bake に必要な Bakefile は、 HCL(Terraform の記法), JSON, YAML を選択出来ますが、拡張性の点で HCL を選択するのが良いと思います。

最小構成だとこんな感じです。

```sh
$ docker build -t myapp:latest .
```

これが

```hcl
# docker-bake.hcl
target "default" {
  context = "."
  tags = [ "myapp:latest" ]
}
```

こうなります。もう少し複雑な例でいくと、

```hcl
# docker-bake.hcl
group "default" {
  targets = ["frontend", "backend"]
}

target "frontend" {
  context = "./frontend"
  dockerfile = "frontend.Dockerfile"
  args = {
    NODE_VERSION = "22"
  }
  tags = ["myapp/frontend:latest"]
}

target "backend" {
  context = "./backend"
  dockerfile = "backend.Dockerfile"
  args = {
    GO_VERSION = "1.24"
  }
  tags = ["myapp/backend:latest"]
}
```

こんな感じに書くことで、フロントエンドとバックエンドのビルドを一発で行えるようになります。

`cache-from` とかの `key=value,key=value` のような形式のオプションを `{ key = value, key = value }` と書けるようになるのでだいぶ閲覧性が上がります。

[Docker DX vscode extension](https://marketplace.visualstudio.com/items?itemName=docker.docker) を入れることで、この Bake ファイルのサポートをしてくれるようになるので便利です(まだ Beta なので色々エラーは出ます)。

compose.yaml では環境変数を `.env` ファイルに書き込んでおくことで、自動的にそれを展開してくれます(hcl では `.env` ファイルは読まれません)。

```.env
# .env
TAG=v1.1.0
```

```yaml
# compose.yaml
services:
  webapp:
    image: docker.io/username/webapp:${TAG:-v1.0.0}
    build:
      dockerfile: Dockerfile
```

```sh
$ docker buildx bake --print
```

```json
{
  "group": {
    "default": {
      "targets": ["webapp"]
    }
  },
  "target": {
    "webapp": {
      "context": ".",
      "dockerfile": "Dockerfile",
      "tags": ["docker.io/username/webapp:v1.1.0"]
    }
  }
}
```

このように、 docker compose の YAML とも互換性があるので、既存の compose.yaml からビルド設定を追加していくことも可能です。

この Bake に関しては公式ドキュメントに全部載っています。

https://docs.docker.com/build/bake/

## Builder

Dockerfile をパースし実際にビルドを実行するのは **BuildKit** です。デフォルトでは `docker` というドライバーが選択されているのですが、このドライバーだと使えないオプションが結構あります。例えばマルチアーキテクチャビルドをするために QEMU でエミュレーションしたり、キャッシュを細かく指示することが出来ません。

そこで、 Bake する際は新しいドライバーを作成することが推奨されます。

```sh
$ docker buildx create --name=container --driver=docker-container --use --bootstrap
```

とすることで、デフォルトのドライバーを `docker-container` にすることが出来ます。これは Docker Engine についてきた BuildKit でビルドするのではなく、新しくコンテナを立ち上げてその中でイメージをビルドするドライバです。

`--use` を指定しておくことで、明示しなくても `docker buildx` を使った時に優先して利用してくれます。

## zstd 圧縮による起動高速化

https://aws.amazon.com/jp/blogs/news/reducing-aws-fargate-startup-times-with-zstd-compressed-container-images/

例えば Amazon ECR では、 zstd という圧縮アルゴリズムをイメージ転送時に使うことで、イメージサイズを小さくし解凍効率を上げコンテナの起動時間を短縮することが出来るので、その設定を追加してみます。

```hcl
group "default" {
    output = [
        {
            type = "image",
            name = "${IMAGE_URI}:${IMAGE_TAG}",
            oci-mediatypes = true,
            compression = zstd,
            compression-level = 3,
            force-compression = true,
            push = true
        }
    ]
}
```

この設定を追加することで、 Fargate でのコンテナ起動の高速化が期待出来ます。記事に書いてある通り、 AWS の試験では compression-level = 3 が最適だったそうです。

※ zstd 圧縮はまだ OCI の標準に入っていないため、全てのランタイムでサポートされているわけではないので注意が必要です(例えば、 docker では containerd ランタイムを使わないと動作しません)

## BuildKit のリモートキャッシュ

https://aws.amazon.com/jp/blogs/news/announcing-remote-cache-support-in-amazon-ecr-for-buildkit-clients/

BuildKit のビルドはレイヤーごとにキャッシュを効かせることで、同一ハッシュのレイヤーをキャッシュから読み込んでビルド時間を高速化することが可能です。

Amazon ECR ではレジストリでのリモートキャッシュをサポートしているので、その設定を追加してみます。

```hcl
group "default" {
    cache-from = [
        {
            type = "registry",
            ref = "${REPOSITORY_URL}:cache"
        }
    ]
    cache-to = [
        {
            type = "registry",
            ref = "${REPOSITORY_URL}:cache",
            mode = "max",
            image-manifest = true,
            oci-mediatypes = true
        }
    ]
}
```

この設定を追加することで、 2 回目以降のイメージのビルド時間を大幅に短縮することが可能です。

どちらも、 `oci-mediatypes = true` を設定しています。これは、デフォルトの [Docker Image v2 Schema 2](https://docs.docker.com/registry/spec/manifest-v2-2/) ではなく [OCI v1 イメージ標準](https://github.com/opencontainers/image-spec) に基づいてビルドすることを指します。

もちろん先ほどの zstd 圧縮の設定と一緒に使うことも可能です。

## GitHub Actions で使う場合

https://github.com/marketplace/actions/docker-buildx-bake

公式で `docker/bake-action` が用意されているので、それを使うだけです。簡単ですね。