在使用 Docker 的时候一般情况下我们都会直接使用 docker build 来构建镜像,切换到 Containerd 的时候,上节我们也介绍了可以使用nerdctl + buildkit来构建容器镜像,除了这些方式之外,还有其他常见的镜像构建工具吗?
接下来我们就来介绍下在 Containerd 容器运行时下面镜像构建的主要工具和方案。
使用 Docker 做镜像构建服务
在 Kubernetes 集群中,部分 CI/CD 流水线业务可能需要使用 Docker 来提供镜像打包服务。可通过宿主机的 Docker 实现,将 Docker 的 UNIX Socket(/var/run/docker.sock) 通过 hostPath 挂载到 CI/CD 的业务 Pod 中,之后在容器里通过 UNIX Socket 来调用宿主机上的 Docker 进行构建,这个就是之前我们使用较多的 Docker outside of Docker 方案。该方式操作简单,比真正意义上的 Docker in Docker 更节省资源,但该方式可能会遇到以下问题:- 无法运行在 Runtime 是 containerd 的集群中。
- 如果不加以控制,可能会覆盖掉节点上已有的镜像。
- 在需要修改 Docker Daemon 配置文件的情况下,可能会影响到其他业务。
- 在多租户的场景下并不安全,当拥有特权的 Pod 获取到 Docker 的 UNIX Socket 之后,Pod 中的容器不仅可以调用宿主机的 Docker 构建镜像、删除已有镜像或容器,甚至可以通过 docker exec 接口操作其他容器。
使用 DinD 作为 Pod 的 Sidecar
如下所示,我们有一个名为 clean-ci 的容器,会该容器添加一个 Sidecar 容器,配合 emptyDir,让 clean-ci 容器可以通过 UNIX Socket 访问 DinD 容器:通过上面添加的 dind 容器来提供 dockerd 服务,然后在业务构建容器中通过 emptyDir{} 来共享 /var/run 目录,业务容器中的 docker 客户端就可以通过 unix:///var/run/docker.sock 来与 dockerd 进行通信。
apiVersion: v1kind: Podmetadata:name: clean-cispec:containers:- name: dindimage: 'docker:stable-dind'command:- dockerd- --host=unix:///var/run/docker.sock- --host=tcp://0.0.0.0:8000securityContext:privileged: truevolumeMounts:- mountPath: /var/runname: cache-dir- name: clean-ciimage: 'docker:stable'command: ["/bin/sh"]args: ["-c", "docker info >/dev/null 2>&1; while [ $? -ne 0 ] ; do sleep 3; docker info >/dev/null 2>&1; done; docker pull library/busybox:latest; docker save -o busybox-latest.tar library/busybox:latest; docker rmi library/busybox:latest; while true; do sleep 86400; done"]volumeMounts:- mountPath: /var/runname: cache-dirvolumes:- name: cache-diremptyDir: {}
使用 DaemonSet 在每个 containerd 节点上部署 Docker
除了上面的 Sidecar 模式之外,还可以直接在 containerd 集群中通过 DaemonSet 来部署 Docker,然后业务构建的容器就和之前使用的模式一样,直接通过 hostPath 挂载宿主机的 unix:///var/run/docker.sock 文件即可。 使用以下 YAML 部署 DaemonSet。示例如下:上面的 DaemonSet 会在每个节点上运行一个 dockerd 服务,这其实就类似于将以前的 docker 服务放入到了 Kubernetes 集群中进行管理,然后其他的地方和之前没什么区别,甚至都不需要更改以前方式的任何东西。将业务构建 Pod 与 DaemonSet 共享同一个 hostPath,如下所示:
apiVersion: apps/v1kind: DaemonSetmetadata:name: docker-cispec:selector:matchLabels:app: docker-citemplate:metadata:labels:app: docker-cispec:containers:- name: docker-ciimage: 'docker:stable-dind'command:- dockerd- --host=unix:///var/run/docker.sock- --host=tcp://0.0.0.0:8000securityContext:privileged: truevolumeMounts:- mountPath: /var/runname: hostvolumes:- name: hosthostPath:path: /var/run
apiVersion: v1kind: Podmetadata:name: clean-cispec:containers:- name: clean-ciimage: 'docker:stable'command: ["/bin/sh"]args: ["-c", "docker info >/dev/null 2>&1; while [ $? -ne 0 ] ; do sleep 3; docker info >/dev/null 2>&1; done; docker pull library/busybox:latest; docker save -o busybox-latest.tar library/busybox:latest; docker rmi library/busybox:latest; while true; do sleep 86400; done"]volumeMounts:- mountPath: /var/runname: hostvolumes:- name: hosthostPath:path: /var/run
Kaniko
Kaniko 是 Google 开源的一款容器镜像构建工具,可以在容器或 Kubernetes 集群内从 Dockerfile 构建容器镜像,Kaniko 构建容器镜像时并不依赖于 docker daemon,也不需要特权模式,而是完全在用户空间中执行 Dockerfile 中的每条命令,这使得在无法轻松或安全地运行 docker daemon 的环境下构建容器镜像成为了可能。![[Docker] 常用容器镜像构建工具和方案介绍 - 图1](/uploads/projects/seekerzw@yaaygq/47b1ca423401565fc8a9c05017d3faff.png)
然后我们可以启动一个 kaniko 容器去完成上面的镜像构建,当然也可以直接在 Kubernetes 集群中去运行,如下所示新建一个 kaniko 的 Pod 来构建上面的镜像:
FROM alpine:latestRUN apk add busybox-extras curlCMD ["echo","Hello Kaniko"]
上面的 Pod 执行的 args 参数中,主要就是指定 kaniko 运行时需要的三个参数: Dockerfile、构建上下文以及远端镜像仓库。 推送至指定远端镜像仓库需要 credential 的支持,所以需要将 credential 以 secret 的方式挂载到 /kaniko/.docker/ 这个目录下,文件名称为 config.json,内容如下:
apiVersion: v1kind: Podmetadata:name: kanikospec:containers:- name: kanikoimage: gcr.io/kaniko-project/executor:latestargs: ["--dockerfile=/workspace/Dockerfile","--context=/workspace/","--destination=cnych/kaniko-test:v0.0.1"]volumeMounts:- name: kaniko-secretmountPath: /kaniko/.docker- name: dockerfilemountPath: /workspace/DockerfilesubPath: Dockerfilevolumes:- name: dockerfileconfigMap:name: dockerfile- name: kaniko-secretprojected:sources:- secret:name: regcreditems:- key: .dockerconfigjsonpath: config.json
其中 auth 的值为: docker_registry_username:docker_registry_password base64 编码过后的值。然后 Dockerfile 通过 Configmap 的形式挂载进去,如果构建上下文中还有其他内容也需要一同挂载进去。 关于 kaniko 的更多使用方式可以参考官方仓库:https://github.com/GoogleContainerTools/kaniko。
{"auths": {"https://index.docker.io/v1/": {"auth": "AbcdEdfgEdggds="}}}
Jib
如果你是在 Java 环境下面,还可以使用 Jib 来构建镜像,Jib 也是 Google 开源的,只是是针对 Java 容器镜像构建的工具。![[Docker] 常用容器镜像构建工具和方案介绍 - 图2](/uploads/projects/seekerzw@yaaygq/8f56c99062a7b67dde3eae6c26131301.png)
- 简单:Jib 使用 Java 开发,并作为 Maven 或 Gradle 的一部分运行。你不需要编写 Dockerfile 或运行 Docker 守护进程,甚至无需创建包含所有依赖的大 JAR 包。因为 Jib 与 Java 构建过程紧密集成,所以它可以访问到打包应用程序所需的所有信息。
- 快速:Jib 利用镜像分层和缓存来实现快速、增量的构建。它读取你的构建配置,将你的应用程序组织到不同的层(依赖项、资源、类)中,并只重新构建和推送发生变更的层。在项目进行快速迭代时,Jib 只将发生变更的层(而不是整个应用程序)推送到镜像仓库来节省宝贵的构建时间。
- 可重现:Jib 支持根据 Maven 和 Gradle 的构建元数据进行声明式的容器镜像构建,因此,只要输入保持不变,就可以通过配置重复创建相同的镜像。
如果需要配置相关参数,可以使用下面的 gradle 配置:
buildscript{...dependencies {...classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.2"}}apply plugin: 'com.google.cloud.tools.jib'
然后执行以下命令就可以直接触发构建生成容器镜像了:
jib {from {image = 'harbor.k8s.local/library/base:1.0'auth {username = '********'password = '********'}}to {image = 'harbor.k8s.local/library/xxapp:1.0'auth {username = '********'password = '********'}}container {jvmFlags = ['-Djava.security.egd=file:/dev/./urandom']ports = ['8080']useCurrentTimestamp = falseworkingDirectory = "/app"}}
如果你还想将构建的镜像保存到本地 dockerd,则可以使用下面的命令构建:
# 构建 jib.to.image 指定的镜像,并且推送至镜像仓库$ gradle jib
当然还有前文我们介绍的 buildkit 可以用于镜像构建,还有一个经常和 Podman 搭配使用的 Buildah,是一个可以用于构建符合 OCI 标准容器镜像的命令行工具,有了这些工具,在构建容器镜像时已经完全可以脱离 docker daemon 了,而且这些工具都能很好的与 Kubernetes 集成,支持在容器环境下完成构建。
gradle jibDockerBuild
