[编者的话] 本文介绍了除了 Docker 以外构建容器镜像的各种开源方案和它们各自的优缺点.
在这篇文章里, 我将会介绍几种无需用到 Docker 本身即可构建容器的方法. 我将会使用 OpenFaaS https://github.com/openfaas/ 作为案例研究, 它使用了 OCI 格式的容器镜像作为它的工作负载. 简单来说, 我们可以把 OpenFaaS 看作是一个面向 Kubernetes https://kubernetes.io/ 的 CaaS 平台, 我们可以在上面运行微服务, 而且它为我们带来了免费的 FaaS 以及由事件驱动的工具.
另请参阅 OpenFaaS.com https://openfaas.com/
文章里的第一个方案将会展示如何使用 Docker 命令行里提供的内置的 buildkit 选项, 随后会介绍一下独立运行的(只支持 Linux) https://github.com/moby/buildkit , 其次就是 Google 的容器构建工具, Kaniko https://github.com/GoogleContainerTools/kaniko .
注意: 这篇文章涵盖的是能够基于一份 Dockerfile 即可构建产出一个镜像的相关工具, 因此任何比如限制用户只能使用 Java 或者 Go 的情况不在本文讨论范围.
然后我将会做一个总结, 让你知道如何得到一些建议, 反馈, 以及自己关于容器工具周边的一些想法和需求的用户故事.
那么, Docker 有什么问题呢?
其实也没啥好说的, Docker 在 armhf , arm64 还有 x86_64 上都运行地很好. Docker 命令行的主要功能已经不再是构建 / 装载 / 运行了, 还包括了拖延了数年之久的沉重负担, 现在它把 Docker Swarm 还有 Docker EE 的一些功能都捆绑到了一些.
有些人做过一些努力, 试图将 "docker" 剥离出来, 回归其原本的组件部分, 我们都爱上了那个最初的 UX:
Docker https://github.com/docker/docker - docker 本身现在是使用 containerd 来运行容器, 而且已经支持通过启用 buildkit 来实施更高效地, 缓存式的构建任务.
Podman https://podman.io/ 和 https://github.com/containers/buildah 的结合 - 这是 RedHat 和 IBM 他们在做的尝试, 使用他们自己的 OSS 工具链来生成 OCI 镜像. Podman 标榜的是无守护进程和去 root, 但是始终需要挂载 overlay 文件系统以及使用 Unix 套接字.
https://github.com/alibaba/pouch - Pouch 来自阿里巴巴, 它被称为 "一个高效的企业级容器引擎". 它同 Docker 一样使用的是 containerd, 而且同时支持 https://github.com/opencontainers/runc 带来的容器级别的隔离, 以及像 https://github.com/hyperhq/runv 这样 "轻量级虚拟机". 此外, 它还把更多的 精力放在了镜像分发以及强隔离方面 .
独立的 buildkit - buildkit 是由 Docker 公司的 Tõnis Tiigi https://twitter.com/tonistiigi?lang=en 发起的, 它是一款全新的兼顾了缓存和并发能力的容器构建工具. buildkit 目前只支持作为守护进程运行, 但是你将会从人们那里听到完全相反的说辞. 它们会 fork 该守护进程然后在一次构建结束后干掉它.
img - img 是一款由 Jess Frazelle https://github.com/jessfraz 编写的工具, 经常在这些指导手册里被引用, 而且它是 buildkit 的一次重新包装. 也就是说, 和上述提到的其他方案相比, 我并没有看到它有啥特别吸引人的地方. 该项目 一直活跃到 2018 年底, 此后仅收到了一些补丁 https://github.com/genuinetools/img/commits/master .img 声称它是无守护进程的, 但是它用到了 buildkit, 因此这里面可能有一些黑科技. 我听说 img 提供了比 buildkit 本身的命令行 buildctr 更棒的 UX, 但是也应该注意的是, img 只发布了 x86_64 下的版本, 而没有针对 armhf / arm64 的二进制文件.
img 的一个替代方案可能会是 k3c , 它也引入了一个运行时组件, 并且计划加入对 ARM 架构的支持.
k3c - 这是一个 Rancher 最近的实验项目, 它借助 containerd 和 buildkit 重新还原了最初的 Docker 版本所具备的原始而又经典的, 香草一样精巧的用户体验.
以上所有方案里, 我认为我最喜欢的是 k3c, 但是它还非常稚嫩, 而且因为把所有东西都打包到了一个二进制文件里, 这很可能造成和其他软件存在冲突, 目前它运行它自己内嵌的 containerd 和 buildkit 执行文件.
注意: 如果你是 RedHat 的客户, 并且购买了支持服务的话, 那么你确实应该物尽其用, 使用他们一整套的工具链. 我查看了一些示例, 并且看到了一个用到了我那篇 "经典的" 多阶段构建的博客文章. 你可以比较一下这两个例子, 看看自己更喜欢 还是 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.YAML
1 directory, 2 files
handler 看上去像下面这样, 而且改起来也方便. 可以通过 vendor 或者 Go modules https://blog.golang.org/using-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.YAML
./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.YAML 的方式将它推送上传到一个镜像仓库.
使用 Docker 和 BuildKit 构建
要做的改动再简单不过了, 而且我们也可以得到一个更快的构建.
DOCKER_BUILDKIT=1 faas-cli build -f build-test.YAML
我们将可以看到, 使用这个方案的情况下, 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.YAML --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.YAML
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 动画.
使用 img 和 buildkit 来构建
由于我从未使用过 img , 也没有真正意义上听闻过有哪个团队经常使用, 而对于更常见的选项我想我会试一试.
我的第一印象是, 多体系结构不是它优先考虑的问题, 而且鉴于该项目的年代, 它也不太可能上岸. 它没有提供适用于 armhf 或者 ARM64 架构下的二进制文件.
- 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"
- 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>)
- 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)
- 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
- Google Cloud Build https://www.openfaas.com/blog/openfaas-cloudrun/
- GitHub Actions
- Jenkins https://docs.openfaas.com/reference/cicd/jenkins/ and
- GitLab CI https://docs.openfaas.com/reference/cicd/gitlab/
来源: http://www.tuicool.com/articles/NjyaUv7