11

无需 Docker 也能构建容器

 4 years ago
source link: http://dockone.io/article/10079
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【编者的话】本文介绍了除了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|<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架构下的二进制文件。

对于 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: <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身份执行时导致的。

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 (译者:吴佳兴)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK