本节一起看一下都有哪些可用于构建容器镜像的工具和方案。可能你会疑问,构建镜像不就是使用docker build就可以吗,即使将来docker真的退出历史舞台,不还有containerd吗,前面第5节的时候也介绍了在containerd下可以使用nerdctl+buildkit构建容器镜像。 如果你只是在单机本地构建镜像的话,使用docker buildnerdctl+containerd+buildkit确实没有问题,但实际中由于不同场景和技术背景下的限制,如docker build依赖于docker daemon,这些限制使得在一些特殊的场景下本机构建镜像的方式不再可用。

下图是一个一个CI/CD流水线中的分布式构建架构,结合了容器技术,将Jenkins用于构建的Slave节点放到容器中,并基于K8S来动态调度管理这些Jenkins Slave容器。这样一来Jenkins就具备了基于容器的分布式构建能力, 一个应用从源码经过各个阶段到最终的镜像是采用分布式构建实现的,各个阶段也都是在容器中进行的。

jenkins-slaves-in-k8s.png

上面基于容器的分布式构建,首先要面对的问题就是要在jenkins slave容器的内部构建镜像。由于docker是以docker cli和docker daemon的C/S形式工作,如果把docker cli放到jenkins slave容器里面执行docker build,那么docker build所需的docker daemon放哪儿呢? 于是就有了Docker in DockerDocker outside of Docker两种方案。

Docker in Docker

Docker in Docker简称DinD。DinD模式是指在一个容器内部安装完整的docker服务,启动一个Docker Daemon,然后就可以在容器内部使用docker cli进行镜像的构建。 此方案的优点是容器中的Docker Daemon与外部完全隔离,隔离性较好,而缺点是容器内部比较臃肿复杂,同时要解决容器内docker服务的持久化存储、构建缓存的问题,在安全上DinD还需要以特权(--privileged)形式启动容器,有安全风险。 因此这种方案在实际好少使用,也不推荐使用。

Docker outside of Docker

Docker outside of Docker简称DooD。DooD模式是指将容器外部宿主机上的docker daemon的socket挂载到容器内,让容器内的docker cli误认为本地启动了docker daemon,这样进行docker build等命令操作时由容器外部宿主机上的docker daemon处理请求。 此方案的优点是容器内部不需要安装docker daemon,构建在宿主机上进行效率较高,缺点是没有与外部隔离,构建的镜像与宿主机上的镜像存在冲突的可能,同一宿主机上不支持并行构建。 在早些年前使用DooD这种方案的人比较多。

Kaniko

Kaniko是Google开源的一款容器镜像构建工具,可以用来在容器中或者Kubernetes集群中进行镜像的构建。 在Kaniko出现之前,使用Dockerfile和Docker cli来构建镜像时,需要将构建的上下文发送至docker daemon,Docker in Docker和Docker outside of Docker也不例外。 Kaniko构建容器镜像时并不依赖于Docker daemon,也不需要特权权限,这一特性使得kaniko成为DinD或者DooD之外的一种全新的解决方案。

Kaniko构建容器镜像时,需要三个输入: Dockerfile,构建上下文,以及构建成功后镜像在仓库中的存放地址。 kaniko executor需要在容器执行,前面三个输入可以挂载到kaniko executor的容器中。

Kaniko支持多种方式将构建上下文挂载到容器中: 可以使用本地文件夹,GCS bucket,S3 bucket等等方式,使用GCS 或者 S3时需要把上下文压缩为tar.gz,kaniko会自行在构建时解压上下文。

Kaniko executor在找到Dockerfile后会逐条解析Dockerfile内容,并且执行相关的命令。在用户目录中形成容器镜像的文件层,每条Dockerfile中的指令都执行完毕后,Kaniko会将新生成的镜像推送到指定的镜像仓库中去。 整个过程中,完全不依赖于docker daemon。

下面是使用docker启动kaniko容器完成容器镜像构建的一个例子:

1
2
3
4
5
6
7
8
docker run --rm \
-v $HOME.docker:/root/.docker \
-e DOCKER_CONFIG=/root/.docker \
-v ./Dockerfile:/workspace/Dockerfile \
gcr.io/kaniko-project/executor:latest \
--dockerfile /workspace/Dockerfile \
--destination harbor.youcompany.com/library/xxapp:1.0 \
--context dir:///workspace/

下面是k8s中使用kaniko的启动pod的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: v1
kind: Pod
metadata:
  name: kaniko
spec:
  containers:
  - name: kaniko
    image: gcr.io/kaniko-project/executor:latest
    args: ["--dockerfile=/workspace/Dockerfile",
            "--context=dir:///workspace/",
            "--destination=harbor.youcompany.com/library/xxapp:1.0"]
    volumeMounts:
      - name: kaniko-docker-config
        mountPath: /workspace/config.json
        subPath: config.json
      - name: kaniko-dockerfile
        mountPath: /workspace/Dockerfile
        subPath: Dockerfile
      - name: kaniko-workspace
        mountPath: /workspace
    env:
      - name: DOCKER_CONFIG
        value: /workspace
  restartPolicy: Never
  volumes:
    - name: kaniko-docker-config
      configMap:
        name: kaniko-docker-config
    - name: kaniko-dockerfile
      configMap:
        name: kaniko-dockerfile
    - name: kaniko-workspace
      hostPath:
        path: /tmp/kaniko
        type: Directory
  imagePullSecrets:
  - name: harbor-secret

Jib

Jib也是Google开源的一款Java容器镜像构建工具,通过使用Jib,Java 开发人员可以使用他们熟悉的Java构建工具来构建容器镜像。 Jib负责处理将应用程序打包到容器镜像中所需的所有步骤,它不需要我们编写Dockerfile或安装docker daemon,而是直接把镜像构建功能集成到了java构建工具gradle和maven中(通过将插件添加到构建中),即用java的构建工具直接完成容器镜像的构建。

以下示例将使用Jib提供的gradle插件集成到一个spring boot项目的构建中,并展示Jib如何简单快速的构建镜像。

首先,在项目的build.gradle构建文件中引入jib插件:

1
2
3
4
5
6
7
8
9
buildscript{
    ...
    dependencies {
        ...
        classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.2"
    }
}
 
apply plugin: 'com.google.cloud.tools.jib'

配置Jib相关参数,以下参数还可以通过 gradle jib -Djib.from.image='********' 这种形式从gradle命令行提供:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
jib {
    from {
        image = 'harbor.youcompany.com/library/base:1.0'
        auth {
            username = '********'
            password = '********'
        }
    }
    to {
        image = 'harbor.youcompany.com/library/xxapp:1.0'
        auth {
            username = '********'
            password = '********'
        }
    }
    container {
        jvmFlags = ['-Djava.security.egd=file:/dev/./urandom']
        ports = ['8080']
        useCurrentTimestamp = false
        workingDirectory = "/app"
    }
}

执行以下命令可以直接触发构建生成容器镜像:

1
2
3
4
// 构建jib.to.image指定的镜像,并且推送至镜像仓库
gradle jib
// 构建jib.to.image指定的镜像,并保存至本地docker daemon
gradle jibDockerBuild

buildkit

buildkit是docker官方社区开源出来下一代的构建工具包,可以更加快速、高效、安全的构建OCI标准的镜像。 前面我们在学习containerd的时候已经结合nerdctl体验过它。它主要包含服务端buildkitd和客户端buildctl。 可以看出buildkit也是C/S架构,但buildctlbuildkitd可以不在同一台服务器上。 buildkitd可以监听TCP端口将服务以gRPC的形式暴露出来,供buildctl调用。 例如,可以将buildkitd部署到k8s集群中,buildctl作为工具放到k8s上的jenkins slave pod容器中,buildctl在k8s集群内以tcp的形式调用buildctl完成容器镜像的构建。

总结

通过本节的学习,可以看出有了kaniko, buildkit等工具,在构建容器镜像时已经完全可以脱离docker daemon, 而且这些工具都能很好的与k8s集成,支持在容器环境下完成构建。