使用 buildx 構建跨平臺鏡像

構建跨平臺鏡像是 Docker 生態系統中的一個重要話題,因爲跨平臺鏡像可以在多種平臺上運行,極具靈活性。爲了實現這個目標,Docker 社區提供了多種方式來構建跨平臺鏡像,其中之一是使用 docker manifest,我在《使用 docker manifest 構建跨平臺鏡像》一文中詳細介紹了這種方法。然而,目前最流行的方式是使用 Docker 的 buildx 工具,這種方式不僅可以輕鬆構建跨平臺鏡像,還可以自動化整個構建過程,大大提高了效率。在本文中,我們將重點介紹使用 buildx 構建跨平臺鏡像的方法和技巧。

簡介

buildx 是 Docker 官方提供的一個構建工具,它可以幫助用戶快速、高效地構建 Docker 鏡像,並支持多種平臺的構建。使用 buildx,用戶可以在單個命令中構建多種架構的鏡像,例如 x86 和 ARM 架構,而無需手動操作多個構建命令。此外,buildx 還支持 Dockerfile 的多階段構建和緩存,這可以大大提高鏡像構建的效率和速度。

安裝

buildx 是一個管理 Docker 構建的 CLI 插件,底層使用 BuildKit 擴展了 Docker 構建功能。

筆記:BuildKit 是 Docker 官方提供的一個高性能構建引擎,可以用來替代 Docker 原有的構建引擎。相比於原有引擎,BuildKit 具有更快的構建速度、更高的並行性、更少的資源佔用和更好的安全性。

要安裝並使用 buildx,需要 Docker Engine 版本號大於等於 19.03。

如果你使用的是 Docker Desktop,則默認安裝了 buildx。可以使用 docker buildx version 命令查看安裝版本,得到以下類似輸出,證明已經安裝過了。

$ docker buildx version
github.com/docker/buildx v0.9.1 ed00243a0ce2a0aee75311b06e32d33b44729689

如果需要手動安裝,可以從 GitHub 發佈頁面下載對應平臺的最新二進制文件,重命名爲 docker-buildx,然後將其放到 Docker 插件目錄下(Linux/Mac 系統爲 $HOME/.docker/cli-plugins,Windows 系統爲 %USERPROFILE%\.docker\cli-plugins)。

Linux/Mac 系統還需要給插件增加可執行權限 chmod +x ~/.docker/cli-plugins/docker-buildx,之後就可以使用 buildx 了。

更詳細的安裝過程可以參考官方文檔。

構建跨平臺鏡像

首先,需要澄清的是,本文中所提到的「跨平臺鏡像」這一說法並不十分準確。實際上,Docker 官方術語叫 Multi-platform images 即「多平臺鏡像」,意思是支持多種不同 CPU 架構的鏡像。之所以使用「跨平臺鏡像」這一術語,是因爲從使用者的角度來看,在使用如 docker pulldocker run 等命令來拉取和啓動容器時,並不會感知到這個鏡像是一個虛擬的 manifest list 鏡像還是針對當前平臺的鏡像。

筆記:manifest list 是通過指定多個鏡像名稱創建的鏡像列表,是一個虛擬鏡像,它包含了多個不同平臺的鏡像信息。可以像普通鏡像一樣使用 docker pulldocker run 等命令來操作它。如果你想了解關於 manifest list 的更多信息,可參考《使用 docker manifest 構建跨平臺鏡像》一文。

跨平臺鏡像構建策略

builder 支持三種不同策略構建跨平臺鏡像:

  1. 在內核中使用 QEMU 仿真支持。

如果你正在使用 Docker Desktop,則已經支持了 QEMU,QEMU 是最簡單的構建跨平臺鏡像策略。它不需要對原有的 Dockerfile 進行任何更改,BuildKit 會通過 binfmt_misc 這一 Linux 內核功能實現跨平臺程序的執行。

工作原理:

QEMU 是一個處理器模擬器,可以模擬不同的 CPU 架構,我們可以把它理解爲是另一種形式的虛擬機。在 buildx 中,QEMU 用於在構建過程中執行非本地架構的二進制文件。例如,在 x86 主機上構建一個 ARM 鏡像時,QEMU 可以模擬 ARM 環境並運行 ARM 二進制文件。

binfmt_misc 是 Linux 內核的一個模塊,它允許用戶註冊可執行文件格式和相應的解釋器。當內核遇到未知格式的可執行文件時,會使用 binfmt_misc 查找與該文件格式關聯的解釋器(在這種情況下是 QEMU)並運行文件。

