无需 Docker 也能构建容器

【编者的话】本文介绍了除了Docker以外构建容器镜像的各种开源方案和它们各自的优缺点。

在这篇文章里,我将会介绍几种无需用到 Docker 本身即可构建容器的方法。我将会使用 OpenFaaS 作为案例研究,它使用了 OCI 格式的容器镜像作为它的工作负载。简单来说,我们可以把 OpenFaaS 看作是一个面向 Kubernetes 的 CaaS 平台,我们可以在上面运行微服务,而且它为我们带来了免费的 FaaS 以及由事件驱动的工具。

另请参阅 OpenFaaS.com

文章里的第一个方案将会展示如何使用 Docker 命令行里提供的内置的 buildkit 选项,随后会介绍一下独立运行的(只支持Linux) buildkit ,其次就是 Google 的容器构建工具, Kaniko

注意:这篇文章涵盖的是能够基于一份 Dockerfile 即可构建产出一个镜像的相关工具,因此任何比如限制用户只能使用 Java 或者 Go 的情况不在本文讨论范围。

然后我将会做一个总结,让你知道如何得到一些建议、反馈,以及自己关于容器工具周边的一些想法和需求的用户故事。

那么,Docker 有什么问题呢?

其实也没啥好说的,Docker 在 armhfarm64 还有 x86_64 上都运行地很好。Docker 命令行的主要功能已经不再是构建/装载/运行了,还包括了拖延了数年之久的沉重负担,现在它把 Docker Swarm 还有 Docker EE 的一些功能都捆绑到了一些。

有些人做过一些努力,试图将 “docker” 剥离出来,回归其原本的组件部分,我们都爱上了那个最初的UX:

  • Docker – docker 本身现在是使用 containerd 来运行容器,而且已经支持通过启用 buildkit 来实施更高效地,缓存式的构建任务。
  • Podmanbuildah 的结合 – 这是 RedHat 和 IBM 他们在做的尝试,使用他们自己的 OSS 工具链来生成 OCI 镜像。Podman 标榜的是无守护进程和去root,但是始终需要挂载 overlay 文件系统以及使用 Unix 套接字。
  • pouch – Pouch 来自阿里巴巴,它被称为 “一个高效的企业级容器引擎”。它同 Docker 一样使用的是 containerd,而且同时支持 runc 带来的容器级别的隔离,以及像 runv 这样 “轻量级虚拟机”。此外,它还把更多的 精力放在了镜像分发以及强隔离方面
  • 独立的 buildkit – buildkit 是由 Docker 公司的 Tõnis Tiigi 发起的,它是一款全新的兼顾了缓存和并发能力的容器构建工具。buildkit 目前只支持作为守护进程运行,但是你将会从人们那里听到完全相反的说辞。它们会 fork 该守护进程然后在一次构建结束后干掉它。
  • img – img 是一款由 Jess Frazelle 编写的工具,经常在这些指导手册里被引用,而且它是 buildkit 的一次重新包装。也就是说,和上述提到的其他方案相比,我并没有看到它有啥特别吸引人的地方。该项目 一直活跃到2018年底,此后仅收到了一些补丁 。img 声称它是无守护进程的,但是它用到了 buildkit,因此这里面可能有一些黑科技。我听说 img 提供了比 buildkit 本身的命令行 buildctr 更棒的UX,但是也应该注意的是,img 只发布了 x86_64 下的版本,而没有针对 armhf / arm64 的二进制文件。

img 的一个替代方案可能会是 k3c ,它也引入了一个运行时组件,并且计划加入对 ARM 架构的支持。

  • k3c – 这是一个 Rancher 最近的实验项目,它借助 containerd 和 buildkit 重新还原了最初的 Docker 版本所具备的原始而又经典的,香草一样精巧的用户体验。

以上所有方案里,我认为我最喜欢的是 k3c,但是它还非常稚嫩,而且因为把所有东西都打包到了一个二进制文件里,这很可能造成和其他软件存在冲突,目前它运行它自己内嵌的 containerd 和 buildkit 执行文件。

注意:如果你是 RedHat 的客户,并且购买了支持服务的话,那么你确实应该物尽其用,使用他们一整套的工具链。我查看了一些示例,并且看到了一个用到了我那篇“经典的”多阶段构建的博客文章。你可以比较一下这两个例子,看看自己更喜欢 buildah 还是 Dockerfile

那么,由于我们在这里关注的是“构建”部分,并且想要了解的是那些相对稳定的方案,接下来我将会看看下面这些选项:

  • docker 内置的 buildkit;
  • 单独运行的 buildkit;
  • 以及 kaniko。

