在上一篇基于 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 声明了两个容器,在这两个容器上共享多个存储卷,以实现共享文件的目的:

  1. 在 Jenkins 启动之前运行的初始化容器 installer,它按照 plugins.txt 先将插件安装到磁盘上,并为工作坊的所有微服务创建内置 Jenkins 任务
  2. installer 运行完成之后才启动的容器 jenkins,它就是 Jenkins Web 服务本身所在的容器

deployment/jenkins 的 Yaml 配置

deployment/jenkins 的 yaml 配置中,我们不难发现,installer 运行的具体过程位于脚本文件 /var/jenkins_config/apply_config.sh 中,它的内容是从 configmap/jenkins 挂载而来的。这个脚本中还将用到很多其他文件,比如安装插件用的 plugins.txt,它们都是从这个 configmap/jenkins 挂载而来。为了加速 Jenkins 插件的安装过程,我们在 installer 容器里使用 JENKINS_UCJENKINS_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 将内置支持 dotnetimage-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 的配置存储机制而预先写入配置来达到内置配置和任务的目的。

Jenkins 的自动化配置

deployment/jenkins 还让这两个容器共享 jenkins-homeplugin-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 命令行工具。接着执行以下工作:

  1. 等待 gogs-postgresqlgogs 部署完成,调用 Gogs 的 RESTful API 接口,完成用户注册和代码库导入工作
  2. 等待 nexus 部署完成,调用 Nexus 的 Scripting API(脚本编程)接口,完成管理员密码的修改

不难发现,虽然都是自动化配置,却使用了不同的技术。比起 RESTful API 接口,Nexus 的脚本编程接口由于是直接注入脚本,似乎功能会更灵活和强大。不过,过于强大的功能也通常会带来额外的安全风险。

总结

简单总结一下,在上面的讲解中,用到过的自动化技术有:

  1. 基于 Deployment 实现容器应用自动化部署(自动化部署 Jenkins、Gogs、Nexus 和 Sonarqube 等软件)
  2. 借助 Pod 的初始化容器(initContainer)实现提前运行自动化任务(在 Jenkins 主容器启动之前,在 initContainer 中安装插件)
  3. 借助 Pod 多容器共享存储卷来跨容器共享文件(在 initContainer 中安装插件后,由 Jenkins 主容器直接使用)
  4. 借助 Pod 环境变量向应用注入预置的配置(为 Jenkins 指定插件下载源)
  5. 借助 ConfigMap 向应用中直接挂载预置的配置文件(为 Jenkins 预设配置)
  6. 借助 Pod 就绪探针和存活探针,配合 kubectl rollout status 跟踪检测应用部署状态(等待 Gogs、Nexus 部署完成)
  7. 借助 Job 执行一次性任务(cicd-installer)
  8. 使用 Dockerfile 构建容器镜像(Jenkins 上的自定义 Slave 节点)
  9. 调用应用准备好的脚本自动完成配置(使用 Jenkins 提供的 install-plubins.sh 安装插件)
  10. 调用应用的 RESTful API 接口导入数据(为 Gogs 注册用户并自动导入代码库)
  11. 调用应用的 Script API 编程接口自动配置(向 Jenkins 和 Nexus 设置登录凭据)
  12. 使用模板引擎替换变量引用

到目前,我们详细地解读了如何有机地结合使用各种自动化技术,让工作坊的各个 CI/CD 软件在 Kubernetes 上完成启动之后,自动地完成各项自动化配置。由于 Kubernetes 部署 Yaml 文件以及各类自动化配置脚本都是文本文件,因此我们上一篇文章基于 Kubernetes 的基础设施即代码中关于“基础设施即代码”的两个要求仍然成立。

最后,工作坊的自动化脚本还没有提供存储支持,在实际的项目中应该会有对应的需求;基本上,只要在你的 Kubernetes 集群中配置好集群的存储类和自动存储供给支持,要支持存储并不困难。