阿里巴巴如何基于 Kubernetes 实践 CI/CD
本文根据华相在「Kubernetes & Cloud Native Meetup- 广州站」现场演讲内容整理,已授权 InfoQ 转载。
大家好, 我是来自阿里云容器服务团队的华相。首先简单解释一下何为 Kubernetes 来帮助大家理解。Kuberentes 是一个生产可用的容器编排系统。Kuberentes 一方面在集群中把所有 Node 资源做一个资源池,然后它调度的单元是 Pod,当然 Pod 里面可以有多个容器。 就像一个人左手抓着 ECS 资源或计算资源,右手抓容器,然后把它们两个匹配起来,这样它就可以作为一个容器的编排系统。
而 Cloudnative 这个概念现在会经常被大家提起,很多人迷惑 Cloudnative 又与 Kuberentes 有什么关联?我们又该如何判断一个应用是 Cloudnative 呢?我认为有以下三个判断标准:
第一,它能够给资源做池化;
第二,应用可以快速接入池的网络。在 Kuberentes 里面有一层自己的独立网络,然后只需要知道我要去访问哪个服务名就可以,就是各种服务发现的一些功能,它可以通过 service mesh 去做一个快速地访问;
第三是有故障转移功能,如果一个池子里面有一台主机,或者某一个节点 down 掉了,然后整个应用就不可用了,这肯定不算是 Cloudnative 的应用。
比较这三点就可以发现 Kuberentes 做的非常好。首先我们看一个资源池的概念,Kuberentes 一个大的集群就是一个资源池,我们再也不用去关心说我这个应用要跑在哪台主机上了,我只要把我们部署的 yaml 文件往 Kuberentes 上发布就可以了,它会自动做这些调度,并且它可以快速地接入我们整个应用的网络,然后故障转移也是自动。接下来我就来分享如何基于 Kuberentes 实现一个弹性的 CI/CD 系统。
CI/CD 的现状
首先了解一下 CI/CD 的现状。CI/CD 这个概念实际上已经提出很多年了,但是随着技术地演进和新工具地不断推出,它在整个流程和实现方式上逐渐丰富。而我们通常最早接触 CI/CD 就是代码提交,随之触发一个事件,然后在某个 CI/CD 系统上做自动构建。
下图可以反映目前 CI/CD 的现状:
它中间包含了很多流程,从代码提交开始触发一个事件,然后它可以中间做一层 Build ,首先通 maven 做一个 build,然后做一个单元测试,然后做代码规范扫描,然后部署。 接下来,做一个端到端的 UI 方面的一层测试,这是一个 UI 的自动测试工具。
然后做了一个压测(性能上的压测),这只是在开发测试环境做了一层处理。然后它可以在 QA 环境,最终延伸到 UAT 环境,整个 pipeline 是一个非常长的过程。CI/CD 的使用范围很广,它可以把整个 IT 从代码编写提交到代码仓库为起点,从这儿开始往后做,各个步骤都可以纳入 CI/CD 的范围。但是随着它的管控范围越来越多,整个流程越来越复杂,它对资源的占用也是越来越高。
大家如果了解 C++ 的话,就会知道 C++ 以前是一门著名的、构建时间特别长的语言。Go 语言的作者之一,也是 C 语言的作者, 谈及当时写 Go 完全是因为不想写一次代码,编译那么久。所以,Go 语言刚出现时的一大优势就是编译时间非常短。Go 的编译时间确实非常短。
我曾用一个 I5 的笔记本编译 Kubernetes 源码做一次完整地构建,大概编译了 45 分钟左右,这是一个非常漫长的过程。所以,即便已经对编译过程做了很大优化和改进,只要项目大了之后,只是构建阶段就已经很长了,更不要说包括后面的一些自动化测试了。包括所有东西之后,CI/CD 流程对资源地消耗和占用的时间是非常大的。
CI/CD 工具的选择
接下来,我们看一下 CI/CD 这些工具的选择和他们的发展。首先最老牌的肯定是 Jenkins。实际上在容器技术兴起之前,CI/CD 简直约等于 Jenkins。但是在出现容器技术之后,很多新生 CI/CD 的工具也应运而生,比如 Drone 工具,它是一个完全基于容器来实现的 CI/CD 工具。它与容器地结合地非常好,并且它的构建过程是完全在容器中实现的。
另外还有 Gitlab CI,它主要特点是与 Gitlab 代码管理工具结合地比较好。Jenkins 2.0 开始引入 pipeline as code 特性,pipeline as code 可以帮我们自动生成一个 jenkins file。
在 Jenkins 1.0 时,如果我们想要配置一条流水线,需要先登录 Jenkins 建一个项目,然后在里面写一些 shell。这样虽然能达到同样的效果,但是它有一个最大的弊端,就是可复制性和迁移性不好。而且它与 Devops 有天然地割裂,比如一般是由运维人员来管理 Jenkins 系统,开发人员编写代码,但是这个代码怎么去构建,发布到哪里,开发人员完全不知道。这就造成了开发和运维地割裂。但 pipeline as code 方式出现了之后,jenkins file 与代码源码可以放在同样的仓库里面。
首先它有一个非常大的好处是发布的流程也可以纳入版本管理,这样对一个错误就可追溯。这是一个非常大地改动,但是实际上我们在与客户沟通中发现,虽然很多人在 Jenkins 上都已经升到 2.0 系列了,但是它们的用法还是完全在 1.0 系列,很多用户都没有把 jenkins file 这种方式用起来。另外一个就是对容器地支持,大概 2016 年左右,那时对容器的支持是非常弱的,在容器里面运行 Jenkins,同时构建的产物也是 Docker 会非常麻烦。
但是, Drone 对容器的支持力度就非常好。首先它完全用 Docker 方式来运行,就是说你的构建环境也在一个容器里,你需要构建一个 Docker build 镜像,然后在推送出去的时候,它也在容器里面运行,然后它需要一个 privilege 权限。它这种方式有几个特别好的地方,首先它不会对宿主机产生任何残留,比如说你这个容器一销毁,构建中产生的一些中间文件完全都跟着销毁了,但是你如果用 Jenkins 的话,随着用的时间越来越久会沉淀出很多临时文件,它占的空间会越来越大。你需要定期做一些清理,而且清理过程中你又不能直接一键清空,所以这是很麻烦的过程。
然后插件的管理 Jenkins 还有一个特别让人头疼的地方,就是它的插件升级。首先你在 Jenkins 登录进去,然后就做插件升级。如果说我想临时在一个新的环境里面,起一个 Jenkins 先测试一下或做一些调试可能每新建一个环境,都需要把这些插件升级一次。而且刚才我们说的在 Jenkins 里面做的所有配置,也都需要重新配置一遍,这是一个非常繁琐的一个过程。
但是 Drone 这个工具它有一个特别好的地方,就是所有的插件都是 Docker 容器,比如说你在 pipeline 里用这个插件,你只要声明用这个插件就可以了,你不用去自己管理把插件下载到哪里,然后怎么安装,它这个一切都是全自动,只要说你网络能把插件容器镜像访问到就可以了,这非常便利。
然后关于生态的构建,Jenkins 的最大的优势就是它的插件非常多,就是你想用的各种东西都有,而且它基础的底座非常好,你的插件可以实现的能力非常的强。比如说 pipeline 就是这种方式,它从 1.0 到 2.0 虽然有了,但是它完全是通过插件来实现的。但是现在 Jenkins 的发展又开始有点第二春的感觉。他开始对 Kuberentes 的支持力度明显的加大了很多,首先从 JenkinsX 开始,它融合了一些 Kuberentes 生态相关的一些工具,比如 Harbor、Helm 它可以非常便利地在 Kuberentes 集群上来做一些构建,并且把一些做服务的固化的一些编排文件放到 Helm 里面。
另外,现在它有一个新的子项目叫 config as code,也就是说他把所有 Jenkin 里面做了一些配置,都可以输出成一个 code 的形式,就是对整个 Jenkins 的迁移,或者说复制都是一个很便利的改进。
讲了那么多,实际上最后我们选择的东西还是 Jenkins,因为最重要的是生态的构建,他们已经很好了。今天我们要讲的在做一个弹性在 CI/CD 这个 Jenkins 上已经有这个插件了,但是在 Drone 的社区里面,有人提这个事,但是现在还没有看到。
CI/CD 的系统业务场景
然后我们看一下 CI/CD 它的系统的业务场景,它有一个比较典型的场景与特点,首先它面向开发人员,这是比较少见的,因为开发人员一般都比较挑剔一点。所以你要是这个系统做的不够稳健了,或者说响应时间比较长一点的话,会被经常吐槽。
然后就是有时效性要求,因为我们代码写完之后,往上提交,我们都不希望在这个代码的构建中一直排队,我们希望马上就开始进行构建,并且资源足够丰富。另外一个就是它的资源占用的波峰波谷是非常明显的。就因为开发人员不可能时时刻刻都在提交代码,有的人可能一天提交几次,有的人会提交很多次。
因为我之前看过有一个分享,有一个人画了一条反映自家公司构建任务的曲线。他们公司大概是每天下午三、四点的时候代码提交量最高,其他时间都比较平缓。这说明他们公司三、四点的时候,程序员提交代码开始划水了。然后随着 CI/CD 资源地需求越来越高,构建集群是一个必须要做的一件事情。就是提高负载能力,缩短任务的排队时间。当然真正的集群有一个让人觉得很不太好的地方,就是它的 Master 实际上只有一个,当然这个也可以通过插件来做改进。
但这不是今天我们要讲的重点,因为很多时候 Master 短暂的不可用,然后能快速恢复也是可以接受的。然后另外它需要来满足各种构建场景。一个公司里面可能用到很多的开发环境,就像虽然我们都用 Java,但可能会有 1.5、1.6、1.7 的差别。 如果不用 Java,很可能用很多其他语言如 Python、Go、NodeJS 等需要很多的构建环境。而且如果我们不引入容器的话,这些构建环境不能重用,可能一台主机只能给 PHP 在用。
容器可以给我们 CI/CD 系统来注入新的能力,就是环境隔离的能力。我们可以通过 Kubernetes 来为 CI/CD 系统注入更多的能力,然后矛盾点就出现了。开发人员总是希望 CI/CD 系统能够快速地响应代码提交的一个事件,但是每个公司资源都不可能是无限的。因为就像上面提到的,如果每天下午三、四点的时候是一个代码提交的高峰,这个时候可能需要 30 或 40 台机器才能满足构建的任务。但是我不可能每天就开着 30 或 40 台机器在这里,就为了每天下午三、四点,可能会构建一、两个小时。
Kubernetes 可以为 jenkins 注入新的能力,让 CI/CD 系统实现弹性的能力。我们期望的目标是什么呢?有构建任务的时候,可以自动为我们资源增加新的机器也好,或增加新的运计算能力也好,反正就是当我需要的时候,可以帮我自动地做一个资源扩张,但是同时也在我不需要的时候,可以自动把这些资源释放掉。
我们期望的目标就是这样,Kuberentes 就可以为 Jenkins 来做这样的能力。Kuberentes 作为一个容器编排的系统,它所能提供的能力,它可以快速地弹出一些新的实例,并且把它们自动调度到空闲的机器上,做一个资源池,在资源池里面做一个调度,并且他执行完任务之后,它可以做一个回收。而且如果把这 Jenkins Master 也部署在 Kuberentes 之上,它可以对 Master 做一个故障转移,就是说如果我们系统可以容忍的话,Master 就算挂了,我可以快速把它调到另外一台机器上,这个响应时间不会很长。
Kubernetes-piugin
这里面也是用一个插件来实现,这个插件名字比较直接叫 Kuberentes-plugin,它这个插件所能提供的能力就是说,他直接管理一个 Kuberentes 集群,它在 Jenkins 里面安装之后,它可以监听 Jenkins 的构建任务。有构建任务,在等待资源的时候,它就可以向 Kuberenetes 去申请一个新的资源,申请一个新的 Pod 去做自动地构建完之后,它就会自动的清理。
先简单介绍一下它的能力,因为这个插件安装完之后,它对 pipeline 的语法也有一个改造, 一会我们来看一下实例。但是就算到了这一步,还是不行的。首先,Kuberentes 的集群规划还是一个问题。比说我有个集群有 30 个节点,真正的 master 部署在这上面,然后装了那些插件,做了一个管理之后,我们可以发现来了一个新的任务,它就起一个新的 Pod,把这个构建任务给执行制定完。
执行完之后,这 Pod 自动销毁不占集群的资源。 平时我们可以在这集群上做一些别的任务,但这个终究还是有一点不好,就是我们这个集群到底规划多大,并且这个集群我们平时不做构建任务的时候,可以在上面做一些别的任务。但是如果正在做任务,突然来了一些构建任务,它可能会出现资源的冲突问题。
Kubernetes Autoscaler
总的来说,还是有一些不完美的地方,那么我们可以利用 Kuberentes 一些比较没那么常见的特性,来解决我们刚才说的这个问题。这两个东西一个是叫 Autoscaler,一个叫 Virtual node。我们先看一下 Autoscaler,Autoscaler 是一个 Kubernetes 官方的一个组件。在 Kuberentes 的 group 下面支持三种能力:
- Cluster Autoscaler,可以对集群节点做自动伸缩;
- Vertical Pod Autoscaler,对集群的 Pod 竖直方向的资源伸缩。因为 Kuberentes 本身里面就带着 HPA 可以做水平方向的 Pod 伸缩、节点数量的伸缩;这个特性还不是生产可用的特性;
- Addone Resizer,是 Kuberentes 上那些 addone 比如说 Ingress Controler、 DNS 可以根据 Node 的数量来对资源的分配做调整。
Cluster autoscaler
我要讲的是 Cluster autoscaler,是对集群 node 节点数量做一个扩缩容。 首先我们看一下,这个是在阿里云我们容器服务上所实现的 Autoscaler 的一个方式。我们看一下这个图,这个是 HPA 和 Autoscler 做结合使用的一个场景。
HPA 监听监控的事件时发现资源使用率上升到一定程度了之后,HPA 会自动通知 workload,来弹出一个新的 Pod,弹出一个新的 Pod,可能这时候集群资源就已经不够了,所以这个 Pod 可能就会 pending 在这里。它就会触发 Autoscaler 的事件,Autoscaler 就会根据我们之前配置好的 ESS 这个模板来去弹出来一台新的 Node,然后自动地把 Node 加入到我们集群里面。 它是利用了 ESS 定制的模板功能,并且它可以支持多种的 Node 实例的类型,可以支持普通实例,支持 gpu,还有抢占式实例。
Virtual node
然后第二种是 Virtual node,Virtual node 实现方式是基于微软开源的 Virtual Kubelet 这个项目。它做了一个虚拟的 Kubelet,然后向 Kubernetes 集群里面去注册上面。但如果不太好理解的话,可以想象一下 MySQL proxy ,然后他把自己伪装成一个 MySQL server,然后后端可能管理着非常多的 MySQL server,并且它可以帮你自动的做一些 SQL 查询的路由或者拼接。
Virtual kubelet 做的也是类似的工作,就是说它本身向着 Kubernetes 注册说我是一个节点,但实际上它后端管理的可能是整个公有云上的非常多的资源,他可能对接公有云的一些 ECI 或者说对接的这些 VPC,这是一个它的大体的示意图。
在阿里云上他们对接的是阿里云的 ECI 做一个弹性的容器实例,它响应时间就非常快,因为它不需要把 Node 去加入到集群里面,它是大概能够到一分钟一百个 Pod 左右这种性能。而且我们可以在 Pod 上声明这种资源的使用情况,这是一个非常快的响应速度时间。
然后刚才说我们利用这两种方式,就可以对我们 CI/CD 弹性的系统做出新的改造,我们不用非常早规划好我们集群的规模,我们可以让集群规模在需要的时候自动的做一些伸缩的动作。但是你做了这些动作之后,我们做了这些把真正的放在容器里面的这些动作之后,引入了一些新的门槛:docker-outside-of-docker 和 docker in docker。
我们在 Docker 中运行 Jenkins 时,通常有两种方式,一个是把宿主机的 docker.sock 挂载到容器里面,让 Jenkins 通过这个文件来和本机的 docker daemon 做一个通信,然后它在这里面做 docker build 构建镜像,或者把这些镜像 push 到远程的仓库里面,所以它中间产生的所有镜像都会堆积在本机上,这是一个问题。
在一些 serverless 的那些场景上使用,它就会有一些限制,因为 serverless 本身就不允许把 socket 文件给挂在进去。另外一个就是 docker in docker 这种方式,它的优点就在于在容器里面它启动一个新的 Docker daemon,它所有的中间产物、构建产物随着容器都一起销毁,但是它有问题,它就是需要 privilege 的权限。
很多时候我们是尽量不要用它。另外一个就是说你做 docker build 的时候能在宿主机上做的时候,它如果有已经有镜像了,它会直接就使用这个镜像,但是你用 docker in docker 这种方式来使用的,它每次都会重新拉进项,拉镜像也是需要一定时间,这个取决于我们各个使用场景来判断。
新的构建工具——Kaniko
这时又引入了一个谷歌开源的新工具——Kaniko。它做的东西是 docker in docker 的方式。它有一个非常大的好处就是不依赖 Docker,而且所以它不需要 privilege 权限就可以在容器里面用用户态的模式,来完全构建 docker image。用户态执行 Dockerfile 的命令,它把这个镜像完全构建出来。
这算是一个比较期望的弹性的 CI/CD 系统。然后这个时候就是说从真正的节点到底层的计算资源全部是弹性扩缩的,而且满足交付的需求,可以非常精细化地管理我们的资源。
Demo 演示
然后我们可以看一下 Demo 演示:
https://github.com/hymian/webdemo 这里是我准备好的一个例子,重点在这个 Jenkinsfile 文件,里面定义了 agent 的 pod template,包含两个容器,一个用来做 golang 的 build,一个用来做 image 的 build。
然后我们现在构建它。开始构建了,刚开始的,因为是现在我们在这环境里面只有一个,只有一个 master,所以他就是没有不会有构建节点。大家可以看到,它现在新启动了一个 Pod,这个 Pod 是作为节点加进来的,但是因为我在这个 Pod 模板里面定义了一个 label,所以它没有这个节点,所以它 Pod 状态是 pending 的。所以我们在构建日志里面显示的这个是 agent 节点是离线的。
但是我们在这个集群里面定义了一个弹性伸缩的一个东西,当没有节点的时候,它会自动做一个新节点分配加入,可以看到有一个节点正在加入,这个我就可以稍等一下。就是说这段时间可能会有个一分钟两分钟的时间。
节点不够的时候,它可以自动扩容然后加入集群,这个要稍微等一下。因为这个时间会稍微久一点,是因为首先它触发到我这个扩容,它有一个轮询的时间差。整个过程下来可能有三分钟左右。我看一下我这个服务器,这个服务器刚才是三个,现在这个服务器是刚刚加入了,这是抢占,这是我刚刚加入的一个东西。
这个是异常,是因为这个节点正在向集群加入,所以它显示是异常,这是我们从命令行看一下,好,已经是四个节点了,加了一个节点,这时候我们看 Pod,这时候在 agent 正在创建,这时候大家可能有一个小的细节,大家可以看一下,就是 0/3 是显示 Pod,它有三个容器,但是我刚才在这个里面定义的,它实际上是 Pod 里面只有两个容器,这就是我们刚才 PPT 上写的一个地方。
JNLP 那个容器,是 plugin 自动注入的一个容器,它通过这个容器实时的向 master 汇报构建的一个中间的状态,我把它的日志给发送出去。这个是 agent 的节点在初始化的一个过程一个事情这时候 slave 节点已经在运行了。我这边已经输出完了,构建完成。我的分享内容就这些,谢谢大家。
作者简介
华相 阿里巴巴解决方案架构师 关注业务容器化,Kubernetes 管理,DevOps 实践等领域。