未来我们如何构建容器镜像?


自2014年夏天Docker 1.0版本发布以来的这些年,看似是一瞬间,又似乎是永久。在随后的4-5年里,云原生范例快速席卷整个产业,无论大小企业都笃信云原生所带来的好处对企业生存至关重要。

这带来了一个结果那就是容器抽象的普及,它让开发者可以将其应用构建成自给自足且不变的软件包——容器镜像。然而,尽管云原生技术领域的发展步伐迅猛,但自2014年夏季以来,我们定义容器镜像的方式以及将其构建为可用程序的方式几乎没有发生太大变化。随着容器技术的发展和成熟,容器镜像构建过程中的缺陷和局限性逐渐显现,这给云原生社区带来了挫败感。

在过去的一两年中,出现了许多不同的项目来尝试解决部分或全部这些缺陷。这一系列名为“容器镜像构建的最新技术”的文章中,我们重点关注某些项目和工具,看看他们如何解决已知问题。

在这之前,我们先要了解镜像创建的基本知识还有那些我们需要解决的缺陷。

Dockerfile和API构建端点

过去,容器镜像用Dockerfile来定义,通过一系列声明性指令集来生成文件系统内容和元数据,并通知容器运行时引擎运行容器。随后,容器镜像通过Docker引擎的API终端节点来创建,这种方式通过顺序的执行每一个Dockerfile的指令来创建内容,或者记录与被构建的镜像相关的元数据。但无论如何,构建终端节点总是通过客户端的docker build命令来实现。

然后,编写容器镜像要求我们通过使用一组适当的Dockerfile指令及其关联参数来定义镜像,然后使用Docker Engine API构建镜像。这很简单,但可曾想过这样做是否问题?

对于Docker Daemon的依赖

一个经常出现的问题是:要想创建容器镜像需要有Docker引擎API,而Docker引擎API需要Docker Daemon支持。Docker Daemon包含许多功能,但这些功能大多和构建镜像无关,同时,这些功能需要root权限才能运行,我们随后会讲到,这会带来安全隐患。如果当下的任务只是创建容器镜像,那么启用Docker Daemon无疑是笨拙且低效的,这也并非是CI/CD pipeline的最佳选择。这里有个争议点:创建容器镜像不需要Docker Daemon。

无效缓存

Docker引擎从一开始就有缓存功能,这意味着,如果一个构建和下一次构建执行Dockerfile运行了相同的命令,或者将相同的内容添加到镜像,系统使用缓存的内容而不是重新创建它。这极大地加速了镜像的构建,并使整个过程更加高效。

不幸的是,由于构建API的顺序性质,一旦由于内容或Dockerfile指令的更改而使缓存无效,Dockerfile中的每个后续指令都会再次执行。无论这些后续指令是否已更改,都会发生这种情况。这意味着需要特别注意指令的顺序,以最大化构建缓存的有效性。

顺序执行

向我们之前说的,低效的缓存利用源自于镜像构建过程中Dockerfile指令的顺序执行。如果指令顺序被审慎的考虑,那么指令的执行顺序将不再会是问题。然而当利用API进行多阶段构建时,顺序执行这一特性将会阻碍其所带来的好处;并行执行的指令彼此没有依赖性。这当然也取决于构建的时间,在某些情况下构建时间会显著减少,在应用程序开发期间迭代镜像构建时,该过程可能会持续很长时间。

挂载临时内容

通常,构建容器镜像的过程依赖临时提供的内容或从中受益。换而言之,我们希望在构建过程中获得内容,但我们不希望最终产品中包含内容,因为这样会不必要地增加镜像的大小。例如,我们需要源代码来构建二进制文件,但最终镜像中不需要源代码。我们可以借助编译器构建缓存来加快构建速度,但是我们不希望缓存内容出现在镜像中。

多阶段构建可以帮助我们解决此问题,但更优雅的解决方案是将内容临时挂载在构建过程中所需的位置。尽管添加此功能的请求有很多,但Dockerfile指令语法或docker build命令的命令行参数中没有这个功能。

创建密钥

有时我们需要使用密钥来访问需要进行客户端身份验证和授权的内容。比如,我们可能需要提供密钥才能从GitHub或者类似的网站下载资料,然后我们就可以在构建镜像时用到这些资料。但这可能会导致密钥在构建过程中被复制到镜像中,密钥嵌入了镜像会导致风险,密钥会被别有心机的人利用。我们甚至可能很想使用环境变量,但是这种方法也容易成为被利用的漏洞。

关于在镜像构建过程中如何处理密钥已经很多方案,但是没有一个方案可以将其与API结合。如果这个功能能够出现在最佳实现指南中,将是众多镜像构建者的福音。它给容器镜像构建者带来了很大的挑战,也需要一些新技术来实现变通方案。

创建特权账号

Docker Daemon要求客户端具有root访问权限或作为Docker组成员,才能与API及其构建终端节点进行交互。但这通常不是必须的,这甚至在某些对访问特权帐户具有严格策略的组织中甚至被禁止。实际中,这使得构建容器镜像的任务非常困难。

当我们在以Docker作为容器运行时的Kubernetes集群中构建镜像或者运行CICD流水线的某部分时,也会遇到这个问题。为了能让Docker引擎API运行在Kubernetes节点中,我们不得不将守护进程的套接字安装到“客户端”容器中,以使其可用于构建。从安全的角度看,这很危险,因为这实际上赋予容器访问Kubernetes节点的root权限。另一个解决方案是避免在节点上运行Docker Daemon,而是使用Docker in Docker(DinD 在容器中运行容器)来提供自包含的镜像构建环境。但是这也有安全隐患,因为运行DinD的容器需要拥有特权。

实现“无root权限“构建一直是社区的追求。

结论

尽管有诸多限制,但使用Docker的build API和Dockerfile仍然是构建容器镜像的主要方法。Docker Hub和Quay等公共镜像仓库中托管的成千上万个镜像及其相关的Dockerfile就是最好的佐证。同时这些局限也为那些致力于解决这问题和增强容器镜像创建体验的新技术和工具的出现提供了机会。这些工具是开源的并得到社区的支持,在功能上经常会有重复。为了更好地了解容器图像构建的最新技术,本系列的下一篇文章将一探究竟。

原文链接:What is the Future of Container Image Building?(翻译:吴世曦)
已邀请:

要回复问题请先登录注册