OpenFaaS 命令行可以输出一个标准的任何构建工具都可以使用的“构建上下文”,因此我们可以方便地验证如上所有或者其他更多方案。

构建一个测试应用

让我们从一个 Golang 的 HTTP 中间件开始吧,这是一个函数和一个微服务之间的交错部分,而它展示了 OpenFaas 的通用性。

faas-cli template store pull golang-middleware



faas-cli new --lang golang-middleware \

build-test --prefix=alexellis2
--lang
build-test
--prefix

我们将可以得到如下结果:

./

├── build-test

│   └── handler.go

└── build-test.yml



1 directory, 2 files

handler 看上去像下面这样,而且改起来也方便。可以通过 vendor 或者 Go modules 来添加额外的依赖项。

package function



import (

"fmt"

"io/ioutil"

"net/http"

)



func Handle(w http.ResponseWriter, r *http.Request) {

var input []byte



if r.Body != nil {

    defer r.Body.Close()



    body, _ := ioutil.ReadAll(r.Body)



    input = body

}



w.WriteHeader(http.StatusOK)

w.Write([]byte(fmt.Sprintf("Hello world, input was: %s", string(input))))

}

以正常方式构建

正常情况下,我们会使用如下方式来构建这个应用:

faas-cli build -f build-test.yml

./template/golang-middleware/Dockerfile 里面也提供了模板文件以及 Dockerfile 的本地缓存。

这个模板在这里将会拉取三个镜像:

FROM openfaas/of-watchdog:0.7.3 as watchdog

FROM golang:1.13-alpine3.11 as build

FROM alpine:3.11

使用传统的构建工具的话,每个镜像将会被逐个顺序拉取。

等待片刻就大功告成了,如今在我们的本地库里已经有了该镜像。

我们也可以通过 faas-cli push -f build-test.yml 的方式将它推送上传到一个镜像仓库。

使用 Docker 和 BuildKit 构建

要做的改动再简单不过了,而且我们也可以得到一个更快的构建。

DOCKER_BUILDKIT=1 faas-cli build -f build-test.yml

我们将可以看到,使用这个方案的情况下,Docker守护进程会自动地将它的构建工具切换到buildkit。

BuildKit 有很多优点:

  • 更复杂的缓存机制;
  • 可以的话,请先执行后面的指令 – 比如,在”sdk”层的构建完成前下载”runtime”镜像;
  • 在第二次构建时能够更快

借助 buildkit,所有的基础镜像都可以立即拉取到我们的本地库中,因为FROM(下载)命令不是顺序执行的。

FROM openfaas/of-watchdog:0.7.3 as watchdog

FROM golang:1.13-alpine3.11 as build

FROM alpine:3.11

此选项甚至在 Mac 上也可以使用,因为 buildkit 是被虚拟机里运行的 Docker 守护进程代理的。

使用独立运行的 BuildKit 构建

要使用在独立运行模式下的 BuildKit 构建镜像的话,我们需要在一台 Linux 宿主机上单独运行 buildkit ,因此这里不能使用 Mac。

faas-cli build 通常会调用执行或者 fork docker ,该命令只是包了一层而已。因此,要绕过此行为的话,我们应当写出一个构建上下文,这可以通过执行如下命令实现:

faas-cli build -f build-test.yml --shrinkwrap



[0] > Building build-test.

Clearing temporary build folder: ./build/build-test/

Preparing ./build-test/ ./build/build-test//function

Building: alexellis2/build-test:latest with golang-middleware template. Please wait..

build-test shrink-wrapped to ./build/build-test/

[0] < Building build-test done in 0.00s.

[0] Worker done.



Total build time: 0.00

如今可以在 ./build/build-test/ 目录下找到我们需要的上下文,其中包含了我们的函数代码,以及带有 entrypoint 和 Dockerfile 的模板文件。

./build/build-test/

├── Dockerfile

├── function

│   └── handler.go

├── go.mod

├── main.go

└── template.yml



1 directory, 5 files

现在我们需要运行 buildkit,我们可以基于源码构建,或者获取上游的二进制文件。

curl -sSLf https://github.com/moby/buildkit/releases/download/v0.6.3/buildkit-v0.6.3.linux-amd64.tar.gz | sudo tar -xz -C /usr/local/bin/ --strip-components=1

如果你查看 releases 页面的话,你还将会找到适用于 armhf 和 arm64 的 buildkit,对于多体系结构的情况这一点棒极了。

在一个新的窗口里运行 buildkit 守护进程:

sudo buildkitd 

WARN[0000] using host network as the default            

