在使用 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 接口操作其他容器。
对于部分需要 containerd 集群,而不改变 CI/CD 业务流程仍使用 Docker 构建镜像一部分的场景,我们可以通过在原有 Pod 上添加 DinD 容器作为 Sidecar 或者使用 DaemonSet 在节点上部署专门用于构建镜像的 Docker 服务。
使用 DinD 作为 Pod 的 Sidecar
如下所示,我们有一个名为 clean-ci 的容器,会该容器添加一个 Sidecar 容器,配合 emptyDir,让 clean-ci 容器可以通过 UNIX Socket 访问 DinD 容器:
- apiVersion: v1
- kind: Pod
- metadata:
- name: clean-ci
- spec:
- containers:
- - name: dind
- image: 'docker:stable-dind'
- command:
- - dockerd
- - --host=unix:///var/run/docker.sock
- - --host=tcp://0.0.0.0:8000
- securityContext:
- privileged: true
- volumeMounts:
- - mountPath: /var/run
- name: cache-dir
- - name: clean-ci
- image: '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/run
- name: cache-dir
- volumes:
- - name: cache-dir
- emptyDir: {}
通过上面添加的 dind 容器来提供 dockerd
服务,然后在业务构建容器中通过 emptyDir{} 来共享 /var/run 目录,业务容器中的 docker 客户端就可以通过 unix:///var/run/docker.sock 来与 dockerd 进行通信。
使用 DaemonSet 在每个 containerd 节点上部署 Docker除了上面的 Sidecar 模式之外,还可以直接在 containerd 集群中通过 DaemonSet 来部署 Docker,然后业务构建的容器就和之前使用的模式一样,直接通过 hostPath 挂载宿主机的 unix:///var/run/docker.sock 文件即可。
使用以下 YAML 部署 DaemonSet。示例如下:
- apiVersion: apps/v1
- kind: DaemonSet
- metadata:
- name: docker-ci
- spec:
- selector:
- matchLabels:
- app: docker-ci
- template:
- metadata:
- labels:
- app: docker-ci
- spec:
- containers:
- - name: docker-ci
- image: 'docker:stable-dind'
- command:
- - dockerd
- - --host=unix:///var/run/docker.sock
- - --host=tcp://0.0.0.0:8000
- securityContext:
- privileged: true
- volumeMounts:
- - mountPath: /var/run
- name: host
- volumes:
- - name: host
- hostPath:
- path: /var/run
上面的 DaemonSet 会在每个节点上运行一个 dockerd 服务,这其实就类似于将以前的 docker 服务放入到了 Kubernetes 集群中进行管理,然后其他的地方和之前没什么区别,甚至都不需要更改以前方式的任何东西。将业务构建 Pod 与 DaemonSet 共享同一个 hostPath,如下所示:
- apiVersion: v1
- kind: Pod
- metadata:
- name: clean-ci
- spec:
- containers:
- - name: clean-ci
- image: '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/run
- name: host
- volumes:
- - name: host
- hostPath:
- path: /var/run
Kaniko
Kaniko 是 Google 开源的一款容器镜像构建工具,可以在容器或 Kubernetes 集群内从 Dockerfile 构建容器镜像,Kaniko 构建容器镜像时并不依赖于 docker daemon,也不需要特权模式,而是完全在用户空间中执行 Dockerfile 中的每条命令,这使得在无法轻松或安全地运行 docker daemon 的环境下构建容器镜像成为了可能。
kaniko
Kaniko 构建容器镜像时,需要使用 Dockerfile、构建上下文、以及构建成功后镜像在仓库中的存放地址。此外 Kaniko 支持多种方式将构建上下文挂载到容器中,比如可以使用本地文件夹、GCS bucket、S3 bucket 等方式,使用 GCS 或者 S3 时需要把上下文压缩为 tar.gz,kaniko 会自行在构建时解压上下文。
Kaniko executor 读取 Dockerfile 后会逐条解析 Dockerfile 内容,一条条执行命令,每一条命令执行完以后会在用户空间下面创建一个 snapshot,并与存储与内存中的上一个状态进行比对,如果有变化,就将新的修改生成一个镜像层添加在基础镜像上,并且将相关的修改信息写入镜像元数据中,等所有命令执行完,kaniko 会将最终镜像推送到指定的远端镜像仓库。。整个过程中,完全不依赖于 docker daemon。
如下所示我们有一个简单的 Dokerfile 示例:
- FROM alpine:latest
- RUN apk add busybox-extras curl
- CMD ["echo","Hello Kaniko"]
然后我们可以启动一个 kaniko 容器去完成上面的镜像构建,当然也可以直接在 Kubernetes 集群中去运行,如下所示新建一个 kaniko 的 Pod 来构建上面的镜像:
- apiVersion: v1
- kind: Pod
- metadata:
- name: kaniko
- spec:
- containers:
- - name: kaniko
- image: gcr.io/kaniko-project/executor:latest
- args: ["--dockerfile=/workspace/Dockerfile",
- "--context=/workspace/",
- "--destination=cnych/kaniko-test:v0.0.1"]
- volumeMounts:
- - name: kaniko-secret
- mountPath: /kaniko/.docker
- - name: dockerfile
- mountPath: /workspace/Dockerfile
- subPath: Dockerfile
- volumes:
- - name: dockerfile
- configMap:
- name: dockerfile
- - name: kaniko-secret
- projected:
- sources:
- - secret:
- name: regcred
- items:
- - key: .dockerconfigjson
- path: config.json
上面的 Pod 执行的 args 参数中,主要就是指定 kaniko 运行时需要的三个参数: Dockerfile、构建上下文以及远端镜像仓库。
推送至指定远端镜像仓库需要 credential 的支持,所以需要将 credential 以 secret 的方式挂载到 /kaniko/.docker/ 这个目录下,文件名称为 config.json,内容如下:
- {
- "auths": {
- "https://index.docker.io/v1/": {
- "auth": "AbcdEdfgEdggds="
- }
- }
- }
其中 auth 的值为: docker_registry_username:docker_registry_password base64 编码过后的值。然后 Dockerfile 通过 Configmap 的形式挂载进去,如果构建上下文中还有其他内容也需要一同挂载进去。
关于 kaniko 的更多使用方式可以参考官方仓库:https://github.com/GoogleContainerTools/kaniko。
Jib
如果你是在 Java 环境下面,还可以使用 Jib 来构建镜像,Jib 也是 Google 开源的,只是是针对 Java 容器镜像构建的工具。
Jib
通过使用 Jib,Java 开发人员可以使用他们熟悉的 Java 工具来构建镜像。Jib 是一个快速而简单的容器镜像构建工具,它负责处理将应用程序打包到容器镜像中所需的所有步骤,它不需要你编写 Dockerfile 或安装 Docker,而且可以直接集成到 Maven 和 Gradle 中,只需要将插件添加到构建中,就可以立即将 Java 应用程序容器化。
Jib 利用了 Docker 镜像的分层机制,将其与构建系统集成,并通过以下方式优化 Java 容器镜像的构建:
- 简单:Jib 使用 Java 开发,并作为 Maven 或 Gradle 的一部分运行。你不需要编写 Dockerfile 或运行 Docker 守护进程,甚至无需创建包含所有依赖的大 JAR 包。因为 Jib 与 Java 构建过程紧密集成,所以它可以访问到打包应用程序所需的所有信息。
- 快速:Jib 利用镜像分层和缓存来实现快速、增量的构建。它读取你的构建配置,将你的应用程序组织到不同的层(依赖项、资源、类)中,并只重新构建和推送发生变更的层。在项目进行快速迭代时,Jib 只将发生变更的层(而不是整个应用程序)推送到镜像仓库来节省宝贵的构建时间。
- 可重现:Jib 支持根据 Maven 和 Gradle 的构建元数据进行声明式的容器镜像构建,因此,只要输入保持不变,就可以通过配置重复创建相同的镜像。
以下示例将使用 Jib 提供的 gradle 插件集成到一个 spring boot 项目的构建中,并展示 Jib 如何简单快速的构建镜像。
首先,在项目的 build.gradle 构建文件中引入 jib 插件:
- buildscript{
- ...
- dependencies {
- ...
- classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.2"
- }
- }
- apply plugin: 'com.google.cloud.tools.jib'
如果需要配置相关参数,可以使用下面的 gradle 配置:
- 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 = false
- workingDirectory = "/app"
- }
- }
然后执行以下命令就可以直接触发构建生成容器镜像了:
- # 构建 jib.to.image 指定的镜像,并且推送至镜像仓库
- $ gradle jib
如果你还想将构建的镜像保存到本地 dockerd,则可以使用下面的命令构建:
- gradle jibDockerBuild
当然还有前文我们介绍的 buildkit 可以用于镜像构建,还有一个经常和 Podman 搭配使用的 Buildah,是一个可以用于构建符合 OCI 标准容器镜像的命令行工具,有了这些工具,在构建容器镜像时已经完全可以脱离 docker daemon 了,而且这些工具都能很好的与 Kubernetes 集成,支持在容器环境下完成构建。