在上一篇基于 Kubernetes 的基础设施即代码一文中,我概要地介绍了基于 Kubernetes 的 .NET Core 微服务和 CI/CD 动手实践工作坊使用的基础设施是如何使用代码描述的,以及它的自动化执行过程。
如果要查看基于 Kubernetes 的基础设施即代码架构全图,以及实现代码,请回到文章基于 Kubernetes 的基础设施即代码。
本文,我们深入探讨其中 CI/CD 软件部分的“基础设施即代码”的实现原理。
变量模板引擎
在工作坊中,由于所有与会者使用的都是同一个 Kubernetes 集群,因此我们需要一种方法来标识当前用户。Kubernetes 的命名空间提供的逻辑隔离功能可以很轻松地实现这个效果。因此,我们要为每个工作坊与会者创建他的一批命名空间:
cicd-<suffix>
用于部署 CI/CD 软件dev-<suffix>
作为“开发环境”,部署微服务stage-<suffix>
作为“预生产环境”,部署微服务
显然,对于每一个与会者来说,这里的 suffix
会有所不同,因此它是一个变量。除了在启动安装 CI/CD 软件时需要使用,这个变量还需要以某种形式保存到 Jenkins 上,因为当 Jenkins 运行部署任务时,它也需要知道目标命名空间的名字。
为了处理变量,我们自己发明了一个小型的“模板引擎”。其作用是,使用变量文件中指定的值,替换各个文件中的变量,输出最终的内容。这个模板引擎以双美元符号 $$
作为变量起始字符。打开 cicd-infra/jenkins.yaml
并搜索 $$
就可以发现其中大量地引用了各个变量。
模板引擎的实现位于 ./tmpl.sh
脚本文件,它能从指定的变量文件和环境变量读入各个变量的值,并将待处理文件中的变量占位符替换为对应的值,最后向标准输出(stdout)打印最终的文件内容。借助流水线指令 |
,这些内容随后被 kubectl apply -f -
命令读取,用于安装配置对应的 Kubernetes 资源。
在 kubelet 1.14 及以上的版本中,新增加了 kustomize 子命令。它提供更多编写模板化、嵌套式 Yaml 文件的方法。在工作坊中,我们需要兼容支持低一些版本的 kubelet,就不得不借助这样的模板引擎。
自动化安装 Jenkins
打开 cicd-infra/jenkins.yaml
会发现接近 600 行,可以说不短了。其中包含如下几个关键的 Kubernetes 资源:
- ServiceAccount
jenkins
是 Jenkins 本身,以及 Jenkins 用于生成容器镜像并部署微服务时所用的 Pod 要使用的集群账号 - RoleBinding
jenkins_edit
为上述集群账号赋予相应权限 - Service
jenkins-jnlp
供 Jenkins 构建运行器(Slave)启动期间连接 Jenkins 主机(Master)时用的集群 Service - Service
jenkins
是供 Ingress 用于把 Jenkins Web 界面暴露给用户用的集群 Service - Ingress
jenkins-ingress
是负责把用户请求转发到集群 Service 的流量入口处理规则 - ConfigMap
jenkins-jobs
可挂载为 Jenkins 内置任务的配置 - ConfigMap
jenkins
一系列用于初始化 Jenkins 的配置 - Deployment
jenkins
用于部署 Jenkins Web 服务
这里需要重点介绍的是 configmap/jenkins
,以及 deployment/jenkins
。后者挂载前者,以文件的方式读入内容并完成 Jenkins 的初始化配置工作。具体来说,deployment/jenkins
声明了两个容器,在这两个容器上共享多个存储卷,以实现共享文件的目的:
- 在 Jenkins 启动之前运行的初始化容器
installer
,它按照plugins.txt
先将插件安装到磁盘上,并为工作坊的所有微服务创建内置 Jenkins 任务 - 在
installer
运行完成之后才启动的容器jenkins
,它就是 Jenkins Web 服务本身所在的容器
从 deployment/jenkins
的 yaml 配置中,我们不难发现,installer
运行的具体过程位于脚本文件 /var/jenkins_config/apply_config.sh
中,它的内容是从 configmap/jenkins
挂载而来的。这个脚本中还将用到很多其他文件,比如安装插件用的 plugins.txt
,它们都是从这个 configmap/jenkins
挂载而来。为了加速 Jenkins 插件的安装过程,我们在 installer
容器里使用 JENKINS_UC
、JENKINS_UC_DOWNLOAD
这两个环境变量来让它从国内的服务器源下载插件。
configmap/jenkins/plugins.txt
定义了工作坊中我们需要用到的插件列表:
- git
- dashboard-view
- pipeline-stage-view
- workflow-aggregator
- kubernetes:1.20.0
其中的 kubernetes 插件让我们的 Jenkins 可以与它所在的 Kubernetes 集群集成,从而实现几乎能把任何容器镜像作为构建运行器(Slave)节点来使用,并且这些节点将以独立的 Pod 的方式“按需”在 Kubernetes 集群中运行,并自动连接到 Jenkins。这大大简化了 Jenkins 的运行器节点的维护工作。如果进一步研读 configmap/jenkins/config.xml
配置内容可以发现,我们的 Jenkins 将内置支持 dotnet
和 image-builder
两种 Slave 节点。
阅读 configmap/jenkins/apply_config.sh
可以看到,它使用了 Jenkins 支持的多种自动化配置功能:
- 运行
/usr/local/bin/install-plugins.sh
脚本文件可以预先安装指定的插件 - 在
/var/jenkins_home/init.groovy.d
目录中创建的 groovy 脚本将在 Jenkins 启动后自动运行,我们这里用来向 Jenkins 中植入容器镜像注册表的登录信息 - 通过预先定义
/var/jenkins_home/config.xml
及其他 xml 文件可以定制 Jenkins 的各类全局系统设置 /var/jenkins_jobs
目录下的子目录将自动被视为内置任务自动被 Jenkins 加载
上面第一种自动化功能,是内置在 Jenkins 安装包中的一个实用工具,它的源代码位于 GitHub 上。第二种自动化功能是 Jenkins 的初始化脚本,它支持以 Groovy 语言为 Jenkins 开发自动运行的脚本钩子。后面两种自动化功能则是根据 Jenkins 的配置存储机制而预先写入配置来达到内置配置和任务的目的。
deployment/jenkins
还让这两个容器共享 jenkins-home
和 plugin-dir
这两个存储卷,这样就可以让 jenkins
容器从 installer
容器继承已经初始化完成的 Jenkins 配置和插件。这样就确保 Jenkins 主容器运行起来时,就已经具备了已经下载好的插件,以及正确的全局配置。
自动化安装 Gogs 和 Nexus
比起 Jenkins 自动化的过程,Gogs 和 Nexus 的自动化安装就简单得多了。虽然 Gogs 需要 Postgre 数据库的支持,我们在工作坊环境中,还是为数据库配置了 emptyDir 类型的临时存储。因此并不提供持久化存储的支持。给 Nexus 提供的存储也一样用的是 emptyDir 临时存储。
值得一提的是这两个软件启动后的初始化操作。在工作坊的自动化脚本中,分别对这两个软件执行了如下自动化初始化:
- 在 Gogs 中自动创建账号,从 GitHub 导入各个微服务的源代码库,并配置 WebHook
- 修改 Nexus 的默认登录信息为
admin/admin
这些过程,都是借助独立的集群任务 cicd-installer
完成的。在该任务中,它首先读入当前 Kubernetes 环境给定的 Service Account 凭据,配置好 kubectl
命令行工具。接着执行以下工作:
- 等待
gogs-postgresql
和gogs
部署完成,调用 Gogs 的 RESTful API 接口,完成用户注册和代码库导入工作 - 等待
nexus
部署完成,调用 Nexus 的 Scripting API(脚本编程)接口,完成管理员密码的修改
不难发现,虽然都是自动化配置,却使用了不同的技术。比起 RESTful API 接口,Nexus 的脚本编程接口由于是直接注入脚本,似乎功能会更灵活和强大。不过,过于强大的功能也通常会带来额外的安全风险。
总结
简单总结一下,在上面的讲解中,用到过的自动化技术有:
- 基于 Deployment 实现容器应用自动化部署(自动化部署 Jenkins、Gogs、Nexus 和 Sonarqube 等软件)
- 借助 Pod 的初始化容器(initContainer)实现提前运行自动化任务(在 Jenkins 主容器启动之前,在 initContainer 中安装插件)
- 借助 Pod 多容器共享存储卷来跨容器共享文件(在 initContainer 中安装插件后,由 Jenkins 主容器直接使用)
- 借助 Pod 环境变量向应用注入预置的配置(为 Jenkins 指定插件下载源)
- 借助 ConfigMap 向应用中直接挂载预置的配置文件(为 Jenkins 预设配置)
- 借助 Pod 就绪探针和存活探针,配合
kubectl rollout status
跟踪检测应用部署状态(等待 Gogs、Nexus 部署完成) - 借助 Job 执行一次性任务(cicd-installer)
- 使用 Dockerfile 构建容器镜像(Jenkins 上的自定义 Slave 节点)
- 调用应用准备好的脚本自动完成配置(使用 Jenkins 提供的
install-plubins.sh
安装插件) - 调用应用的 RESTful API 接口导入数据(为 Gogs 注册用户并自动导入代码库)
- 调用应用的 Script API 编程接口自动配置(向 Jenkins 和 Nexus 设置登录凭据)
- 使用模板引擎替换变量引用
到目前,我们详细地解读了如何有机地结合使用各种自动化技术,让工作坊的各个 CI/CD 软件在 Kubernetes 上完成启动之后,自动地完成各项自动化配置。由于 Kubernetes 部署 Yaml 文件以及各类自动化配置脚本都是文本文件,因此我们上一篇文章基于 Kubernetes 的基础设施即代码中关于“基础设施即代码”的两个要求仍然成立。
最后,工作坊的自动化脚本还没有提供存储支持,在实际的项目中应该会有对应的需求;基本上,只要在你的 Kubernetes 集群中配置好集群的存储类和自动存储供给支持,要支持存储并不困难。