关于在容器中构建镜像

1,254 阅读5分钟

工作中遇到了构建镜像相关的内容,对涉及到的知识点做个梳理。

构建容器镜像

我们都知道通过dockerfile可以很快捷的构建容器镜像,他的原理是通过一系列的声明式指令集来生成文件系统和镜像的元数据。通过docker build命令来构建镜像需要docker引擎的api,他的底层是由docker daemon来支持的。在我们的业务场景中需要在kubernetes环境中制作镜像,所以和往常遇到的在主机上构建镜像遇到的问题不一样,这里后文会讲如何在kubernetes环境中制作镜像。

docker build只是docker daemon提供的众多功能中的一个,而且使用这个功能需要宿主机的root权限,这就涉及到了安全隐患和权限问题。如果业务中只是需要构建镜像,就必须要求构建镜像的宿主机具有docker daemon,并且构建的触发着还必须具有主机的root权限,这是冗余且不优雅的,而且也不是优秀的CI/CD流程中应该有的。我们只是需要docker daemon其中的一小个功能,仅此而已。

而且在我们的业务中也遇到了需要在流程中构建镜像,为了能让Docker build运行在Kubernetes节点中,我们不得不将docker daemon的socket安装到业务流程的构建容器中,以使其可用于构建。

docker run -it -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/kaniko:/tmp/kaniko docker

从安全的角度看,这是不合适的,因为这实际上赋予构建容器访问Kubernetes节点的root权限。另一个解决方案是避免在节点上运行Docker Daemon,而是使用Docker in Docker(DinD 在容器中运行容器)来提供自包含的镜像构建环境。但是这也有安全隐患,因为运行DinD的容器需要拥有特权。而且这种方式也有其他的问题,比如使用Docker in Docker时,每个构建作业都在没有历史痕迹的干净环境中。并发作业可以正常工作,因为每个构建都拥有自己的Docker引擎实例,因此它们不会互相冲突。但这也意味着作业会变慢,因为没有层缓存。还有因为overlay2网络导致的的docker版本问题。在下文我们会提到两种方法来解决这个问题。

方法一:构建镜像的新方法podman

podman来源于在工作流中剔除docker daemon的诉求。podman模拟了docker build的所有构建语句,使用unix经典的fork,exec模式来执行构建,这样就真正意义上的实现了多进程构建,可谓是十分优雅了。而且podman还为兼容了oci的实现提供了构建功能。另外podman是无root权限构建的,底层原理是podman使用的是被隔离的用户命名空间。这种被隔离的命名空间将默认命名空间中的一系列非特权用户和组的ID映射到了新创建的命名空间中的一组用户和组的ID上,这时映射到新的容器中的用户和组ID都是0,从而为新建容器提供了需要的特权,因为存在隶属关系,所以在新容器中的特权只有原始映射用户的文件访问权限。

方法二:在容器中构建镜像kaniko

kaniko是google推荐的在容器中构建镜像的方案,也是我们用在生产上的方案。Kaniko 并不依赖于Docker daemon进程,完全是在用户空间根据Dockerfile的内容逐行执行命令来构建镜像,这就使得在一些无法获取docker daemon进程的环境下也能够构建镜像,比如在标准的Kubernetes Cluster上。 Kaniko 以容器镜像的方式来运行的,同时需要三个参数: Dockerfile,上下文,以及远端镜像仓库的地址。可以从下面的语句中看到入参中有dockerfile,上下文和destination

cmd = exec.Command("/kaniko/executor", dockerfile, buildContext, destination, "--insecure-pull", "--skip-tls-verify")

Kaniko会先提取基础镜像(Dockerfile FROM 之后的镜像)的文件系统,然后根据Dockerfile中所描述的,一条条执行命令,每一条命令执行完以后会在用户空间下面创建一个snapshot,并与存储与内存中的上一个状态进行比对,如果有变化,就将新的修改生成一个镜像层添加在基础镜像上,并且将相关的修改信息写入镜像元数据中。等所有命令执行完,kaniko会将最终镜像推送到指定的远端镜像仓库。

详细的使用方法:Kaniko

dockerfile的构建顺序和缓存

因为dockerfile的构建是有顺序的,而且针对指令顺序执行。由 Dockerfile 最终构建出来的镜像是在基础镜像之上一层层叠加而成的,因此在过程中会产生一个个新的镜像层。Docker daemon 在构建镜像的过程中会缓存一系列中间镜像。缓存镜像如何生效?简而言之就是在执行dockerfile指令时,docker daemon会去比较所有的子镜像,若发现有子镜像是相同的指令构建而成的,那可以复用这个缓存的镜像。为了提高构建速度,在精确控制dockerfile指令的前提下,并行执行非依赖性的指令可以提高构建速度。