在上一篇基於 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 集群中配置好集群的存儲類和自動存儲供給支持,要支持存儲並不困難。