公司的应用架构一开始就选定了微服务+Kubernetes,整个开发环境都在内网,使用 Jenkins 做半自动化的 CI/CD.
整个前后端都拆分得很细,分了很多层次:
- 脚手架层:封装了开源的各种库(mysql/redis/es 的 CRUD 库,log/tracing/config 库等)。
- 基础层(BaseXxx):基于脚手架层,实现了如下几类应用的基础层:测试器、数据库升级器、微服务、网关、通用工具、一次性任务等。
- 中台:在基础服务上,按功能实现了一些比较通用的中台服务。比如权限认证服务、用户服务、订单服务等。
- 应用层:每个仓库对应一个具体的微服务,也可能包含测试器、数据库升级器等。可能会在 CI 中被打包成多个镜像。
最终只对外暴露出几个对外网关。
这种结构的目的,就是提升代码的复用能力,把应用层能复用的东西,都抽到下面两层去了。
但这要求我们的基础层API一开始就设计得足够好,因为越到后期,API 的影响面就越大,几乎无法修改。
一、CI/CD#
1. Continuous Integration 方案#
目前我们是使用 Jenkins 作为 CI/CD 工具,层次结构也完全对应前面讲到的代码层次结构。
通过一套 BatchJob (批量构建任务,串行或并行地调用相关的子任务)来按依赖顺序,自下向上地层层更新 csharp/python/golang 依赖,构建 nuget 包,最后打包成 docker 镜像。
更新过程中会通过如下几个检测项来判断是否需要构建 nuget 包/docker 镜像:
- 更新私有依赖,返回值:是否更新了依赖
- 更新第三方依赖的小版本,返回值:是否更新了依赖、是否有主版本变更(主版本需要手动升级)
- 与上次构建相比,代码仓库是否存在变更(目的是加速构建)
每一个仓库的依赖更新与版本自增都对应一个 jenkins 任务,由每一层的 BatchJob 按预先定义好的顺序启动这些小任务。(相互独立的任务会被并行调用,以加速构建)
而在应用层,是通过 batchjob 并行构建所有的 docker 镜像。
有一个专门的 job_config 仓库(Python 模块),存储着:
- Git 仓库、Jenkinsfile 与 Jenkins 任务的对应关系,以 yaml 格式保存
- 通过 python 代码提供 api,动态地从上述配置中查询出各层的 Git 仓库、Jenkins 任务的各种信息。
- 提供命令从上述 yaml 配置中生成出所有的 jenkins jobs 配置(xml文档),这样就不需要通过 UI 一个个添加 Jenkins 任务。
- 上述的任务分层、任务之间的优先级(相同优先级的任务是相互独立的,可并行构建)
2. Continuous Deployment 方案#
构建完成后,需要通过一个“镜像快照”的功能,将所有镜像的版本号、扫描出来,然后生成它们的 k8s yaml 配置文件,保存到 git 仓库中,并打上 tag(时间戳)。方便随时回退。
k8s 配置生成方面,我们目前是使用的自定义模板,通过字符串替换的方式进行填充。以后可能会考虑使用 kustomize。
最后通过一个部署的任务将指定的版本的 yaml 应用到集群中。
另外现在正在考察 jenkins-x,以后可能会将应用层的镜像构建到 k8s 部署,从 jenkins 移出来。
3. CI/CD 目前存在的问题#
3.1 Kubernetes 配置生成#
先说说 k8s 配置生成,试用了 kustomize,发现它功能还是比较弱,匹配不上我们现在的 yaml 配置生成的需求。也可能是我们目前的使用姿势不对吧。
3.2 GitOps#
另外也查了很多 Jenkins-X 的资料,它遵从 GitOps 开发流程,能检测 Git 仓库变更/Pull Request,直接生成 Docker 镜像并部署到 Preview - Stageing - Production 环境。
但是我 GitOps 和公司目前的这套构建体系不太契合:这种以 Git 仓库为中心的方式,好像只面向能生成最终的 Docker 镜像/K8s Pod 的 Git 代码,而不适合用于构建底层依赖包。
比如说我更新了一个基础层的依赖包 A,现在需要让所有的应用层项目都引用这个新版本依赖。应用层可能有几十上百个仓库,手动更新几乎不可能。
通过 GitOps 做不到让应用层的这上百个仓库自己更新一下底层依赖。只能借助一个 Jenkins 的 BatchJob,调用一下所有应用层依赖更新的子任务。
P.S. dotnet-nuget/java-maven 目前没有很多现代现代语言都有的 动态依赖(如
pyproject.toml
/package.json
/pubspec.yml
/go.mod
/Cargo.toml
,可以通过指定范围之类的方法灵活配置依赖) + 依赖快照(如pyproject.lock
/yarn.lock
/pubspec.lock
/go.sum
/Cargo.lock
,所有依赖的完整快照,保证当前依赖环境可复现) 这类的依赖管理方法,以及poetry update
/yarn upgrade
/flutter packages upgrade
/go get -u
/cargo update
之类的依赖升级命令。只有一个 xxx.csproj/xxx.xml 记录所有直接依赖的精确版本。要做自动化依赖管理,只能自己写脚本去访问 nuget api,读取并更新 csproj 文件。
查询资料 nuget versions 发现 nuget/maven 确实也支持基于范围的版本依管理,但问题是它们没有 xxx.lock 文件作为环境快照!这意味着使用灵活的依赖管理,可能导致历史环境无法复现。
另外 GitOps 自动化部署的流程也和公司目前的部署方法不切合。我们每个开发/测试人员都有一套自己的开发/测试环境,有的测试会需要使用特定版本的一套后端微服务。也就是说测试人员需要能够控制自己测试环境整套微服务的版本,比如回退到某个时间点、将版本固定在某个时间点。而且不能在工作时间自动更新测试环境的后端微服务。(否则测试到一半,后端滚动更新了微服务,那大半天的测试就作废了。)
GitOps 只适合一些扁平的应用,而对公司这种分层结构的代码就有点无所适从。
解决方法
- 已有的 Jenkins 分层更新构建流程不变,只在应用层进行 GitOps 方式的 CI/CD。因为应用层仓库相互之间是独立的。
- 给 GitOps 的构建部署提供专用环境:Preview - Stageing - Production,个人服务器的部署仍然使用现有流程。
旧的分层更新任务会修改应用层的 csproj 文件,这样就会触发 GitOps。
以这种方式进行结合是比较好的,GitOps 和现有的分层结构不会冲突。
3.3 网络问题/缓存问题#
每次构建 dotnet 程序时,都需要从公网拉取依赖,使用静态 Jenkins Slave 时,拉了第一遍后本地就有缓存了,不需要再拉第二第三遍。
可用 Serverless Jenkins (Jenkins-X)的话,每次都是启动新容器,岂不是每次都要拉依赖?这个挺费时间的。
暂时想到的解决方案是使用 Baget 的缓存功能,让私有 nuget 仓库来缓存这些依赖。
云上生产环境部署#
开发人员需要一个直观的 UI 界面,进行生产环境的灰度部署、监控分析。我们调研了很多管理平台:Rancher/Rainbond/KubeSphere,以及这些工具与 OAM/Istio 的契合度。
目前想到的比较好的一个方案,就是 flux+flagger+istio(istio 可换成 linkerd2),
此方案使用 github.com/gitee.com/coding.net 私有仓库保存生产环境的 k8s 配置,在内网通过 jenkins 生成 k8s 配置,在生产环境中通过 flux 监控该仓库的更新,然后 flux 使用 flagger 对其中的 k8s deployment 变更进行自动的灰度发布。
另外 Jenkins-X 有 Preview-Staging-Production 的一套 GitOps 部署流程,也可以一试。只是 jx 目前只提供了 CLI,没有 UI。上手可能有点难。