Docker并不是唯一的容器化工具,可能还有更好的选择……
在容器的早期时代(其实更像是4年前),Docker是容器游戏中唯一的玩家。但现在情况已经不一样了,Docker不再是唯一的一个,而只是其中一个容器引擎而已。Docker允许我们构建、运行、拉、推或检查容器镜像,然而对于每一项任务,都有其他的替代工具,甚至可能比Docker做得还要好。所以,让我们探索一下,然后再卸载(只是可能),直至完全忘记Docker……
那,为什么不再用Docker了?
如果你已经使用Docker很长时间了,估计要真正说服你去考虑其他工具,得先提供些依据。
首先,Docker是一个单体工具。它尝试去涵盖所有的功能,通常这并不是最佳实践。大多数情况下,我们都是只选择一种专门的工具,它只做一件事,并且做得非常好,非常精。
如果害怕切换到不同的工具集是因为将不得不学习使用不同的CLI、API或者说不同的概念,那么这不会是一个问题。本文中展示的任何工具都可以是完全无缝的,因为它们(包括Docker)都遵循OCI (Open Container Initiative)下的相同规范。它们包含了容器运行时、容器分发和容器镜像的规范,其中涵盖了使用容器所需的所有特性。
有了OCI,你可以选择一套最符合你需求的工具,同时你仍然可以享受跟Docker一样使用相同的API和CLI命令。
所以,如果你愿意尝试新的工具,那么让我们比较一下Docker和它的竞争对手的优缺点和特性,看看是否有必要考虑放弃Docker,使用一些新的闪亮的工具。
容器引擎
在比较Docker和其他工具时,我们需要将其分解为组件,首先我们先讨论一下容器引擎。Container Engine是一种工具,它为处理镜像和容器提供用户界面,这样就不必处理SECCOMP规则或SELinux策略之类的事情。它的工作还包括从远程仓库提取镜像并将其扩展到磁盘。它看起来也是运行容器,但实际上它的工作是创建容器清单和带有镜像层的目录。然后它将它们传递到容器运行时,如runC或Crun(稍后我们将讨论这一点)。
目前已经有许多容器引擎,但Docker最突出的竞争对手是由红帽开发的Podman。与Docker不同,Podman不需要Daemon来运行,也不需要root特权,这是Docker长期以来一直关注的问题。基于它的名字,Podman不仅可以运行容器,还可以运行pods。如果你不熟悉pods的概念,其实,简单的概括就是,Pod是Kubernetes的最小计算单元。它由一个或多个容器(主容器和执行支持任务的Sidecar)组成,这使得Podman用户以后更容易将他们的工作负载迁移到Kubernetes。因此,作为一个简单的演示,这是如何在一个Pod中运行两个容器:
~ \$ podman pod create --name mypod
\~ \$ podman pod list
POD ID NAME STATUS CREATED # OF CONTAINERS INFRA ID
211eaecd307b mypod Running 2 minutes ago 1 a901868616a5
\~ \$ podman run -d --pod mypod nginx # First container
\~ \$ podman run -d --pod mypod nginx # Second container
\~ \$ podman ps -a --pod
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES POD POD NAME
3b27d9eaa35c docker.io/library/nginx:latest nginx -g daemon o... 2 seconds ago Up 1 second ago brave\_ritchie 211eaecd307b mypod
d638ac011412 docker.io/library/nginx:latest nginx -g daemon o... 5 minutes ago Up 5 minutes ago cool\_albattani 211eaecd307b mypod
a901868616a5 k8s.gcr.io/pause:3.2 6 minutes ago Up 5 minutes ago 211eaecd307b-infra 211eaecd307b mypod
最后,Podman提供了与Docker完全相同的CLI命令,因此只需执行alias docker = podman并装作什么都没有改变。
除了Docker和Podman之外,还有其他的容器引擎,但我个人认为它们都是没什么出路的技术,或者都不太适合本地开发和使用。但是,要全面了解,至少要看一下其中的内容:
- LXD——LXC (Linux Containers)是一个容器管理器(守护进程)。该工具提供了运行系统容器的能力,这些系统容器提供了更类似于VM的容器环境。它位于非常狭窄的空间,没什么用户,所以除非你有非常具体的实例,否则最好还是使用Docker或Podman。
- CRI-O——当你Google什么是CRI-O你可能会发现它被描述为容器引擎。不过,实际上它只是容器运行时。其实它既不是引擎,也不适合“正常”使用。我的意思是,它是专门为Kubernetes运行时(CRI)而构建的,而不是为最终用户使用的。
- Rkt——rkt(“Rocket”)是由CoreOS开发的容器引擎。这里提到这个项目只是为了完整性,因为这个项目已经结束,开发也停止了——所以也就没必要再使用了。
构建镜像
对于容器引擎来说,一般都只选择Docker。但是,当涉及到构建镜像时,选择的余地还是比较多的。
首先,介绍一下Buildah。Buildah是红帽开发的另一个工具,它与Podman配合使用相当合适。如果已经安装了Podman,你可能会注意到podman build子命令,它实际上只是伪装的Buildah,因为它的二进制文件已经包含在Podman里。
至于它的特性,它遵循了与Podman相同的路线——无守护程序和无根的,并遵循OCI的镜像标准,所以它能保证所构建的镜像和Docker构建的是一样的。它还能够从Dockerfile或更恰当的命名Containerfile来构建镜像,Dockerfile和Containerfile都是相同的,只是命名的区别。除此之外,Buildah还对镜像层提供了更精细的控制,允许在单层中提交更多变更。唯一的例外是(在我看来)与Docker的区别是,由Buildah构建的镜像是基于用户的,因此用户可以只列出自己构建的镜像。
那么,考虑到Buildah已经包含在Podman CLI中,大家可能会问,为什么还要使用单独的Buildah CLI?Buildah CLI是podman build中包含的命令的超集,所以基本不需要单独接触Buildah CLI,但是通过使用它,你可能还会发现一些额外有用的特性(有关podman build和buildah之间的差异的细节,请参阅这个文章[1])。
现在,我们来看看一个演示:
~ \$ buildah bud -f Dockerfile .
\~ \$ buildah from alpine:latest # Create starting container - equivalent to "FROM alpine:latest"
Getting image source signatures
Copying blob df20fa9351a1 done
Copying config a24bb40132 done
Writing manifest to image destination
Storing signatures
alpine-working-container # Name of the temporary container
\~ \$ buildah run alpine-working-container -- apk add --update --no-cache python3 # equivalent to "RUN apk add --update --no-cache python3"
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86\_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86\_64/APKINDEX.tar.gz
...
\~ \$ buildah commit alpine-working-container my-final-image # Create final image
Getting image source signatures
Copying blob 50644c29ef5a skipped: already exists
Copying blob 362b9ae56246 done
Copying config 1ff90ec2e2 done
Writing manifest to image destination
Storing signatures
1ff90ec2e26e7c0a6b45b2c62901956d0eda138fa6093d8cbb29a88f6b95124c
\~ # buildah images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/my-final-image latest 1ff90ec2e26e 22 seconds ago 51.4 MB
从上面的脚本可以看到,我们可以只用buildah bud构建镜像,bud代表使用Dockerfile构建,但是你还可以使用更多Buildahs的脚本:from,run和copy,这些命令对应命令Dockerfile的(FROM image,RUN…,COPY…)。
下一个是谷歌的Kaniko。Kaniko也是从Dockerfile构建容器镜像,跟Buildah类似,也不需要守护进程。与Buildah的主要区别在于,Kaniko更专注于在Kubernetes中构建镜像。
Kaniko使用gcr.io/ Kaniko -project/executor作为镜像运行。这对于Kubernetes来说是行得通的,但是对于本地构建来说不是很方便,并且在某种程度上违背了它的初衷,因为我们得先使用Docker来运行Kaniko镜像,然后再去构建镜像。也就是说,如果正在为Kubernetes集群中构建镜像的工具进行选型(例如在CI/CD Pipeline中),那么Kaniko可能是一个不错的选择,因为它是无守护程序的,而且(可能)更安全。
从我个人的经验来看——我在Kubernetes/OpenShift集群中使用了Kaniko和Buildah来构建镜像,我认为两者都能很好地完成任务,但在使用Kaniko时,我看到了一些将镜像导入仓库时的,会有随机构建崩溃和失败的情况。
第三个竞争者是Buildkit,也可以称为下一代的Docker build。它是Moby项目的一部分。在Docker里可以使用DOCKER_BUILDKIT=1 Docker build…作为实验特性进行启用。那么,它的核心价值到底有哪些?它引入了许多改进和炫酷的特性,包括并行构建、跳过未使用的阶段、更好的增量构建和无根构建。然而另一方面,它仍然需要运行守护进程(buildkitd)才能运行。所以,如果你不想摆脱Docker,但是想要一些新的特性和更好的改进,那么使用Buildkit可能是最好的选择。
和前面一样,这里我们也还有一些“光鲜亮丽的产品”,它们也都有非常具体的场景,虽然并不是我们的首选:
- Source-To-Image(S2I)是一个不需要Dockerfile直接从源代码构建镜像的工具包。这个工具在简单的、预期的场景和工作流中运行的很好,但是如果有太多的定制,或者该项目没有预期的布局,你很快就会觉得这个工具很烦人和笨拙。如果你对Docker还不是很有信心,或者如果在OpenShift集群上构建镜像,那么你可以尝试考虑一下使用S2I,因为使用S2I构建是一个内置特性。
- Jib是谷歌的另一个工具,专门用于构建Java镜像。它包括Maven和Gradle插件,可以轻松地构建镜像,而不会干扰Dockerfile。
- 最后一个但并不是不重要的是Bazel,它是谷歌的另一款工具。它不仅用于构建容器镜像,而且是一个完整的构建系统。如果你只是想构建一个镜像,那么钻研Bazel可能有点过头,但绝对是一个很好的学习体验,所以如果你想尝试,rules_docker绝对是一个很好的起点。
容器运行时
最后一个大块儿是容器运行时,它负责运行容器。容器运行时是整个容器生命周期/栈的一部分,除非你对速度、安全性等有一些非常具体的要求,否则一般是不需要对其进行干扰。所以,如果读者看到这里已经厌倦,那么可以跳过这一部分。如果不是,那么有关容器运行时的选择,如下:
runC是基于OCI容器运行时规范创建的,且最流行的容器运行时。Docker(通过containerd)、Podman和crio使用它,所以几乎所有东西都依赖于LXD。它几乎是所有产品/工具的默认首选项,所以即使你在阅读本文后放弃Docker,但你仍然会用到runC。
runC的另一款替代方产品为Crun,名称类似(容易混淆)。这是Red Hat开发的工具,完全用C编写(runC是用Go编写的)。这使得它比runC更快,内存效率更高。考虑到它也是OCI兼容的运行时。所以,如果你想做个测试,切换起来很容易。尽管它现在还不是很流行,但在RHEL 8.3技术预览版中,它将作为一个替代OCI运行时,同时,考虑到它是红帽的产品,我们可能最终会看到它会成为Podman或CRI-O的默认首选项。
说到CRI-O。前面我说过,CRI-O实际上不是一个容器引擎,而是容器运行时。这是因为CRI-O不包括比如推送镜像这样的特性,而这正是容器引擎的特性。作为运行时的CRI-O在内部使用runC运行容器。通常情况下不需要在单机尝试这个工具,因为它被构建为用于Kubernetes节点上的运行时,可以看到它被描述为“Kubernetes需要的所有运行时,仅此而已”。因此,除非你正在设置Kubernetes集群(或OpenShift集群——CRI-O已经是默认首选项了),否则不大可能会接触到这个。
本节的最后一个内容是containerd,它是CNCF的一个毕业的项目。它是一个守护进程,充当各种容器运行时和操作系统的API。在后台,它依赖于runC,是Docker引擎的默认运行时。谷歌Kubernetes引擎(GKE)和IBM Kubernetes服务(IKS)也在使用。它是Kubernetes容器运行时接口的一个部署(与CRI-O相同),因此它是Kubernetes集群运行时的一个很好的备选项。
镜像检测与分发
容器栈的最后一部分是镜像的检测与分发。这有效地替代了docker inspect,还(可选地)增加了远程镜像仓库之间复制/映射镜像的能力。
这里唯一要提到的可以完成这些任务的工具是Skopeo。它由红帽公司开发,是Buildah,Podman和CRI-O的配套工具。除了我们都从Docker中知道的基本的skopeo inspect之外,Skopeo还能够使用skopeo copy复制镜像,它允许你在远程镜像仓库之间映射镜像,而无需先将它们拉到本地仓库。如果你使用本地仓库,此功能也可以作为pull/push。
另外,我还想提一下Dive,这是一个检查、探测和分析镜像的工具。它对用户更友好一些,提供了更可读的输出,可以更深入地探测镜像,并分析和衡量其效率。它也适合在CI管道中使用,它可以测量你的镜像是否“足够高效”,或者换句话说——它是否浪费了太多空间。
结论
本文的目的并不是要说服大家完全抛弃Docker,而是向大家展示构建、运行、管理和分发容器及其镜像的整个场景和所有选项。包括Docker在内的每一种工具都有其优缺点,评估哪一组工具最适合你的工作流和场景才是最重要的,真心希望本文能在这方面帮助到你。