QEMU 和 binfmt_misc 的結合使得通過 buildx 跨平臺構建成爲可能。這樣我們就可以在一個架構的主機上構建針對其他架構的 Docker 鏡像,而無需擁有實際的目標硬件。

雖然 Docker Desktop 預配置了 binfmt_misc 對其他平臺的支持,但對於其他版本 Docker,你可能需要使用 tonistiigi/binfmt 鏡像啓動一個特權容器來進行支持:

$ docker run --privileged --rm tonistiigi/binfmt --install all
  1. 使用相同的構建器實例在多個本機節點上構建。

此方法直接在對應平臺的硬件上構建鏡像,所以需要準備各個平臺的主機。因爲此方法門檻比較高,所以並不常使用。

  1. 使用 Dockerfile 中的多階段構建,交叉編譯到不同的平臺架構中。

交叉編譯的複雜度不在於 Docker,而是取決於程序本身。比如 Go 程序就很容易實現交叉編譯,只需要在使用 go build 構建程序時指定 GOOSGOARCH 兩個環境變量即可實現。

創建 builder

要使用 buildx 構建跨平臺鏡像,我們需要先創建一個 builder,可以翻譯爲「構建器」。

使用 docker buildx ls 命令可以查看 builder 列表:

$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT STATUS  BUILDKIT PLATFORMS
default *       docker
  default       default         running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux   docker
  desktop-linux desktop-linux   running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

這兩個是默認 builderdefault * 中的 * 表示當前正在使用的 builder,當我們運行 docker build 命令時就是在使用此 builder 構建鏡像。

可以發現,這兩個默認的 builder 第二列 DRIVER/ENDPOINT 項的值都是 docker,表示它們都使用 docker 驅動程序。

buildx 支持以下幾種驅動程序:

IrVapM

默認的 docker 驅動程序優先考慮簡單性和易用性,所以它對緩存和輸出格式等高級功能的支持有限,並且不可配置。其他驅動程序則提供了更大的靈活性,並且更擅長處理高級場景。

具體差異你可以到官方文檔中查看。

因爲使用 docker 驅動程序的默認 builder 不支持使用單條命令(默認 builder--platform 參數只接受單個值)構建跨平臺鏡像,所以我們需要使用 docker-container 驅動創建一個新的 builder

命令語法如下:

$ docker buildx create --name=<builder-name> --driver=<driver> --driver-opt=<driver-options>

參數含義如下:

更多可選參數可以參考官方文檔。

我們可以使用如下命令創建一個新的 builder

$ docker buildx create --name mybuilder
mybuilder

再次查看 builder 列表:

$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS   BUILDKIT PLATFORMS
mybuilder *     docker-container
  mybuilder0    unix:///var/run/docker.sock inactive
default         docker
  default       default                     running  20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux   docker
  desktop-linux desktop-linux               running  20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

可以發現選中的構建器已經切換到了 mybuilder,如果沒有選中,你需要手動使用 docker buildx use mybuilder 命令切換構建器。

啓動 builder

我們新創建的 mybuilder 當前狀態爲 inactive,需要啓動才能使用。

$ docker buildx inspect --bootstrap mybuilder
[+] Building 16.8s (1/1) FINISHED
 =[internal] booting buildkit                                                                                                                                  16.8s
 ==> pulling image moby/buildkit:buildx-stable-1                                                                                                               16.1s
 ==> creating container buildx_buildkit_mybuilder0                                                                                                              0.7s
Name:   mybuilder
Driver: docker-container

Nodes:
Name:      mybuilder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Buildkit:  v0.9.3
Platforms: linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

inspect 子命令用來檢查構建器狀態,使用 --bootstrap 參數則可以啓動 mybuilder 構建器。

再次查看 builder 列表,mybuilder 狀態已經變成了 running

$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS  BUILDKIT PLATFORMS
mybuilder *     docker-container
  mybuilder0    unix:///var/run/docker.sock running v0.9.3   linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
default         docker
  default       default                     running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux   docker
  desktop-linux desktop-linux               running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

其中 PLATFORMS 一列所展示的值 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6 就是當前構建器所支持的所有平臺了。

現在使用 docker ps 命令可以看到 mybuilder 構建器所對應的 BuildKit 容器已經啓動。

$ docker ps
CONTAINER ID   IMAGE                           COMMAND                  CREATED         STATUS         PORTS                                NAMES
b8887f253d41   moby/buildkit:buildx-stable-1   "buildkitd"              4 minutes ago   Up 4 minutes                                        buildx_buildkit_mybuilder0

這個容器就是輔助我們構建跨平臺鏡像用的,不要手動刪除它。

使用 builder 構建跨平臺鏡像

現在一些準備工作已經就緒,我們終於可以使用 builder 構建跨平臺鏡像了。

這裏以一個 Go 程序爲例,來演示如何構建跨平臺鏡像。

hello.go 程序如下:

package main

import (
 "fmt"
 "runtime"
)

func main() {
 fmt.Printf("Hello, %s/%s!\n", runtime.GOOS, runtime.GOARCH)
}

這個程序非常簡單,執行後打印 Hello, 操作系統/CPU 架構

Go 程序還需要一個 go.mod 文件:

module hello

go 1.20

編寫 Dockerfile 內容如下:

FROM golang:1.20-alpine AS builder
WORKDIR /app
ADD . .
RUN go build -o hello .

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]

這是一個普通的 Dockerfile 文件,爲了減小鏡像大小,使用了多階段構建。它跟構建僅支持當前平臺的鏡像所使用的 Dockerfile 沒什麼兩樣。

$ ls
Dockerfile go.mod     hello.go

以上三個文件需要放在同一個目錄下,然後就可以在這個目錄下使用 docker buildx 來構建跨平臺鏡像了。

$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go .

docker buildx build 語法跟 docker build 一樣,--platform 參數表示構建鏡像的目標平臺,-t 表示鏡像的 Tag,. 表示上下文爲當前目錄。

唯一不同的是對 --platform 參數的支持,docker build--platform 參數只支持傳遞一個平臺信息,如 --platform linux/arm64,也就是一次只能構建單個平臺的鏡像。

而使用 docker buildx build 構建鏡像則支持同時傳遞多個平臺信息,中間使用英文逗號分隔,這樣就實現了只用一條命令便可以構建跨平臺鏡像的功能。

執行以上命令後,我們將會得到一條警告:

WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load

這條警告提示我們沒有爲 docker-container 驅動程序指定輸出,生成結果將只會保留在構建緩存中,使用 --push 可以將鏡像推送到 Docker Hub 遠程倉庫,使用 --load 可以將鏡像保存在本地。

這是因爲我們新創建的 mybuilder 是啓動了一個容器來運行 BuildKit,它並不能直接將構建好的跨平臺鏡像輸出到本機或推送到遠程,必須要用戶來手動指定輸出位置。

我們可以嘗試指定 --load 將鏡像保存的本地主機。

$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go . --load
[+] Building 0.0s (0/0)
ERROR: docker exporter does not currently support exporting manifest lists

結果會得到一條錯誤日誌。看來它並不支持直接將跨平臺鏡像輸出到本機,這其實是因爲傳遞了多個 --platform 的關係,如果 --platform 只傳遞了一個平臺,則可以使用 --load 將構建好的鏡像輸出到本機。

那麼我們就只能通過 --push 參數將跨平臺鏡像推送到遠程倉庫了。不過在此之前需要確保使用 docker login 完成登錄。

$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go . --push

現在登錄 Docker Hub 就可以看見推送上來的跨平臺鏡像了。

jianghushinian/hello-go

我們也可以使用 imagetools 來檢查跨平臺鏡像的 manifest 信息。

$ docker buildx imagetools inspect jianghushinian/hello-go
Name:      docker.io/jianghushinian/hello-go:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:    sha256:51199dadfc55b23d6ab5cfd2d67e38edd513a707273b1b8b554985ff562104db

Manifests:
  Name:      docker.io/jianghushinian/hello-go:latest@sha256:8032a6f23f3bd3050852e77b6e4a4d0a705dfd710fb63bc4c3dc9d5e01c8e9a6
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm64

  Name:      docker.io/jianghushinian/hello-go:latest@sha256:fd46fd7e93c7deef5ad8496c2cf08c266bac42ac77f1e444e83d4f79d58441ba
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/amd64

可以看到,這個跨平臺鏡像包含了兩個目標平臺的鏡像,分別是 linux/arm64linux/amd64

我們分別在 Apple M2 芯片平臺和 Linux x86 平臺來啓動這個 Docker 鏡像看下輸出結果。

$ docker run --rm jianghushinian/hello-go
Hello, linux/arm64!
$ docker run --rm jianghushinian/hello-go
Hello, linux/amd64!

至此,我們使用 builder 完成了跨平臺鏡像的構建。

使用交叉編譯

以上演示的構建跨平臺鏡像過程就是利用 QEMU 的能力,因爲 Go 語言的交叉編譯非常簡單,所以我們再來演示一下如何使用交叉編譯來構建跨平臺鏡像。

我們只需要對 Dockerfile 文件進行修改:

FROM --platform=$BUILDPLATFORM golang:1.20-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
ADD . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o hello .

