Go - 跨平台构建Docker镜像

1. 概述

Docker镜像支持多平台,即单个镜像可支持不同的OS和CPU架构,例如linux/amd64linux/arm64等。
在DockerHub,可以查看每个镜像支持的操作系统和处理器架构,如下图,python支持windows和linux两个操作系统,多种CPU架构。

python

ubuntu

在不同的操作系统和CPU架构下,通过docker pulldocker rundocker daemon会帮助我们自动选择适合的镜像,非常方便。
那么,如何build不同CPU架构的镜像呢,本文重点来介绍。

2. Buildx

集成了Buildx的BuildKit,可以实现跨平台的镜像构建,只需要通过--platform指定对应的平台即可,例如linux/amd64linux/arm64darwin/amd64

下面,先从一些术语来介绍其功能。

2.1. builder or builder实例

通过docker buildx ls可以查看,也支持createrm等操作

% 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.

2.2. builder node

一个builder可以包含多个node,作用:可提高build效率,也可支持更复杂的CPU架构下镜像构建。

实例创建之后可以添加新的节点,通过docker buildx create命令的--append选项可将--node <node>节点加入到--name <builder>选项指定的 builder 实例。如下将把一个远程节点加入 builder 实例:

$ docker buildx create --driver docker-container --platform linux/amd64 --name multi-builder
multi-builder
$ export DOCKER_HOST=tcp://10.10.150.66:2375
$ docker buildx create --name multi-builder --append --node remote-builder

刚创建的 builder 处于 inactive 状态,可以在 create 或 inspect 子命令中添加 --bootstrap 选项立即启动实例(可验证节点是否可用):

$ docker buildx inspect --bootstrap multi-builder
[+] Building 3.9s (2/2) FINISHED
 => [remote-builder internal] booting buildkit                                                                                                                                            3.9s
 => => pulling image moby/buildkit:buildx-stable-1                                                                                                                                        2.8s
 => => creating container buildx_buildkit_remote-builder                                                                                                                                  1.2s
 => [multi-builder0 internal] booting buildkit                                                                                                                                            3.7s
 => => pulling image moby/buildkit:buildx-stable-1                                                                                                                                        2.4s
 => => creating container buildx_buildkit_multi-builder0                                                                                                                                  1.3s
Name:   multi-builder
Driver: docker-container

Nodes:
Name:      multi-builder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Buildkit:  v0.10.5
Platforms: linux/amd64*, linux/386

Name:      remote-builder
Endpoint:  tcp://10.10.88.20:2375
Status:    running
Buildkit:  v0.10.5
Platforms: linux/arm64

docker buildx ls 将列出所有可用的 builder 实例和实例中的节点:

$ docker buildx ls
NAME/NODE         DRIVER/ENDPOINT         STATUS   PLATFORMS
multi-builder       docker-container
  multi-builder0    unix:///var/run/docker.sock running v0.10.5  linux/amd64*, linux/386
  remote-builder    tcp://10.10.88.20:2375      running v0.10.5  linux/arm64
default *         docker                           
  default         default                 running  linux/amd64, linux/386

如上就创建了一个支持多平台架构的 builder 实例,执行docker buildx use <builder>将切换到所指定的 builder 实例。

docker buildx inspectdocker buildx stopdocker buildx rm 命令用于管理一个实例的生命周期。

参考:如何使用 docker buildx 构建跨平台 Go 镜像 | Shall We Code? (waynerv.com)

2.3. builder driver

buildx 实例通过两种方式来执行构建任务,两种执行方式被称为使用不同的驱动

  • docker 驱动:使用 Docker 服务程序中集成的 BuildKit 库执行构建
  • docker-container 驱动:启动一个包含 BuildKit 的容器并在容器中执行构建

docker驱动无法使用一小部分 buildx 的特性,例如不能在一次运行中同时构建多个平台镜像。此外,在镜像的默认输出格式上也有所区别:docker驱动默认将构建结果以 Docker 镜像格式直接输出到 docker 的镜像目录(通常是 /var/lib/overlay2),之后执行 docker images 命令可以列出所输出的镜像。而,docker container则需要通过--output选项指定输出格式为镜像或其他格式。

