无需 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 是用来推送上传我们 OCI 镜像的 Docker Hub 用户名


我们将可以得到如下结果:
./
├── 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 的方式将它推送上传到一个镜像仓库。
1.png

使用 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 守护进程代理的。
2.png

使用独立运行的 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|<id>[=<socket>|<key>[,<key>]]

如下命令和 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动画。
3.png

使用 img 和 buildkit 来构建

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

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

对于 x86_64 来说,目前最新版本是 2019 年 5 月 7 日发布的 v0.5.7,该版本使用 Go 1.11 构建,而当前版本是 Go 1.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: <none>)
--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: <none>)

以下是我们需要进行构建时执行的命令:
sudo img build -f ./build/build-test/Dockerfile -t alexellis2/build-test:latest ./build/build-test/

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

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 文件用于推送镜像到一个远端镜像仓库。


5.png

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 方案打造了一个完整的无需干涉的 CI/CD 体验。对于所有其他用户的话,我将会建议他们使用 Docker,或者带有 buildkit 的 Docker。对于 faasd 的用户,建议使用带守护进程模式的 buildkit。

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

原文链接:Building containers without Docker(译者:吴佳兴)

0 个评论

要回复文章请先登录注册