七叶笔记 » golang编程 » 技术分享|企业级容器 CI/CD 的进阶之路

技术分享|企业级容器 CI/CD 的进阶之路

今天我们介绍的主题是 “ 企业级容器 CI /CD 的进阶之路 ”,也就是我们 KubeSphere DevOps 的成长之路。

我这里介绍将分为以下几部分,我们 KubeSphere DevOps 主要分为两个重要功能,一是 DevOps 流水线,二是 S2I/B2I。

首先,我们介绍我们为什么使用 Jenkins 作为我们流水线的底层引擎,当我们选择好 Jenkins 后,我们如何把它跟 KubeSphere 融合在一起,打造我们 KubeSphere CI/CD 流水线。同时,我们后面使用复杂的方案,可能有些用户不太接受,我们提出更加简单易用的 S2I/B2I。我们在上线后有很多用户,这些用户给我们做了很多反馈,我们踩了很多的坑。这里讲讲我们具体优化的点,以及我们未来的简单规划。

1

为什么我们选择 Jenkins

首先,为什么我们选择 Jenkins 作为我们 CI/CD 流水线?

先看看企业里的 CI/CD 流程。KubeSphere 最初的目标用户是比较大型的用户,像大型的银行业、保险业,面对的用户 DevOps 流程很复杂。我们可以看到在图中,这里涉及很多人员、流程复杂、审批等一系列的需求。同时,在大型企业当中已经有一些 CI/CD 工具时,已经有 CI/CD 自己的流程,我们希望通过 KubeSphere 的流水线 把传统用上的工具融合进来,同时也能为它提供容器平台之上,借助 KubeSphere 助力业务快速上线的能力。

在这些客户当中,一些企业已经在使用一些 DevOps 工具做测试、进度管理等,但是他不知道如何在容器上做发布,我们希望能够 通过 KubeSphere 的 DevOps 功能对整个流程进行把控。

于是我们开始做技术选型。在做技术选型时,这是我从国外的网站 StackShare 拿到的一份数据,我选了一个我认为比较主流的 CI/CD 引擎。这里是市场份额的数据,可以明显看到 Jenkins 的用户、相关问题、投票都非常多。比如 Drone.io 和 GitLab CI 是几百,到了 Jenkins 是几万。

那用户为什么会选择 Jenkins 呢?

这也是我们选择 Jenkins 作为我们流水线引擎非常重要依据,这里可以看到 Drone.io 和 GitLab CI,用户喜欢他们的点是安装简单、配置简单,和 Docker 结合很简单等一系列的东西。当我们看 Jenkins 时,首先是开源和可以私有化部署。

更重要的是使用 Jenkins 我们可以和许多 CI/CD 工具很容易结合在一起,完成打包、部署一系列的流程。同时 Jenkins 有很多插件、文档,他们有成熟且活跃的社区,我们可以借助社区的力量,来完成我们的 DevOps 功能。

这是使用 Jenkins 的代表企业,可以从代表企业看出,我们不了解这些企业具体是做什么的,只有 Jenkins 的代表企业我看的比较眼熟,比如 Facebook、Netflix、ebay 等这种比较出名的国外大型企业,说明他们企业内部的复杂流水,使用 Jenkins 是可以满足的。

于是我们选择了 Jenkins 作为我们 CI/CD 底层流水线引擎。这个方案是用户喜欢的方案,已经有很多用户在使用。它有活跃、成熟的社区,我们可以很容易借助社区的力量完成我们的产品。同时很容易跟企业已有的 CI/CD 工具进行集成。它有非常可靠的插件体系,我们想完成 Jenkins 和我们 KubeSphere 进行融合,很有可能会使用插件扩展的功能,如果它没有可靠的扩展开发的体系,可能我们集成起来会比较困难。

当我们选择 Jenkins 作为我们 CI/CD 的引擎后,下一步是如何将 Jenkins 融入到我们 KubeSphere CI/CD 流水线里。

2

打造 KubeSphere CI/CD Pipeline