为了一次性构建多个平台的镜像,本文使用docker container作为默认的 builder 实例驱动。

2.4. dockerfile中的环境变量

  • BUILDPLATFORM,构建节点的平台信息,例如linux/amd64linux/arm64
  • BUILDARCH
  • BUILDOS
  • TARGETPLATFORM,目标平台信息
  • TARGETARCH
  • TARGETOS

以下面的dockerfile为例,可以看到对应的env值:

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log

FROM alpine
COPY --from=build /log /log

build命令:docker buildx build --platform linux/amd64,linux/arm64 -t test:0 -o type=oci,dest=./oci-image .

2.5. output

执行构建命令时,除了指定镜像名称,另外两个重要的选项是指定目标平台输出格式

当使用 docker-container 驱动时,--platform可以接受用逗号分隔的多个值作为输入以同时指定多个目标平台,所有平台的构建结果将合并为一个整体的镜像列表作为输出,因此,无法直接输出为本地的 docker images 镜像。

docker buildx build支持丰富的输出行为,通过--output=[PATH,-,type=TYPE[,KEY=VALUE]选项可以指定构建结果的输出类型和路径等,常用的输出类型有以下几种:

  • local:构建结果将以文件系统格式写入 dest 指定的本地路径, 如 --output type=local,dest=./output
  • tar:构建结果将在打包后写入 dest 指定的本地路径
  • oci:构建结果以 OCI 标准镜像格式写入 dest 指定的本地路径
  • docker:构建结果以 Docker 标准镜像格式写入 dest 指定的本地路径或加载到 docker 的镜像库中。同时指定多个目标平台时无法使用该选项
  • image:以镜像或者镜像列表输出,并支持 push=true 选项直接推送到远程仓库,同时指定多个目标平台时可使用该选项
  • registry:type=image,push=true 的精简表示

常用的也就两种方式,分别是-o type=registry-o type=docker,dest=./linux-amd64-image
第一种,支持同时build多个镜像,第二种每次只能build一个特定平台的镜像。

docker buildx build --platform linux/amd64,linux/arm64 -t test:0 -o type=registry

# 上面的命令等价与下面两条命令
docker buildx build --platform linux/amd64 -t test:0 -o type=docker,dest=./linux-amd64-image .
docker buildx build --platform linux/arm64 -t test:0 -o type=docker,dest=./linux-arm64-image .
% docker buildx build --platform linux/amd64 -t test:0 -o type=docker,dest=./linux-amd64-image .
[+] Building 7.8s (11/11) FINISHED
 => [internal] load build definition from dockerfile                                                                                                     0.0s
 => => transferring dockerfile: 250B                                                                                                                     0.0s
 => [internal] load .dockerignore                                                                                                                        0.0s
 => => transferring context: 2B                                                                                                                          0.0s
 => [internal] load metadata for docker.io/library/alpine:latest                                                                                         7.6s
 => [internal] load metadata for docker.io/library/golang:alpine                                                                                         7.4s
 => [auth] library/golang:pull token for registry-1.docker.io                                                                                            0.0s
 => [auth] library/alpine:pull token for registry-1.docker.io                                                                                            0.0s
 => [build 1/2] FROM docker.io/library/golang:alpine@sha256:913de96707b0460bcfdfe422796bb6e559fc300f6c53286777805a9a3010a5ea                             0.0s
 => => resolve docker.io/library/golang:alpine@sha256:913de96707b0460bcfdfe422796bb6e559fc300f6c53286777805a9a3010a5ea                                   0.0s
 => [stage-1 1/2] FROM docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126                                  0.0s
 => => resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126                                          0.0s
 => CACHED [build 2/2] RUN echo "I am running on linux/arm64, building for linux/amd64" > /log                                                           0.0s
 => CACHED [stage-1 2/2] COPY --from=build /log /log                                                                                                     0.0s
 => exporting to docker image format                                                                                                                     0.1s
 => => exporting layers                                                                                                                                  0.0s
 => => exporting manifest sha256:5f55c6c58da4636aa38363b1aed04cc2ff1921bfb12c42aa308430a8a20c12ac                                                        0.0s
 => => exporting config sha256:f2ce60afc09e4bb74d905f6b1a58504c140f2090a1420d275e89381a36c9947e                                                          0.0s
 => => sending tarball                                                                                                                                   0.1s
% ll
total 4108
-rw-r--r-- 1 shuzhang staff     211  5  7 16:34 dockerfile
-rw-r--r-- 1 shuzhang staff 3384832  5  7 17:26 linux-amd64-image

% docker load < linux-amd64-image
f1417ff83b31: Loading layer [==================================================>]  3.375MB/3.375MB
c5ddf0bc21cf: Loading layer [==================================================>]     148B/148B
Loaded image: test:0

% docker images | grep test
test                                                                  0                 f2ce60afc09e   50 minutes ago   7.05MB

3. Example

下面将以一个简单的 Go 项目作为示例,假设示例程序文件 main.go 内容如下:

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println("Hello world!")
    fmt.Printf("Running in [%s] architecture.\n", runtime.GOARCH)
}

