在使用 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 的环境下构建容器镜像成为了可能。



dockerFile 镜像制作 docker镜像制作工具_kubernetes

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 容器镜像构建的工具。



dockerFile 镜像制作 docker镜像制作工具_dockerFile 镜像制作_02

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 集成,支持在容器环境下完成构建。