KubeSphere CI/CD 流水线早期时有一系列的需求,我简单列出来。首先是多租户隔离的需求,最初一些大企业用户有很多部门、分公司,他们希望用我们 KubeSphere 作为容器平台,最后是多租户管理,方便他们进行管理。我们是一个容器平台我选择 K8s 作为我们底层容器的引擎。我们希望在 K8s 上,Jenkins 可以充分地释放、利用 K8s 自己的能力,并且我们要把前面提到的企业中复杂的开发、测试、构建、部署一整套流程。同时我们要支持多种方式的流水线,我和代码仓库绑定还是不绑定,满足企业的复杂需求。有一些用户没用过 Jenkins,他搭建流水线比较困难,我们希望通过图形化编辑的手段简化他们流水线搭建的过程,使用起来更加简单。

首先,我介绍一下我们如何实现多租户 。在多租户里主要分为两部分,一是认证部分,二是 鉴权 部分。 在认证部分比较简单,我们 KubeSphere 本身内置 Ldap 进行用户的存储。Jenkins 本身有对接 Ldap 方案的,我们让 Jenkins 直接对接 Ldap 里。这时候我们完成用户的打通,这时候会有一个问题,用户打通后,Requset 无法打通。KubeSphere 不知道 Ldap 里的用户的密码是什么,因此在 KubeSphere 想要以用户身份对 Jenkins 做操作时也比较困难了。

如何完成认证?

在认证里,我当时做了一些调研,我发现 Jenkins 扩展果然很丰富,左边是 Jenkins 一张官方的图,介绍 Jenkins 鉴权过程是怎样的。他分为 Web UI 和 Rest API 两部分,在 Rest API 里面有一个叫 Basic Header 验证器,我看到这个验证器有真正的密码验证器、API Token 验证器,我想这是不是可扩展的密码,我研究了一下发现它真的可以扩展,我们自己写了一个 KubeSphere Token 的验证器,帮用户使用 KubeSphere Token 请求 Rest API 时,这个请求最后会转发到我们 KubeSphere IAM 请求验证的过程。

这时候我们的认证完成了,我们的用户是打通的,并且我们可以使用统一的 API Token 进行验证,访问 Jenkins 的 API。

接下来是另一部分鉴权。

这一部分里,我先介绍 Jenkins 的 API 和 K8s API 的区别。上面是 K8s API 获取的信息,下面是 Jenkins 的 API。K8s 的 API 我们可以非常容易发现它非常结构化,它有自己的动作、API group,它具体请求的 Resource 是什么,它的名称是什么。

我们再来看 Jenkins 的 API,Jenkins 的 API 看起来是一堆不知道是什么的东西,Jenkins 在设计之初不是基于 API 的服务,这导致它的一些差距。我们可以看 K8s 的界面方式,K8s 的方式是基于 Rest API 的路径和动作进行鉴权的。但在 Jenkins 当中,从一些代码里内嵌检查语句,看这个人是不是有管理权限或者读权限等。这说明我们很难借助 K8s 的能力把它鉴权。

前面提到 K8s 的鉴权有很多问题,我们后面进行改造。最初我们希望借助 K8s 的能力,我们在想 DevOps 能否借助 Jenkins 的能力先完成鉴权的方法。于是我们使用了另一个方案,这可以理解为是暂时的方案。最后我们会通过 KubeSphere 内置鉴权可扩展来完成这部分工作。

这时候用户在请求 KubeSphere API 时分为两部分,一部分是和权限操作相关的。比如我要为 DevOps 工程添加用户,分类、角色等。另一部分是非权限相关的,当 K8s API 收到请求后,权限 API 转发为 API Server,我们会在 API Server 里同步往 Jenkins 写入一些权限规则,满足权限的匹配。当你们访问其他 API 时可以直接发到 Jenkins,借助原有 Jenkins 的鉴权能力和 API 的能力,使用这种方式有一个优点,用户仍然可以使用 Jenkins 的页面。我们的鉴权、认证是可以打通的,并且我们不需要对 Jenkins 的 API 进行大量的包装,因为它不规则,我们包装起来非常困难。我们在之后的版本中也对 API 进行了包装。这时候我们完成了认证和鉴权的部分,可以满足多租户的需求。

关于 如何充分释放 K8s 能力 ,最初我们使用 Jenkins 设计中的 Kubernetes Plugin 的方案,我们做了配置上的调整,我们默认全部使用 Kubernetes 的 Agent,自动动态创建,这样比较节省资源,也可以充分释放 K8s 的能力。

