一、前言

总所周知,从Kubernetes1.24版本开始已经弃用Docker这个陪伴它风声水起的"初恋女友", 届时在Kubernetes社区掀起了异常"轩然大波",影响甚至波及到社区之外的,也导致了Kubernetes不得不写好几篇博客来反复解释这么做的原因,虽然是老生常谈的问题了,如今距离1.24版本正式发布已过去一年之久,本篇文章带大家回顾下为什么Kubernetes要弃用陪伴它多年的Docker

二、Kubernetes发展史

时间回到2014年,Docker正如日中天,可以说放眼容器领域,没有任何对手,可谓是无敌于天下,而这是Kubernetes才刚刚诞生,虽然背后有Google和Borg这两位大佬支持,但自身实力还是比较弱小,需要时间去孵化成长,就在此时,Kubernetes与Dcoker邂逅了,它被Docker强大的魅力所吸引,自然而然的选择了依附于它,毕竟“背靠大树好乘凉”。同时也好趁机"养精蓄锐"逐步发展壮大自己。

时间转瞬即逝来到了2016年,CNCF已经成立了一年,而Kubernetes也已经发布了1.0版本,可以正式投入企业生产环境中。这已经标志Kubernetes已成长起来了,有了底气之后不需要"看脸色吃饭", 为了进一步壮大自己,于是乎它加入了CNCF这个组织,成为了CNCF一个托管项目,其目的就是借助基金会的力量联合其它厂商一起"绊倒"Docker

那么Kubernetes是如何一步步实现自己的“宏图大业”呢? 在2016年底1.5版。Kubernetes引入了新的接口标准,即CRI(Container Runtime Interface),该接口采用了ProtoBuffer和gPRC,规定kubelet该如何调用容器运行时去管理容器和自己的镜像。但由于这是一套全新的接口,无法和自己的"初恋女友"Docker继续相处。其实到这里,Kubernetes意思很明显,就是不想再绑定Docker上,允许在底层接入其它容器技术(比如rkt、kata等),随时可以将Docker踢开

但是这个Docker已是非常强大了。占领较大的市场份额且市场惯性非常强大,很多企业已对Docker这款开源产品非常依赖了,再者各大云厂商不可能一下子将Docker全部替换掉,因此Kubernetes便提出了折中的方案,在kubelet和Docker中加入了符合CRI接口,我们可以把接口理解为"适配器",主要适用于Docker的接口转换成符合Kubernetes 自己的CRI标准的接口

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Docker

如上图所示,这个"适配器"在kubelet和Docker之间,所以就被很形象的称之为"shim",也就是垫片的意思。有了CRI和shim,虽然Kubernetes还使用Docker作为底层运行时,但也具备了和Docker解耦的条件,从此拉开了"弃用Docker"的帷幕。

另一方面,Docker没有坐以待毙,而是采用了断臂求生的策略,推动自身的重构,把原本单体架构的Docker Engine拆分成多个模块,其中的Docker daemon部分捐献给CNCF,至此这就形成了我们熟知的Containerd概念,Containerd作为CNCF托管项目,自然是要符合CRI标准的,但由于Docker出于自身诸多原因考虑,它只是在Docker  Engine里调用了containerd。外部接口仍然保持不变,也就是说还是不能和CRI兼容

此时Kubernetes便给出了两种调用链,如下如所示:

1.kubelet基于CRI接口去调用dockershim适配器,然后dockershim适配器进行转换去调用docker,随后docker在去调用containerd去操作容器

2.kubelet基于CRI接口直接调用containerd去操作容器

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Docker_02

综合对比,上述都是用containerd来管理容器,虽然最终效果是完全一样的,但第二种方式省去了dockershim和Docker Engine两个环节,对于整个架构体系来说,更加间接明了,在性能方面也得到的较大提升。同时在2018年Kubernetes 1.10发布时,Containerd也更新至1.1版。正式与Kubernetes集成,官方也针对其两者做了性能压测,如下所示

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Devops_03

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Docker_04

