使用 Docker Buildx Bake 精通多平台构建、测试等

本指南演示了如何使用 Docker Buildx Bake 简化和自动化镜像构建、测试以及生成构建制品的过程。通过在声明式 docker-bake.hcl 文件中定义构建配置,您可以消除手动脚本,并为复杂的构建、测试和制品生成启用高效的工作流程。

前提

本指南假定您熟悉

先决条件

  • 您的机器上安装了最新版本的 Docker。
  • 您安装了 Git 以克隆仓库。
  • 您正在使用 containerd 镜像存储。

简介

本指南使用一个示例项目来演示 Docker Buildx Bake 如何简化您的构建和测试工作流程。该仓库包含一个 Dockerfile 和一个 docker-bake.hcl 文件,为您提供了一个即用型设置来尝试 Bake 命令。

从克隆示例仓库开始

git clone https://github.com/dvdksn/bakeme.git
cd bakeme

Bake 文件 docker-bake.hcl 使用目标(target)和组(group)以声明式语法定义了构建目标,使您能够高效地管理复杂的构建。

这是开箱即用的 Bake 文件样子

target "default" {
  target = "image"
  tags = [
    "bakeme:latest",
  ]
  attest = [
    "type=provenance,mode=max",
    "type=sbom",
  ]
  platforms = [
    "linux/amd64",
    "linux/arm64",
    "linux/riscv64",
  ]
}

`target` 关键字为 Bake 定义了一个构建目标。`default` 目标定义了当命令行中没有指定特定目标时要构建的目标。以下是 `default` 目标的选项快速摘要

  • target:Dockerfile 中的目标构建阶段。

  • tags:要分配给镜像的标签。

  • attest:要附加到镜像的证明信息

    提示

    证明信息提供元数据,例如构建出处(用于跟踪镜像构建的来源)以及 SBOM(软件物料清单),这些信息对于安全审计和合规性很有用。

  • platforms:要构建的平台变体。

要执行此构建,只需在仓库根目录下运行以下命令

$ docker buildx bake

使用 Bake,您可以避免冗长、难以记住的命令行咒语,通过用结构化的配置文件替换手动、易出错的脚本来简化构建配置管理。

作为对比,以下是如果没有 Bake,此构建命令的样子

$ docker buildx build \
  --target=image \
  --tag=bakeme:latest \
  --provenance=true \
  --sbom=true \
  --platform=linux/amd64,linux/arm64,linux/riscv64 \
  .

测试和 Linting

Bake 不仅用于定义构建配置和运行构建。您还可以使用 Bake 运行测试,有效地将 BuildKit 用作任务运行器。在容器中运行测试对于确保结果的可重复性非常有用。本节展示如何添加两种类型的测试

  • 使用 go test 进行单元测试。
  • 使用 golangci-lint 进行代码风格检查。

以测试驱动开发 (TDD) 的方式,首先向 Bake 文件添加一个新的 test 目标

target "test" {
  target = "test"
  output = ["type=cacheonly"]
}

提示

使用 type=cacheonly 可确保构建输出被有效丢弃;层会保存到 BuildKit 的缓存中,但 Buildx 不会尝试将结果加载到 Docker Engine 的镜像存储中。

对于测试运行,您无需导出构建输出 — 只需要关心测试执行结果。

要执行此 Bake 目标,运行 docker buildx bake test。此时,您将收到一个错误,指示 test 阶段在 Dockerfile 中不存在。

$ docker buildx bake test
[+] Building 1.2s (6/6) FINISHED
 => [internal] load local bake definitions
...
ERROR: failed to solve: target stage "test" could not be found

为了满足此目标,添加相应的 Dockerfile 目标。这里的 test 阶段基于与构建阶段相同的基础阶段。

FROM base AS test
RUN --mount=target=. \
    --mount=type=cache,target=/go/pkg/mod \
    go test .

提示

--mount=type=cache 指令在构建之间缓存 Go 模块,通过避免重新下载依赖项来提高构建性能。此共享缓存可确保相同的依赖项集合在构建、测试和其他阶段中可用。

现在,使用 Bake 运行 test 目标将评估此项目的单元测试。如果您想验证它是否有效,可以随意更改 main_test.go 以使测试失败。

接下来,要启用 Linting,向 Bake 文件添加另一个目标,命名为 lint

target "lint" {
  target = "lint"
  output = ["type=cacheonly"]
}

并在 Dockerfile 中添加构建阶段。此阶段将使用 Docker Hub 上的官方 golangci-lint 镜像。

提示

由于此阶段依赖于执行外部依赖项,因此通常最好将您想要使用的版本定义为构建参数。这使得将来更容易管理版本升级,通过将依赖项版本集中放置在 Dockerfile 的开头。

ARG GO_VERSION="1.23"
ARG GOLANGCI_LINT_VERSION="1.61"