这时候有一个问题,用户写 Jenkinsfile 需要 Agent 的定义。这对不熟悉 K8s 的人来说是非常困难的,他要先学会怎么做 Docker 镜像,再学会怎么写 K8s yaml 文件,再运行。于是我们内置了一些用户可能比较常用的 Agent 类型,比如 Maven Agent、NodeJs Agent 同时支持用户自己扩展 Agent 类型,满足他自己独特的需求。通过这种方式,我们充分释放 K8s 的能力。

另一部分是 In SCM 流水线的实现方式, 我们的流水线一般分为两种,一种是通过代码操作绑定的,另一种是通过代码操作不绑定的。我调研了很多方案,很多厂商做的和代码分支绑定,我个人认为是比较假的方案。我绑定了这个流水线到某一个代码仓库的某一个分支上,我不支持自己发现分支,发现它的 PR,自动进行 CI/CD。我实际上只能为一个分支执行流水线的过程。

这会有一些问题,我们在开发过程中很容易创建新的分支、新的 PR,这时候 DevOps 是没有运行的。只有将提交代码到 Master 分支或者其他的已经设置了流水线的分支上才会,不利于企业 DevOps 的发展。

于是我们当时调研想着借助 Jenkins 的生态来提供分支发现这样的功能。Jenkins 自己有支持 Jenkinsfile 发现的功能,它可以支持多种厂商的 SCM,有 Git、GitHub、GitLab、Bitbucket 等,当我们选择这种方案时,同时还拥有了另一个能力, 可以兼容传统用户的 Jenkinsfile, 我们不需要自己定义一套流水线的编写规范。如果是传统的 Jenkins 用户,他们想迁移到 KubeSphere 的流水线上来也会变得更加简单。

我们需要图形化编辑流水线的能力,在了解这个功能时候,我们不得不提到 Jenkinsfile 具体内容。Jenkinsfile 有两种类型:脚本类型的 Jenkinsfile 和声明式定义的 Jenkinsfile。

这是我举的两个简单的例子,想说明他俩有什么不同。左边的 Jenkinsfile 里面有变量定义、有 for 循环,一看感觉我在写代码,不是在写一个流水线的定义。这个方案有一个缺点,学习曲线非常陡峭。我要先学会 Groovy 语言,再学习 Jenkins 上的 Groovy 语言。显然对于我们前端的同学来说是非常难以解析的,这不是结构化的定义,我们要做图形化的编辑不能用这种方案。

另一种是声明式流水线,我们可以来看看声明式的流水线定义:它有一个流水线,在流水线有很多 Stage,Stage 下面可能有具体的 Step 执行一些命令。这种方案曲线相对平滑,他们的功能相对有限,但足以满足企业里复杂的 CI/CD 的需求。这时候可以进行结构化的解析,我们后端提供结构化解析的 API,前端同学拿到结构化解析的数据,再进行流水线的展示。于是我们得到 KubeSphere 流水线的编辑页面,在编辑页面里我们可以很顺利添加顺序的 Stage 或者运行的 Stage,添加一些具体的 Step,比如我们要拉代码、执行命令等。

经过这个阶段,我们可以满足企业用户的复杂 DevOps 流程,让他们在 K8s 之上进行 DevOps 的方案,让企业拥有更快的上线时间、更高的部署频率、更快的修复时间和更短的恢复时间。

当我们发布了复杂流水线的功能后,我们在社区和其他用户收到很多反馈。使用 Jenkins 的方案,它的流程定义是相对复杂的,我要定义我的构建、测试、部署等一系列的阶段。而有些企业就是想试一试自己能否快速将自己的应用部署到 K8s 之上,复杂的流水线对这种用户来说是不适用的,于是我们提供了更加简单易用的 S2I/B2I 功能。

3

更加简单易用的 S2i

首先,S2I 是什么?S2I 不是我们创造的一个概念,它是开源社区的一个命令行工具。这个工具用来做什么?让用户在不需要了解 Docker 情况下可以构建自己的应用镜像。那 S2I 和传统的 Docker 构建有什么区别呢?在我们使用传统 Docker 构建的时候是这样一个过程,我们有很多应用,每个应用都可以有自己 Dockerfile,通过 Dockerfile 可以得到应用的镜像。使用 S2I 后,我们的流程变成了什么?我们每个应用可以通过同一个 S2I 的 Builder Image 来构建自己的应用镜像,Builder Image 也是 Docker 镜像。