INFO[0000] found worker "l1ltft74h0ek1718gitwghjxy", labels=map[org.mobyproject.buildkit.worker.executor:oci org.mobyproject.buildkit.worker.hostname:nuc org.mobyproject.buildkit.worker.snapshotter:overlayfs], platforms=[linux/amd64 linux/386] 

WARN[0000] skipping containerd worker, as "/run/containerd/containerd.sock" does not exist 

INFO[0000] found 1 workers, default="l1ltft74h0ek1718gitwghjxy" 

WARN[0000] currently, only the default worker can be used. 

INFO[0000] running server on /run/buildkit/buildkitd.sock 

现在让我们发起一次构建,把收缩包装(shrinkwrap)了的位置作为构建上下文传进去。我们需要的命令即是 buildctl ,buildctl 是守护进程的客户端程序,它将会配置如何构建镜像,以及完成后的操作,比如导出tar,忽略构建或者推送到镜像仓库。

buildctl build --help

NAME:

buildctl build - build



USAGE:



To build and push an image using Dockerfile:

$ buildctl build --frontend dockerfile.v0 --opt target=foo --opt build-arg:foo=bar --local context=. --local dockerfile=. --output type=image,name=docker.io/username/image,push=true





OPTIONS:

--output value, -o value  Define exports for build result, e.g. --output type=image,name=docker.io/username/image,push=true

--progress value          Set type of progress (auto, plain, tty). Use plain to show container output (default: "auto")

--trace value             Path to trace file. Defaults to no tracing.

--local value             Allow build access to the local directory

--frontend value          Define frontend used for build

--opt value               Define custom options for frontend, e.g. --opt target=foo --opt build-arg:foo=bar

--no-cache                Disable cache for all the vertices

--export-cache value      Export build cache, e.g. --export-cache type=registry,ref=example.com/foo/bar, or --export-cache type=local,dest=path/to/dir

--import-cache value      Import build cache, e.g. --import-cache type=registry,ref=example.com/foo/bar, or --import-cache type=local,src=path/to/dir

--secret value            Secret value exposed to the build. Format id=secretname,src=filepath

--allow value             Allow extra privileged entitlement, e.g. network.host, security.insecure

--ssh value               Allow forwarding SSH agent to the builder. Format default|[=|[,]]

如下命令和 Docker 命令用 DOCKER_BUILDKIT 覆盖后执行的结果是等价的:

sudo -E buildctl build --frontend dockerfile.v0 \

--local context=./build/build-test/ \

--local dockerfile=./build/build-test/ \

--output type=image,name=docker.io/alexellis2/build-test:latest,push=true

在执行此命令前,你需要运行 docker login ,或者创建一个 $HOME/.docker/config.json 文件,里面带上一组有效的未加密的安全凭证。

你将可以看到一个漂亮地描述当前构建进度的ASCII动画。

使用 img 和 buildkit 来构建

由于我从未使用过 img ,也没有真正意义上听闻过有哪个团队经常使用,而对于更常见的选项我想我会试一试。

我的第一印象是,多体系结构不是它优先考虑的问题,而且鉴于该项目的年代,它也不太可能上岸。它没有提供适用于armhf或者ARM64架构下的二进制文件。

对于 x86_64 来说,目前最新版本是2019年5月7日发布的 v0.5.7 ,该版本使用Go1.11构建,而当前版本是Go1.13。

sudo curl -fSL "https://github.com/genuinetools/img/releases/download/v0.5.7/img-linux-amd64" -o "/usr/local/bin/img" \

&& sudo chmod a+x "/usr/local/bin/img"

提供的构建选项看起来像是buildctl的一个子集:

img build --help

Usage: img build [OPTIONS] PATH



Build an image from a Dockerfile.



Flags:



-b, --backend  backend for snapshots ([auto native overlayfs]) (default: auto)

--build-arg    Set build-time variables (default: [])

-d, --debug    enable debug logging (default: false)

-f, --file     Name of the Dockerfile (Default is 'PATH/Dockerfile') (default: )

--label        Set metadata for an image (default: [])

--no-cache     Do not use cache when building the image (default: false)

--no-console   Use non-console progress UI (default: false)

--platform     Set platforms for which the image should be built (default: [])

-s, --state    directory to hold the global state (default: /home/alex/.local/share/img)

-t, --tag      Name and optionally a tag in the 'name:tag' format (default: [])

--target       Set the target build stage to build (default: )

以下是我们需要进行构建时执行的命令:

sudo img build -f ./build/build-test/Dockerfile -t alexellis2/build-test:latest ./build/build-test/