#...

FROM golangci/golangci-lint:v${GOLANGCI_LINT_VERSION}-alpine AS lint
RUN --mount=target=.,rw \
    golangci-lint run

最后,要同时运行两个测试,您可以使用 Bake 文件中的 groups 构造。一个组可以指定多个目标通过一次调用运行。

group "validate" {
  targets = ["test", "lint"]
}

现在,运行两个测试就像这样简单

$ docker buildx bake validate

构建变体

有时您需要构建一个程序的多个版本。以下示例使用 Bake 构建程序的单独的“release”和“debug”变体,使用矩阵。使用矩阵可以并行运行具有不同配置的构建,从而节省时间并确保一致性。

矩阵将单个构建扩展为多个构建,每个构建代表矩阵参数的唯一组合。这意味着您可以协调 Bake 并行构建程序的生产版本和开发版本,且只需最少的配置更改。

本指南的示例项目配置为使用构建时选项来有条件地启用调试日志和跟踪功能。

  • 如果您使用 go build -tags="debug" 编译程序,将启用额外的日志和跟踪功能(开发模式)。
  • 如果您在没有 debug 标签的情况下构建,程序将使用默认日志记录器编译(生产模式)。

更新 Bake 文件,添加一个定义要构建的变量组合的矩阵属性

docker-bake.hcl
 target "default" {
+  matrix = {
+    mode = ["release", "debug"]
+  }
+  name = "image-${mode}"
   target = "image"

`matrix` 属性定义了要构建的变体(“release”和“debug”)。`name` 属性定义了矩阵如何扩展为多个不同的构建目标。在本例中,矩阵属性将构建扩展为两个工作流程:image-releaseimage-debug,每个工作流程使用不同的配置参数。

接下来,定义一个名为 BUILD_TAGS 的构建参数,它接受矩阵变量的值。

docker-bake.hcl
   target = "image"
+  args = {
+    BUILD_TAGS = mode
+  }
   tags = [

您还需要更改这些构建的镜像标签分配方式。目前,两个矩阵路径会生成相同的镜像标签名称,并相互覆盖。更新 tags 属性,使用条件运算符根据矩阵变量值设置标签。

docker-bake.hcl
   tags = [
-    "bakeme:latest",
+    mode == "release" ? "bakeme:latest" : "bakeme:dev"
   ]
  • 如果 moderelease,则标签名称是 bakeme:latest
  • 如果 modedebug,则标签名称是 bakeme:dev

最后,更新 Dockerfile,使其在编译阶段使用 BUILD_TAGS 参数。当 -tags="${BUILD_TAGS}" 选项计算结果为 -tags="debug" 时,编译器会使用debug.go 文件中的 configureLogging 函数。

Dockerfile
 # build compiles the program
 FROM base AS build
-ARG TARGETOS TARGETARCH
+ARG TARGETOS TARGETARCH BUILD_TAGS
 ENV GOOS=$TARGETOS
 ENV GOARCH=$TARGETARCH
 RUN --mount=target=. \
        --mount=type=cache,target=/go/pkg/mod \
-       go build -o "/usr/bin/bakeme" .
+       go build -tags="${BUILD_TAGS}" -o "/usr/bin/bakeme" .

就是这样。通过这些更改,您的 docker buildx bake 命令现在可以构建两个多平台镜像变体。您可以使用 docker buildx bake --print 命令检查 Bake 生成的规范构建配置。运行此命令会显示 Bake 将运行一个包含两个目标的 default 组,这两个目标具有不同的构建参数和镜像标签。

{
  "group": {
    "default": {
      "targets": ["image-release", "image-debug"]
    }
  },
  "target": {
    "image-debug": {
      "attest": ["type=provenance,mode=max", "type=sbom"],
      "context": ".",
      "dockerfile": "Dockerfile",
      "args": {
        "BUILD_TAGS": "debug"
      },
      "tags": ["bakeme:dev"],
      "target": "image",
      "platforms": ["linux/amd64", "linux/arm64", "linux/riscv64"]
    },
    "image-release": {
      "attest": ["type=provenance,mode=max", "type=sbom"],
      "context": ".",
      "dockerfile": "Dockerfile",
      "args": {
        "BUILD_TAGS": "release"
      },
      "tags": ["bakeme:latest"],
      "target": "image",
      "platforms": ["linux/amd64", "linux/arm64", "linux/riscv64"]
    }
  }
}

同时考虑所有平台变体,这意味着构建配置会生成 6 个不同的镜像。

$ docker buildx bake
$ docker image ls --tree

IMAGE                   ID             DISK USAGE   CONTENT SIZE   USED
bakeme:dev              f7cb5c08beac       49.3MB         28.9MB
├─ linux/riscv64        0eae8ba0367a       9.18MB         9.18MB
├─ linux/arm64          56561051c49a         30MB         9.89MB
└─ linux/amd64          e8ca65079c1f        9.8MB          9.8MB

bakeme:latest           20065d2c4d22       44.4MB         25.9MB
├─ linux/riscv64        7cc82872695f       8.21MB         8.21MB
├─ linux/arm64          e42220c2b7a3       27.1MB         8.93MB
└─ linux/amd64          af5b2dd64fde       8.78MB         8.78MB

导出构建制品

导出二进制文件等构建制品对于部署到没有 Docker 或 Kubernetes 的环境很有用。例如,如果您的程序旨在运行在用户的本地机器上。

提示

本节讨论的技术不仅适用于二进制文件等构建输出,也适用于任何类型的制品,例如测试报告。

对于 Go 和 Rust 等编程语言,编译后的二进制文件通常是可移植的,创建仅用于导出二进制文件的备用构建目标非常简单。您只需在 Dockerfile 中添加一个空阶段,该阶段除了要导出的二进制文件外不包含任何内容。

首先,让我们添加一种快速方法来为您本地平台构建二进制文件并将其导出到本地文件系统上的 ./build/local 路径。

docker-bake.hcl 文件中,创建一个新的 bin 目标。在此阶段,将 output 属性设置为本地文件系统路径。Buildx 会自动检测到输出看起来像文件路径,并使用local exporter 将结果导出到指定的路径。

target "bin" {
  target = "bin"
  output = ["build/bin"]
  platforms = ["local"]
}

注意此阶段指定了一个 local 平台。默认情况下,如果未指定 platforms,构建会针对 BuildKit 主机的操作系统和架构。如果您使用 Docker Desktop,这通常意味着构建会针对 linux/amd64linux/arm64,即使您的本地机器是 macOS 或 Windows,因为 Docker 运行在 Linux 虚拟机中。使用 local 平台会强制目标平台与您的本地环境匹配。

接下来,向 Dockerfile 添加 bin 阶段,该阶段会将编译好的二进制文件从构建阶段复制过来。

FROM scratch AS bin
COPY --from=build "/usr/bin/bakeme" /

现在您可以使用 docker buildx bake bin 导出您本地平台的二进制版本。例如,在 macOS 上,此构建目标会生成一个 Mach-O 格式的可执行文件 — 这是 macOS 的标准可执行文件格式。

$ docker buildx bake bin
$ file ./build/bin/bakeme
./build/bin/bakeme: Mach-O 64-bit executable arm64

接下来,让我们添加一个目标来构建程序的所有平台变体。为此,您可以继承刚刚创建的 bin 目标,并通过添加所需的平台来扩展它。

target "bin-cross" {
  inherits = ["bin"]
  platforms = [
    "linux/amd64",
    "linux/arm64",
    "linux/riscv64",
  ]
}

现在,构建 bin-cross 目标会为所有平台创建二进制文件。将自动为每个变体创建子目录。

$ docker buildx bake bin-cross
$ tree build/
build/
└── bin
    ├── bakeme
    ├── linux_amd64
    │   └── bakeme
    ├── linux_arm64
    │   └── bakeme
    └── linux_riscv64
        └── bakeme

5 directories, 4 files

为了同时生成“release”和“debug”变体,您可以使用矩阵,就像对默认目标所做的那样。使用矩阵时,您还需要根据矩阵值区分输出目录,否则二进制文件会写入到每个矩阵运行的相同位置。

target "bin-all" {
  inherits = ["bin-cross"]
  matrix = {
    mode = ["release", "debug"]
  }
  name = "bin-${mode}"
  args = {
    BUILD_TAGS = mode
  }
  output = ["build/bin/${mode}"]
}
$ rm -r ./build/
$ docker buildx bake bin-all
$ tree build/
build/
└── bin
    ├── debug
    │   ├── linux_amd64
    │   │   └── bakeme
    │   ├── linux_arm64
    │   │   └── bakeme
    │   └── linux_riscv64
    │       └── bakeme
    └── release
        ├── linux_amd64
        │   └── bakeme
        ├── linux_arm64
        │   └── bakeme
        └── linux_riscv64
            └── bakeme

10 directories, 6 files

结论

Docker Buildx Bake 简化了复杂的构建工作流程,实现了高效的多平台构建、测试和制品导出。通过将 Buildx Bake 集成到您的项目中,您可以简化 Docker 构建,使构建配置可移植,并更容易处理复杂的配置。

尝试不同的配置,并扩展您的 Bake 文件以满足项目的需求。您可以考虑将 Bake 集成到您的 CI/CD 流水线中,以自动化构建、测试和制品部署。Buildx Bake 的灵活性和强大功能可以显著改进您的开发和部署流程。

进一步阅读

有关如何使用 Bake 的更多信息,请查看以下资源

页面选项