用户只需要选择 Image 就可以得到自己应用的镜像,这时候用户构建 Docker 的过程变得比较简单,因为我们只需要使用 S2I Image 进行简单的选择,并且在我们企业用户中,维护、管理起来更加方便。

比如我收到安全警报说某某版本有一个安全漏洞,我们要进行升级。企业当中有很多应用,但其实他们的 Dockerfile 都很类似,我要通知他们一个个进行升级。而当我们使用 S2I 时,我们只需要找专门的人升级 Image,告诉每个应用负责的人员触发构建就升级完成了,可以一步上线。管理复杂度也减小了。

使用 S2I 后,可以简化开发者的工作方式。业务开发人员只需要在自己的 IDE 里写代码,提交代码到 Git 仓库,触发构建。通过构建器镜像构建出他自己的应用镜像,就可以把这个镜像推送到 Docker 的镜像仓库,并且进行部署等一系列的过程。

前面介绍了使用 S2I 开发者的流程是怎样的。S2I 很好,但它只是一个命令行工具,在企业中使用,大家不是特别喜欢这种方式。因为要学习它的命令,交互起来没那么轻松。于是我们想着把 S2I 这个不错的工具融入到我们 KubeSphere Console 或者 KubeSphere 里。让它成为我们功能模块的一部分。

这里不得不提到 CRD,CRD 是扩展 K8s 的方式,叫特殊资源定义。使用这个方式我们可以轻松得到和 K8s 一样的声明式 API,我们模块之间是松耦合的。K8s 底层提供了 List-Watch 机制,使用这种方式我们可以快速响应用户 S2I 的需求。

于是我们 KubeSphere 里,S2I 架构变成这样。左边是我们的用户,右边是我们的具体细节。用户可以通过 Kubectl 或者其他工具,比如 KubeSphere Console,或者直接请求我们的 API。这个 API Server 是 K8s 的 API Server,K8s 的 API Server 在收到用户请求时,会把数据存储到 etcd 里,会通知 S2I Operator 完成一系列的任务操作。

在这里我们主要定义了三个 CRD,第一个是 S2I Template,有 Java 、Golang 和 Binary 。在填写很多信息时不想填写重复的信息,他想要选择一个模板。比如我创建流水线时,我是 Java 语言的,S2I Template 就是这样。S2i Builder 类似我们的流水线定义,当我们选择好要使用语言,这时候只需要选择代码仓库或者上传制品,加上填写我们要制作的镜像或者推送的镜像名称,这时候我们定义好一个流水线。我们可以触发,每次触发都会生出一个 S2I Run,在 S2I Run 里我们具体执行 S2I 的过程,完成构建镜像、推送镜像,帮忙把这一系列的信息上报到 API Server 进行汇总展示。

于是我们简单的 CI/CD 只需要填写简单的表单,我们选择构建环境是什么,填写镜像、仓库地址、名称,就可以完成了。通过这种方式,在我们 KubeSphere Console 上,可以从源代码一键直达服务,我们不是只有构建镜像的过程,我还会自动为你创建 Deployment 以及 Service 等资源。用户通过填写一个表单可以一键启动自己的服务。

当然,用户可能有一些团队协作需求。我们会在平台展示构建数据、统计,方便团队协作。比如我看到他今天下午构建了一次等。这是我们 KubeSphere S2I 2.1 版本后有一个单独的页面展示其状态,用户使用起来是非常简单的。通过简单的 S2I/B2I,我们为了让用户有更快的上线时间、更高的频率、更快的修复时间和更短的恢复时间。

4

持续优化,让 CI/CD 更加迅速

我们 DevOps 流水线和 S2I/B2I 可以满足绝大多数用户的需求,包括复杂的 CI/CD 场景,还有简单的 CI/CD 场景。我们还会有一些优化点,这是我们 KubeSphere 中的页面。

如果你是 KubeSphere 早期流水线的用户,相信你一定见过这个页面。这个地方是流水线在初始化,初始化时间很长,这困扰了我们团队很久,也困扰我们的客户、用户很久。于是我们在 Jenkins 社区中进行了一些研究。

这里提到 Jenkins EMA 算法,这个算法你们不需要了解它是什么,我简单介绍它的功能是什么。Jenkins 诞生的年代非常早,大概有十几二十年的时间,这个 EMA 算法其实是 Jenkins 内部的 Agent 调度算法,在这个调度算法里当时的目标满足一个目标,不要让我们 Agent 发生频繁的变化,比如你要扩容,我判断是否有任务运行完了,如果有任务要运行完了,要等一等。因为做动态的扩容、缩容非常消耗资源,也很慢。