FROM --platform=$TARGETPLATFORM alpine:latest
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]

其中 BUILDPLATFORMTARGETOSTARGETARCHTARGETPLATFORM 四個變量是 BuildKit 提供的全局變量,分別表示構建鏡像所在平臺、操作系統、架構、構建鏡像的目標平臺。

在構建鏡像時,BuildKit 會將當前所在平臺信息傳遞給 Dockerfile 中的 BUILDPLATFORM 參數(如 linux/arm64)。

通過 --platform 參數傳遞的 linux/arm64,linux/amd64 鏡像目標平臺列表會依次傳遞給 TARGETPLATFORM 變量。

TARGETOSTARGETARCH 兩個變量在使用時則需要先通過 ARG 進行聲明,BuildKit 會自動爲其賦值。

在 Go 程序進行編譯時,可以通過 GOOS 環境變量指定目標操作系統,通過 GOARCH 環境變量指定目標架構。

所以這個 Dockerfile 所表示的含義是:首先拉取當前構建鏡像所在平臺的 golang 鏡像,然後使用交叉編譯構建目標平臺的 Go 程序,最後將構建好的 Go 程序複製到目標平臺的 alpine 鏡像。

最終我們會通過交叉編譯得到一個跨平臺鏡像。

筆記:通過 FROM --platform=$BUILDPLATFORM image 可以拉取指定平臺的鏡像,由此我們可以知道,其實 golang 和 alpine 鏡像都是支持跨平臺的。

構建鏡像命令不變:

$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-cross-go . --push

啓動鏡像後輸出結果不變:

$ docker run --rm jianghushinian/hello-cross-go
Hello, linux/arm64!
$ docker run --rm jianghushinian/hello-cross-go
Hello, linux/amd64!

至此,我們利用 Go 語言的交叉編譯完成了跨平臺鏡像的構建。

平臺相關的全局變量

關於上面提到的幾個全局變量,BuildKit 後端預定義了一組 ARG 全局變量(共 8 個)可供使用,其定義和說明如下:

8dFupW

使用示例如下:

# 這裏可以直接使用 TARGETPLATFORM 變量
FROM --platform=$TARGETPLATFORM alpine

# 稍後的 RUN 命令想要使用變量必須提前用 ARG 進行聲明
ARG TARGETPLATFORM

RUN echo "I'm building for $TARGETPLATFORM"

刪除 builder

我們已經實現了使用 builder 構建跨平臺鏡像。如果現在你想要恢復環境,刪除新建的 builder。則可以使用 docker buildx rm mybuilder 命令來完成。

$ docker buildx rm mybuilder
mybuilder removed

跟隨 mybuilder 啓動的 buildx_buildkit_mybuilder0 容器也會隨之被刪除。

現在再使用 docker buildx ls 命令查看構建器列表,已經恢復成原來的樣子了。

$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT STATUS  BUILDKIT PLATFORMS
default *       docker
  default       default         running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux   docker
  desktop-linux desktop-linux   running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

功能清單

除了前文介紹的幾個 buildx 常用命令,更多功能可以通過 --help 參數進行查看。

$ docker buildx --help

Usage:  docker buildx [OPTIONS] COMMAND

Extended build capabilities with BuildKit

Options:
      --builder string   Override the configured builder instance

Management Commands:
  imagetools  Commands to work on images in registry

Commands:
  bake        Build from a file
  build       Start a build
  create      Create a new builder instance
  du          Disk usage
  inspect     Inspect current builder instance
  ls          List builder instances
  prune       Remove build cache
  rm          Remove a builder instance
  stop        Stop builder instance
  use         Set the current builder instance
  version     Show buildx version information

Run 'docker buildx COMMAND --help' for more information on a command.

stoprm 可以管理 builder 的生命週期。每條子命令又可以使用 docker buildx COMMAND --help 方式查看使用幫助,感興趣的同學可以自行學習。

總結

本文講解了如何使用 buildx 構建跨平臺鏡像,這也是在 Docker 生態中目前最優的構建方式。

首先介紹了 buildx 是什麼,以及如何安裝。接下來就進入了構建跨平臺鏡像的講解,我們分析了三種跨平臺鏡像構建策略,並且分別對 QEMU 和 交叉編譯兩種策略進行了演示。QEMU 策略無需對 Dockerfile 做任何更改,而使用交叉編譯方式則需要根據程序的支持來編寫 Dockerfile 構建跨平臺應用。

最後我們還講解了如何管理 buildx 的生命週期,以及羅列了 buildx 的功能清單幫助你進一步深入學習。

參考

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OvOIjj4ser-QWd2BsfsY-Q