目前由于某种原因, img 实际上无法成功构建。可能是由于某些优化原因在尝试以非root身份执行时导致的。

fatal error: unexpected signal during runtime execution

[signal SIGSEGV: segmentation violation code=0x1 addr=0xe5 pc=0x7f84d067c420]



runtime stack:

runtime.throw(0xfa127f, 0x2a)

/home/travis/.gimme/versions/go1.11.10.linux.amd64/src/runtime/panic.go:608 +0x72

runtime.sigpanic()

/home/travis/.gimme/versions/go1.11.10.linux.amd64/src/runtime/signal_unix.go:374 +0x2f2



goroutine 529 [syscall]:

runtime.cgocall(0xc9d980, 0xc00072d7d8, 0x29)

/home/travis/.gimme/versions/go1.11.10.linux.amd64/src/runtime/cgocall.go:128 +0x5e fp=0xc00072d7a0 sp=0xc00072d768 pc=0x4039ee

os/user._Cfunc_mygetgrgid_r(0x2a, 0xc000232260, 0x7f84a40008c0, 0x400, 0xc0004ba198, 0xc000000000)

Github上似乎有 三个类似的issue 还处于未关闭状态。

使用 Kaniko 构建

Kaniko 是 Google 的容器构建工具,它的目标是沙盒容器构建。你可以把它当成一次性容器使用,也可以用作独立的二进制文件。

在这篇博客文章里 ,我们体验了一下构建过程:

docker run -v $PWD/build/build-test:/workspace \

-v ~/.docker/config.json:/kaniko/config.json \

--env DOCKER_CONFIG=/kaniko \

gcr.io/kaniko-project/executor:latest \

-d alexellis2/build-test:latest
  • -d 标志指定了在构建成功后应当将镜像推送到的位置。
  • -v 标志会把当前目录监听挂载(bind-mount)到 Kaniko 容器里,它还会添加config.json 文件用于推送镜像到一个远端镜像仓库。

Kaniko 在缓存方面提供了一些支持,但是由于 Kaniko 是采用一次性执行方式运行的,而不是像 Buildkit 那样的守护进程,因此我们可能需要手动管理和保存。

对上述方案做一下总结

  • Docker – 传统的构建工具

安装Docker可能会稍显繁重,而且会添加一些超出我们系统需求的功能。这个构建工具是最老的,也是最慢的,但是它可以完成任务。要注意的是docker安装的网桥,它可能会和使用相同私有IP段的其他私有网络冲突。

  • Docker – 和buildkit一起工作

这是在尽量不分裂或者变化最少的情况下的最快选项了。只需要简单地在命令前面加上前缀 DOCKER_BUILDKIT=1 即可启用。

  • 独立运行的 buildkit

这个方案非常适用于集群内的构建,或者说是不需要用到 Docker 的系统(比如一个CI盒或者执行器)。它确实需要一台 Linux 宿主机,而且目前没有什么在 MacOS 使用它的优良经验,也许是通过跑一台额外的虚拟机或者宿主机然后通过 TCP 访问来实现?

我还想在这里附上 [Akihiro Suda]( https://twitter.com/@AkihiroSuda /) 的一次演示,他是来自日本NTT的 buildkit 维护人员。这个演讲已经大概是2年前的事情了,但是它为我们提供了另外一个宏观角度的概述,在2018年 比较下一代容器镜像构建工具里的面貌

对于 faasd 用户 来说这是一个最佳选择了,这些用户仅需要依赖 containerd 和 CNI,而不是 Docker 或者 Kubernetes。

  • Kaniko

我们使用 Kaniko 时始终是需要安装Docker的,但是它其实提供了其他的选项。

结语

你可以继续在 OpenFaaS 里使用常规的容器构建工具,又或者是执行 faas-cli build --shrinkwrap 然后把构建上下文传给你偏好的工具。

下面是一些构建OpenFaaS容器用到的工具的例子:

OpenFaaS 云 ,我们使用 buildkit 守护进程搭配在本文里标明的 shrinkwrap 方案打造了一个完整的无需干涉的CICD体验。对于所有其他用户的话,我将会建议他们使用 Docker,或者带有 buildkit 的 Docker。对于 faasd 的用户,建议使用带守护进程模式的buildkit。

在这篇文章里,确实少了作为重要部分之一的工作流,即部署这块的内容。任何OCI容器 只要符合serverless工作负载的定义 ,就都可以部署到Kubernetes上面的OpenFaaS控制平面。如果想要了解构建,推送和部署这块的完整经验的话,请参阅 OpenFaaS 的研讨会

原文链接: building-containers-without-docker (译者:吴佳兴)