但 EMA 算法不适合这个时代,我们知道 K8s 的能力是瞬间启动,启动完后瞬间执行。于是我进行了一系列的调研,发现默认的调度算法是可以进行扩展的。我改变了 Kubernetes Agent 的调度算法,现在已经贡献给社区。

最后变成右边的样子,策略非常简单,没有任何延迟的调度策略。如果有一个流水线来了,我们立马对它启动一个 Agent 执行流水线。我们进行测试会发现原来初始化时间等待有几十秒、几分钟甚至几十分钟。通过优化可以让调度时间变成毫秒级,也就是说我收到一个任务后,立马可以启动,执行我的任务。

在这张图是 Jenkins 的监控图,灰色线指的是等待构建任务的数量,红色线指的是忙碌 Agent 数量。可以看到灰色线刚刚开始后,红色的线会迅速升起来,完成 CI/CD 的任务。我们当时做了 Master 节点的监控,可以从这个时间看出来,我们当时测试了 100 个流水线,大概完成时间,同时完成需要 4 分钟。我们这里看到 Jenkins Master CPU 用量当时打满了。如果我们对 Master 节点进行扩容,或者对 K8s 环境进行扩容,我们可能可以跑得更快。

这一部分是我们 Jenkins Agent 的扩展。当 Agent 启动后有一个问题,我们怎么让流水线跑的更快?这时候提到用户的场景,用户很多是 Java 的开发者,他们会在企业的环境搭一个 Nexus 作为自己的制品管理和依赖缓存。

他们当时使用这种方案,在我们 Jenkinsfile 里配置使用 Nexus,看起来是没问题的,实际上真的没有问题吗?这时不得不说到开发高峰期,当开发高峰期来的时候,一个下午开发人员最多提交代码有几十个或者上百人同时提交代码,会启动非常多的 Agent。因为 Agent 每次会销毁,所以每次运行流水线的时候,Agent 都需要从 Nexus 重新拉缓存,这时候我们发现 Agent 宿主机的硬盘 I/O 跑满了,并且 Nexus 出口带宽也跑满了。Nexus 还会出现部分的连接失败,它的流水线虽然启动很快,但运行不够快。

于是我们在 2.1 版本中添加了另一个功能, 基于硬盘的依赖缓存 。之前我们使用 Nexus 缓存中心仓库,在这个版本中我们从节点硬盘中加上缓存,当没有依赖的时候,我会先检查节点的上有没有依赖,如果没有,我会去 Nexus,Nexus 没有再去中心仓库。使用这种方式我们硬盘 I/O 和 Nexus 出口带宽已经降下来,可以避免每次重新拉缓存,让我们 CI/CD 跑得更迅速。

这是我们的构建,主要针对缓存的对比。可以看到这里有两个测试用例,一个是 Java 的,另一个是 Node.js,二者也是开源的。通过用它满足依赖,我们速度提升,Java 可以做到 9 倍,Node.js 差不多有 5 倍之多。 当我们的运行速度、调度速度很快的时候,企业的发布速度也会变得很快。

当然,我介绍了 KubeSphere DevOps 优化的两个点,我们优化了很多地方,包括前面提到的用户优化、无感知缓存以及我们支持 S2i Runtime Image 等,还有 DevOps 动态 PVC,我们做的优化是想 让用户使用得更方便,让他拥有更快上线时间、更高的部署频率、更快的修复时间和更短的恢复时间。

针对前两部分,我的 CI/CD 可以跑得很快,并且满足用户的任何需求。其实我们还是有很多未来的规划,在这里我只是展示了未来规划的设计图。

这是我们在 3.0 计划做的目标和创建,我们 Jenkinsfile 定义起来相对比较复杂,我们希望用户可以通过选择语言,选择自己做什么的方式可以完成自己复杂的流水线创建,简化用户的创建过程。我们也会支持用户的邮件通知等。我这里没有列出太多,我希望能够从社区的用户、社区贡献者当中得到更多的反馈,包括你可以向我们反馈你想要什么样的功能,或者你直接给我们提交代码,让我们的社区变得更好。

相关文章