定义构建过程的 Dockerfile 如下:

FROM --platform=$BUILDPLATFORM golang:1.14 as builder

ARG TARGETOS
ARG TARGETARCH

WORKDIR /app
COPY main.go /app/main.go
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -o output/main main.go

FROM alpine:latest
WORKDIR /root
COPY --from=builder /app/output/main .
CMD /root/main

构建过程分为两个阶段:

  • 在一阶段中,我们将拉取一个和当前构建节点相同平台的 golang 镜像,并使用 Go 的交叉编译特性将其编译为目标架构的二进制文件。
  • 然后拉取目标平台的 alpine 镜像,并将上一阶段的编译结果拷贝到镜像中。

4. Setup builder

Docker Desktop provides binfmt_misc multi-architecture support, which means you can run containers for different Linux architectures such as arm, mips, ppc64le, and even s390x.

对于没有Desktop的服务器而言,最新的docker版本默认支持buildx,可能需要安装binfmt,以支持跨平台构建。

# 检查我们当前的buildx可以使用的全部的构建实例/构建节点,我们发现还是使用默认的,没有任何改变
[thinktik@thinkdev ~]$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
default * docker                  
  default default         running linux/amd64, linux/386
  
# 安装binfmt,它内置了QEMU能提供跨平台构建功能
[thinktik@thinkdev ~]$ docker run --privileged --rm tonistiigi/binfmt --install all
Unable to find image 'tonistiigi/binfmt:latest' locally
latest: Pulling from tonistiigi/binfmt
2b4d0e08bd75: Pull complete 
c331be51c382: Pull complete 
Digest: sha256:5bf63a53ad6222538112b5ced0f1afb8509132773ea6dd3991a197464962854e
Status: Downloaded newer image for tonistiigi/binfmt:latest
installing: s390x OK
installing: riscv64 OK
installing: mips64le OK
installing: mips64 OK
installing: arm64 OK
installing: arm OK
installing: ppc64le OK
{
  "supported": [
    "linux/amd64",
    "linux/arm64",
    "linux/riscv64",
    "linux/ppc64le",
    "linux/s390x",
    "linux/386",
    "linux/mips64le",
    "linux/mips64",
    "linux/arm/v7",
    "linux/arm/v6"
  ],
  "emulators": [
    "qemu-aarch64",
    "qemu-arm",
    "qemu-mips64",
    "qemu-mips64el",
    "qemu-ppc64le",
    "qemu-riscv64",
    "qemu-s390x"
  ]
}

# 创建一个buildx构建器
[thinktik@thinkdev ~]$ docker buildx create --name crossbuilder --driver docker-container
crossbuilder
# 使用新创建的构建器
[thinktik@thinkdev ~]$ docker buildx use crossbuilder
# 再次检查buildx可以使用的构建实例/构建节点,我们看到新加了一个crossbuilder,并且显示支持了一大批CPU架构
[thinktik@thinkdev ~]$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS   PLATFORMS
crossbuilder *  docker-container                     
  crossbuilder0 unix:///var/run/docker.sock inactive 
default         docker                               
  default       default                     running  linux/amd64, linux/386, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6

5. References

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容