上述性能对比来自Kubernetes官方(https://kubernetes.io/blog/2018/05/24/kubernetes-containerd-integration-goes-ga/)从上述数据可以看到,Containerd相对于Docker,Pod的启动延迟降低了20%,CPU使用率降低了68,内存使用率降低了12%。 由此,这是一个相对较大的性能优化,这这对于我们企业生产环境来说是非常具有诱惑力的。

有了上述对比,一直青睐于Docker的各位云厂商以及企业开始有了动摇,到了2020年,Kubernetes在 1.20版本正式向Docker宣战,宣告在未来1.24版本彻底弃用Docker,让它做好心里准备。

三、需求背景

时间来到今天,因为项目环境特殊需求,阿里云平台ACK版本最老的集群版本就是1.24,对于我们一直在用Kubernetes v1.24之前算是新的挑战和学习。首先要克服的就是研发提交代码如何自动化部署到k8s集群的问题,这里我将问题归类以下两点:

1、1.24版本开始已弃用Docker,如何集成DevOps CICD流水线技术呢?

2、在1.24版本之前通常基于Dockerfile构建业务镜像,但弃用Docker之后,如何构建业务镜像部署到K8S集群呢?

带着上述的疑问,接下来介绍Kubernetes 构建业务镜像的新宠-"kaniko"

3.1、什么是kaniko?

kaniko文档地址:https://github.com/GoogleContainerTools/kaniko#–insecure

Kaniko是一种在容器或者Kubernetes 集群内从Dockerfile构建容器镜像的工具,不依赖Docker.sock守护进程,而是完全在用户空间中执行Dockerfile中的每个命令,这使得在没有docker.sock的基础环境中构建Dockerfile镜像成为可能,例如标准的Kubernetes1.24集群。

3.2、为何要使用kaniko?

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Devops_05

其主要原因由于Kaniko不依赖Docker守护进程,并且完全在用户空间下执行Dockerfile中每个命令,这使得能够轻松并安全的运行在无Docker环境守护进程,在kubernetes 1.24版本之后默认将会采用containerd.io,作为缺省的CRI接口,不在支持dockershim,也就意味着不需要安装docker环境就能构建Dockerfile镜像。

3.3、kaniko工作原理流程是什么?

首先会读取指定Dockerfile文件,将基本的镜像(FROM指令中指定)提取到容器文件系统中,在独立的Dockerfile中分别运行每个指令,并且每次运行后都会对用户空间文件系统做一次快照,并将快照层附加到基础层,完成之后就会自动推送镜像至仓库,全程无需人工干涉

Kaniko仅支持容器镜像运行,接受三个参数:一是Dockerfile构建文件,二是构建上下文目录,三是将业务镜像构建完成之后需要推送的注册表中心,也就是我们所说的镜像仓库。kaniko容器中有一个可执行的二进制文件即/kaniko/executor,它在执行程序镜像中提取基本镜像的文件系统,然后在Dockerfile中执行任何命令,快照用户空间的文件系统,Kaniko在每个命令后都会将一层更改的文件附加到基本镜像,最后,执行程序将新的镜像推送到镜像仓库中,由于Kaniko在执行程序镜像的用户空间中完全执行了这些操作,因此它完全避免了用户计算机上需要特权访问,从而提高了安全性。

参数详解

--context:指定构建上下文的路径。默认情况下,上下文是Dockerfile所在的目录。可简写 -c
--dockerfile:指定要使用的Dockerfile的路径。默认情况下,Kaniko会在上下文中查找名为Dockerfile的文件。可简写 -f
--destination:指定构建完成后的Docker镜像名称,可以包括标签。例如:myregistry/myimage:tag。可简写 -d
--cache:启用或禁用Kaniko的构建缓存功能。默认情况下,缓存是启用的。
--cache-ttl:设置构建缓存的生存时间。例如,--cache-ttl=10h表示缓存在构建完成后的10小时内有效。
--cache-repo:指定用于存储构建缓存的Docker仓库。默认情况下,缓存存储在本地。
--cache-dir:指定用于存储构建缓存的本地目录路径。
--skip-tls-verify:跳过TLS证书验证,用于不安全的Docker仓库。
--build-arg:传递构建参数给Dockerfile中的ARG指令。例如:--build-arg key=value。
--insecure:允许从不受信任的Registry拉取基础镜像。
--insecure-registry:允许连接到不受信任的Registry。
--verbosity:设置构建的详细程度,可以是panic、error、warning、info、debug或trace。
--digest-file:指定一个文件,用于存储构建生成的镜像的摘要(Digest)。
--oci-layout-path:指定OCI(Open Container Initiative)布局文件的路径,用于存储构建过程的元数据。

四、企业实战演练

上述对Kankio进行了细致化概念性的讲解,接下来通过实战带大家更好的去了解Kaiko它独有的魅力,逐一去演示。

Dockerfile准备就绪之后,开始启动容器,并验证是否自动生成镜像并完成推送

4.1、基于Kubernets环境使用Kaniko构建业务镜像

4.1.1. 定义镜像仓库secret

获取镜像仓库认证信息,在这里我使用阿里云中的作为镜像仓库,随便找一台有docker服务的Linux服务器,然后基于docker login做一次镜像仓库认证即可生成认证文件~/.docker/config.json, 随后,将config.json拷贝拷贝至容器编排集群中,将其定义为secret资源。

指定harbor镜像仓库用户名以及地址
#docker login -u [镜像账号] [镜像仓库地址] 
创建指定harbor镜像config.json文件,定义名为kaniko-secret的secret资源
# kubectl create secret generic kaniko-secret --from-file=/root/.docker/config.json

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Devops_06

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Docker_07

4.1.2. 定义Dockerfile

既然要通过Kaniko去构建业务镜像,那么资源对象仍然是Dockerfile,在这里准备了简单的Docker文件,需要将其定义为Configmap作为容器编排使用。

定义configmap资源
#kubectl create configmap kaniko-dockerfile --from-file=Dockerfile

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Dockerfile_08

截止目前,Kaniko所依赖的镜像仓库认证信息以及Dockfile都已准备完毕。

4.1.3. 基于Pod实例实现业务镜像构建

如下所示,在这里我给出了pod定义的实例,

apiVersion: v1
kind: Pod
metadata:
  name: kaniko
spec:
  containers:
  - name: kaniko
    image: kubebiz/kaniko:executor-v1.9.1
    args: ["--dockerfile=/workspace/Dockerfile",	#指定Dockerfile文件
            "--context=dir://workspace",	#指定Dockerfile 上下文件目录
            "--destination=registry.cn-beijing.aliyuncs.com/devops-op/kaniko-demo:1.0"]  #指定所构建的最终镜像
    volumeMounts:  #将上述信息都挂载至kaniko容器相应的目录中
      - name: kaniko-secret
        mountPath: /kaniko/.docker
      - name: dockerfile
        mountPath: /workspace
  volumes:
    - name: kaniko-secret
      secret:
        secretName: kaniko-secret #容器编排所依赖的镜像仓库认证信息
        items:
          - key: config.json  
            path: config.json
    - name: dockerfile
      configMap:
        name: kaniko-dockerfile #容器编排所依赖的Dockerfile

通过kubectl apply 创建更新下pod实例资源

#kubectl apply -f kaniko_build.yaml
[root@akc-node01 kaniko]# kubectl apply -f kaniko_build.yaml
pod/kaniko created
[root@akc-node01 kaniko]# kubectl get pod
NAME                                      READY   STATUS    RESTARTS   AGE
kaniko                                    1/1     Running   0          3s
查看kaniko日志即可看到详细的构建过程,如下图所示
[root@akc-node01 kaniko]# kubectl logs -f kaniko

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Docker_09

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Dockerfile_10

最后在仓库平台上可以看到,镜像已推送成功。以上就是在基于容器化的kianko实现镜像的构建推送的的演示。

4.2、基于Jenkins Pipeline 流水线环境集成Kaniko构建业务镜像

那么如何将Kaniko集成至我们的企业生产的DevOps流程中呢?能否直接将上面的pod实例拷贝直接用呢?答案显然是不行的!我们都知道每个项目所构建的业务镜像是不一样,也就是说Dockerfile对象资源是动态的,上述演示案例本质是将Dockerfile资源作为Configmap资源挂载到pod实例中进行构建。再来回顾下传统打包流程是怎样的呢?当业务代码构建完毕之后,通过docker build -t去指定Dockerfile资源。我们的Kaniko能否像Docker build去构建业务镜像呢?话不多说,来吧展示!

4.2.1、定义镜像仓库认证信息Secret

由于镜像认证信息在上述环节演示过,这里不再赘述。我们直接简写的Pipeline脚本文件示例

4.2.2、Jenkins pipeline使用

Jenkins slave 会根据定义好的Pipeline脚本去执行,连接到K8s集群之后,会首先调用定义在PIpeline脚本中的标签基础镜像,比如jnlp、maven、以及我们今天的主角Kaniko,这个Kaniko同样是以pod的形式定义在脚本中。随后在镜像构建打包环节中直接引用Kaniko pod中的二进制文件即可完成镜像打包。

基于Kubernetes 动态生成Jenkins Slave pod CI/CD,以下仅是给出简化的Pipeline示例

pipeline {
  agent {
    kubernetes {
      cloud 'kubernetes-ACK'
 .............................................
  .............................................
      yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
.............................................
.............................................
.............................................
.............................................
#配置kaniko slave pod镜像
    - command:
      - /bin/sh
      - -c
      args: 
        - cat 
      tty: true
      volumeMounts: #将镜像仓库地址挂载到kaniko中
        - name: kaniko-secret	#指定secret名称,上述已定义好,这里直接指定即可
          mountPath: /kaniko/.docker
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: registry.cn-beijing.aliyuncs.com/devops-tols/kaniko-executor:debug #kaniko二进制容器镜像
      imagePullPolicy: Always
      name: "kaniko"
      tty: true
  nodeSelector:
    jenkins-build: "true"
  volumes:
    - name: kaniko-secret
      secret:
        secretName: kaniko-secret
        items:
          - key: config.json
            path: config.json
    - hostPath:
        path: "/usr/share/zoneinfo/Asia/Shanghai"
      name: "localtime"
    - name: "cachedir"
      hostPath:
        path: "/opt/m2"
'''
    }
}
  stages {
    stage('Pulling Code') {
      parallel {
        stage('Pulling Code by Jenkins') {
.............................................
代码拉取环节
.............................................

      }
    }

    stage('Building') {
      steps {
        container(name: 'build') {
            sh """
构建部分 .............................................

            """
        }
      }
    }

    stage('Docker build for creating image') {
      environment {
        HARBOR_USER = credentials('HARBOR_ACCOUNT')
    }
      steps {
        container(name: 'kaniko') {
          sh """
          echo ${HARBOR_USER_USR} ${HARBOR_USER_PSW} ${TAG}
          pwd
          #由于Dockerfile定义在git上,项目构建完成之后,基于Kaniko二进制文件/kaniko/executor构建业务镜像,其中
          -f是dockerfile的简写,用于指定Dockerfile文件路径
          -c是context的简写,用于指定构建上下文的路径。默认情况下,上下文是Dockerfile所在的目录
          -d是destination的简写,用于指定构建完成后的Docker镜像名称
          /kaniko/executor -f Dockerfile -c ./ -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} --force
          """
        }
      }
    }

    stage('Deploying to K8s') {
      environment {
        MY_KUBECONFIG = credentials('k8s-kubeconfig')
    }
      steps {
        container(name: 'kubectl'){
           sh """
部署 .....................
        }
      }
    }

  }
  environment {
定义变量
   .............................................
   .............................................
    TAG = ""
  }
  parameters {
    gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
  }
}

五、验证

基于Kaniko集成至Pipeline流水线体系到这里就结束了,如下图所示

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Dockerfile_11

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Docker_12

Kubernetes与Docker"分手"之后如何设计DevOps流水线_Devops_13

关于Kaniko 集成Jenkins Pipeline实现业务容器构建的相关内容就说这么多,与君共勉

END