實戰攻略:利用GitOps在Kubernetes上實現持續交付

2019-09-16 08:01:57

作者 | Luc Juggery
譯者 | 核子可樂
GitOps 是 Weaveworks 提出的一種持續交付方式,其工作原理,是利用 Git 作為宣告基礎設施與應用程式的單一事實來源。

本文我們將通過一個簡單的專案,瞭解如何設定典型的 CI/CD 流水線,而後通過修改將 GitOps 新增到其中。同時,我們還將演示 Flux——GitOps 的核心元件。幾周之前,Flux 已經被 CNCF 正式接納為沙箱培養專案。

我們要做什麼  

下面,我們先來看看整個流程中的具體操作步驟:

  • 對 GitOps 進行簡單介紹

  • 設定一個簡單的專案,並在 GitLab 之內進行管理

  • 整合一個 Kubernetes 叢集

  • 設定一條典型的 CI/CD 流水線

  • 利用 GItOps 處理其中的 CD 部分

什麼是 GitOps?  

GitOps 是一種持續交付實現方式。其將 Git 作為宣告基礎設施與應用程式的事實來源。當對 Git 進行變更時,自動交付流水線也會對您的基礎設施進行相應變更。

將變更部署至叢集:push 與 pull  

在一條典型的 CI/CD 流水線當中,CI 工具負責執行測試、構建映象、檢查 CVE 並將新映象重新部署至叢集當中,具體如下圖所示。

典型的 CI/CD 流水線(圖片來源:Weaveworks)

GitOps 方法的區別在於,其中的部署部分不再由 CI 工具完成,而是由操作程式通過叢集內 Pod 中的執行程序完成(由 Flux 負責實現)。

包含 GitOps 的 CI/CD 流水線 (圖片來源:Weaveworks)

相關元件  

下圖所示為在 Kubernetes 叢集當中使用 GitOps 時所需要用到的各元件。

在 Kubernetes 叢集當中的各 GitOps 元件 (圖片來源:Weaveworks)

為了簡單起見,Flux 守護程式會不斷執行並檢查是否存在新的 Docker 映象。檢測到新映象之後,它會呼叫 API Server 對當前正在執行的部署加以更新。

在本文的最後一部分中,我們將設定 Flux 並利用它部署一款簡單的應用程式。

我們的專案  

在這裡,我們使用一個非常簡單的 Flask 應用程式。專案的複雜程度並不重要,真正重要的是理解整個 CI/CD 流程的實現方式。

原始碼  

我們只需要考慮以下檔案:

  • app.py 用於公開一個單獨的 HTTP 端點並返回一個字串

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000)

- requirements.txt 用於定義 app.py 所需要的依賴性,即 Flask 庫

Flask==1.0.2
  • Dockerfile 用於通過原始碼構建起一套映象

FROM python:3-alpine
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD python /app/app.py
確保一切正常  

我們先通過以下命令為我們的應用程式建立第一套 Docker 映象:

$ docker image build -t hello:1.0 .

在映象構建完畢之後,我們可以執行容器以使用該映象。

$ docker container run -p 8000:8000 hello:1.0
* Serving Flask app “app” (lazy loading)
* Environment: production

注意:不要在生產環境當中使用該開發伺服器,請使用生產 WSGI 伺服器作為替代。

* Debug mode: off

* Running on http://0.0.0.0:8000/ (Press CTRL+C to quit)

我們的伺服器在監聽埠 8000,如下圖所示。

GitLab 專案  

我們將利用 GitLab 管理這款應用程式,下面建立一個名為 hello 的新專案:

在 GitLab 當中建立一個新專案

接下來,我們可以為該應用程式資料夾進行 git 初始化,並將一切 push 至 GitLab 專案當中:

$ git init
$ git remote add origin [email protected]:lucj/hello.git
$ git add .
$ git commit -m "Initial commit"
$ git push -u origin master

幾秒之後,我們可以通過 GitLab 的 Web 介面看到該專案中的三個檔案。

程式碼的首次 commit

迎接 Kubernetes  

由於需要在 Kubernetes 叢集上部署我們的應用程式,所以這裡我們將使用 GitLab 的 Kubernetes 整合功能將外部叢集的配置匯入專案當中。

建立一個託管叢集  

DOKS (DigitalOcean 託管 Kubernetes 叢集) 是我個人最喜愛的解決方案,其易於設定及使用。我們可以通過 DigitalOcean Web 介面或者使用專門的 doctl 命令列介面進行建立。在本示例中,我們將設定一套包含 3 個工作節點的叢集,其中管理器節點由 DIgitalOcean 替我們管理。

通過 DigitalOcean 的 Web 介面建立託管 Kubernetes 叢集

配置基礎設施以及建立叢集大概需要幾分鐘的時間。完成之後,我們需要檢索 kubeconfig 檔案,以確保我們的 kubectl 客戶端能夠與叢集的 API 伺服器通訊。我們將使用 doctl 命令並將該配置儲存在 k8s-demo.cfg 檔案當中:

$ doctl k8s cluster cfg show k8s-demo > k8s-demo.cfg

接下來,我們配置 kubectl 以使其與我們的叢集進行通訊,從而設定 KUBECONFIG 環境變數:

$ export KUBECONFIG=$PWD/k8s-demo.cfg

搞定。下面我們來看看目前的叢集狀態:

$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-demo-rlf5 Ready 2m10s v1.15.2
k8s-demo-rlfh Ready 2m40s v1.15.2
k8s-demo-rlfk Ready 2m33s v1.15.2
與 GitLab 專案相整合  

通過 GitLab 的 Web 介面,我們可以輕鬆將外部 Kubernetes 叢集整合至專案當中。我們只需要進入 Operations > Kubernetes,而後點選 Add Kubernetes cluster 即可:

Kubernetes 叢集的整合操作

接下來,我們需要選擇 Add existing cluster 選項卡。在這裡,我們需要填寫幾個欄位,其中第一個欄位可以從配置檔案當中輕鬆檢索到:

需要在 Kubernetes 叢集整合過程中填寫的欄位

  • 叢集名稱

  • API Server 的 URL

  • 叢集的 CA 證書

要向 GitLab 當中新增叢集 CA 證書,我們需要解碼配置中指定的證書(以 base64 形式編碼)。

$ kubectl config view --raw \
-o=jsonpath='{.clusters[0].cluster.certificate-authority-data}' \
| base64 --decode
  • 服務令牌

整個令牌獲取過程分為幾個步驟。我們首先需要建立一個 ServiceAccount,併為其提供 cluster-admin 角色。具體操作命令如下:

$ cat apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab-admin
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: gitlab-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: gitlab-admin
namespace: kube-system
EOF

在 ServiceAccount 建立完成之後,我們開始檢索相關 Secret:

$ SECRET=$(kubectl -n kube-system get secret | grep gitlab-admin | awk '{print $1}')
add extract its JWT token, the one we need to enter in the Service Token field in the GitLab interface:
$ TOKEN=$(kubectl -n kube-system get secret $SECRET -o jsonpath='{.data.token}' | base64 --decode) && echo $TOKEN

在對叢集整合進行驗證之前,我們取消原本被選中的 GitLab-managed-cluster 複選框,這代表著我們將自行管理名稱空間。

在叢集整合完畢後,GitLab 即可通過 Helm 圖表一鍵安裝多款應用程式。不過這不是今天討論的重點,因此不再贅述。

Kubernetes 叢集與我們的 GitLab 專案順利整合

設定一條典型的 CI/CD 流水線  

我們首先在專案的 root 目錄處新增一個.gitlab-ci.yml 檔案。該檔案用於定義每當有新程式碼被提交至程式碼庫時,所應觸發的具體操作。

在檔案開頭,我們首先定義流水線中的不同階段:

stages:
- package
- test
- push
- deploy

在各個階段當中,我們進一步定義需要執行的操作:

  • 其中的 package 階段負責利用原始碼建立一套 Docker 映象,並使用一個臨時標籤(我們稍後將詳加解釋)將其推送至專案的 GitLab 映象庫。

build:
image: docker:stable
stage: package
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:tmp .
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:tmp
only:
- master
  • 而 test 階段則負責利用新建立的映象執行一套容器,並確保返回的訊息以“Hello”為開頭。

test:
image: docker:stable
stage: test
services:
- docker:dind
script:
- docker run -d --name hello $CI_REGISTRY_IMAGE:tmp
- sleep 10s
- TEST=$(docker run --link hello lucj/curl -s http://hello:8000)
- $([ "${TEST:0:5}" = "Hello" ])
only:
- master

接下來的 push 階段向該映象中 push 新的標籤,第一個標籤基於該 git 提交的 hash,第二個為當前分支的名稱(在本示例中為 master,因為我們只需要在主分支上進行操作)。最後,將這些新標籤 push 回 GitLab 庫。

push:
image: docker:stable
stage: push
services:
- docker:dind
script:
- docker image pull $CI_REGISTRY_IMAGE:tmp
- docker image tag $CI_REGISTRY_IMAGE:tmp $CI_REGISTRY_IMAGE:$CI_BUILD_REF
- docker image tag $CI_REGISTRY_IMAGE:tmp $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_BUILD_REF
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
only:
- master

最後,deploy 的階段負責在我們的 Kubernetes 叢集之內建立 / 更新應用程式。我們將在 k8s 資料夾當中定義 2 個 manifest 檔案:Deployment 用於我們我們 Web 伺服器的 Pod,而 Service 則用於將其面向外部公開。

我們首先定義以下 k8s/deploy.tpl 模板。它將被用於在 deploy 階段生成用於指定 Deployment 資源的 k8s/deploy.yml 檔案。這套模板將定義 Deployment,用於管理根據 registry.gitlab.com/lucj/hello 映象建立的 Pod 的一套單獨副本。

在這套模板中,我們使用名為 GIT_COMMIT 的佔位符替換實際提交的 hash,具體如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
labels:
app: hello
spec:
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: registry.gitlab.com/lucj/hello:GIT_COMMIT

我們還在 k8s/service.yml 當中定義了 Service 資源,用以向外部公開我們的應用程式。Service 的型別為 LoadBalancer。

apiVersion: v1
kind: Service
metadata:
name: hello
spec:
type: LoadBalancer
ports:
- name: hello
port: 80
targetPort: 8000
protocol: TCP
selector:
app: hello

需要在 deploy 階段執行的操作如下:

deploy:
stage: deploy
image: lucj/kubectl:1.15.2
environment: test
script:
- kubectl config set-cluster my-cluster --server=${KUBE_URL} --certificate-authority="${KUBE_CA_PEM_FILE}"
- kubectl config set-credentials admin --token=${KUBE_TOKEN}
- kubectl config set-context my-context --cluster=my-cluster --user=admin --namespace default
- kubectl config use-context my-context
- cat k8s/deploy.tpl | sed 's/GIT_COMMIT/'"$CI_BUILD_REF/" > k8s/deploy.yml
- kubectl apply -f k8s
only:
- master

另外幾點注意事項:

  • 這一階段需要在包含 kubectl 客戶端的映象上下文中執行

  • 從 GitLab 自動設定的環境變數當中檢索叢集資訊,這些資訊將用於設定 Kubernetes 的上下

  • Deployment 資源根據模板檔案建立而成,其中的 CIT_COMMIT 佔位符將被替換為 $CI_BUILD_REF 環境變數當中的實際提交資訊

  • Service 與 Deployment 資源分別位於 k8s/service.yml 與 k8s/deploy.yml 當中,通過常用的“kubectl apply”命令進行建立 / 更新

備註:這條流水線非常簡單,但並不是最優方案。我們只是利用它來展示不同的流程。

專案測試  

下面,讓我們把這些變更 push 到 GitLab 專案當中,而後檢查由此觸發的 CI/CD 流水線:

$ git add k8s
$ git commit -m ‘Add K8s resources’$ git add .gitlab-ci.yml
$ git commit -m ‘Add GitLab pipeline’$ git push origin master

這會觸發 GitLab 流水線,具體如下圖中的 Web 介面所示:

在該流水線的 deploy 階段(最終階段),Deployment 與 Service 需要進行首次建立(因為之前並不存在)。由於 Service 的型別為 LoadBalancer,因此我們可以從下圖中看到 DigitalOcean 基礎設施上會建立對應的負載均衡器資源。

利用與該 Load Balancer 相關聯的外部 IP 地址,我們可以在指向執行有應用程式的底層 Pod 的埠 80 上訪問自己的應用程式。

這證明 Service 與 Deployment 都已經正確建立完成。

下面,我們需要對 app.py 做出一點調整,把返回內容由“Hello World!”改為“Hello from Kube”。

from flask import Flask
app = Flask(__name__)@app.route("/")
def hello():
return "Hello from Kube"if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000)

我們對這些變更進行 commit 與 push:

$ git add app.py
$ git commit -m 'change message to Hello from Kube'
$ git push origin master

新的 CI/CD 流水線由此觸發,我們可以重新整理瀏覽器並看到如下結果:

當然,我們在這裡設定的只是一條簡單的流水線。對於真實場景中的應用程式,還需要新增一些額外的增強功能。例如,我們可能需要考慮以下步驟:

  • 額外的測試

  • 映象掃描,用於確保該映象不包含任何 CVE 漏洞(或者至少不存在高危漏洞)

若需瞭解更多與映象掃描相關的細節資訊,請參閱:

https://medium.com/better-programming/adding-cve-scanning-to-a-ci-cd-pipeline-d0f5695a555a

新增 GitOps  

現在,我們需要再次修改這條 CI/CD 流水線,以利用 GItOps 方法處理其中的 CD 部分。以下結構展示了 GitOps Deployment 工作流中所涉及的元件。

GitOps Deployment 工作流(圖片來源:Weaveworks)

基本上,每當系統在映象登錄檔中檢測到新的映象標籤,我們就要利用 Flux 操作程式(執行在 Pod 內的叢集當中)對應用程式進行重新部署。

安裝 Flux  

Flux 可以通過 Deployment 或者 Helm 進行手動安裝。在本文中,我們也使用手動方案。第一步,就是對 fluxcd 庫進行 clone:

$ git clone https://github.com/fluxcd/flux && cd flux

接下來,在 Deployment 規範之內(deploy/flux-deployment.yaml)變更以下引數:

  • [email protected]:lucj/hello, 用於告知 Flux 檢測哪個 Git 庫

  • --git-path=k8s, 在此庫當中只考慮 k8s 資料夾(我們的 Kubernetes manifests 檔案就位於該資料夾內)

  • --git-ci-skip, 此選項允許我們在 Flux 完成對 GitLab 專案庫的更新之後(包括標籤與 Deployment 資源更新),跳過 CI 流水線

現在,我們可以將 Flux 部署至叢集當中了:

$ kubectl apply -f deploy
serviceaccount/flux created
clusterrole.rbac.authorization.k8s.io/flux created
clusterrolebinding.rbac.authorization.k8s.io/flux created
deployment.apps/flux created
secret/flux-git-deploy created
deployment.apps/memcached created
service/memcached created

由此建立的幾種資源:

  • ServiceAccount、ClusterRole 以及 ClusterRoleBoinding,用於為 Flux Pod 提供執行所需的驗證 / 授權

  • Flux 操作程式

  • 用於 memcached 的 Service 與 Deployment,由 Flux 用於快取映象元資料

$ kubectl get pods
NAMESPACE NAME READY STATUS RESTARTS AGE
default flux-dcb965db7-pn97k 1/1 Running 0 56s
default memcached-554f994578-t2tss 1/1 Running 0 56s
...

檢視 Flux Pod 日誌,我們會看到一條錯誤訊息,因為 Flux 無法讀取專案的 Git 庫。

“許可權被拒絕(公鑰)。嚴重:無法從遠端庫中讀取。請確保您具有正確的訪問許可權,且目標庫存在。

為了解決這個問題,我們可以使用 fluxctl 實用程式檢索安裝期間所公開 ssh 金鑰。

$ fluxctl identity
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCx4fk4YjcM7cP1FL/AKWtHpN+cg9/Qz1p5dzAlsFLMKilUUy0uCQQmaptXDZQGaZrbvNSyezgT5/yH6qau6W6ICoLYAzBku47PoWlqbUfcbPhMxHSfivjv7s4lSeUE+u3kR2opROxdyHHL+VQMI6n9Xc7qnTq6YC+VJ+RkoUUd0bgBC+Rg/aMURLD9mkAVzmWw6+Y8QAJMVNMzNDgId+8iSHKtOYsHqoxg4GqexdB1R5goE0ChBU9DPsiqLfk8jzuD2I3xuZeGW6or+/JHxa/6vO8lX+of1ZGZGZKr5i3E4OIehSwFUP2A/ypeqXEEI5gmO1s2YrM49jpS+jW4oUMP

接下來,將該金鑰新增至我們的 GitLab 庫,為其提供建立 / 更新所需要的讀取 / 寫入訪問許可權。

修改原有流水線  

由於 Flux 負責對專案作出的變更進行部署,因此我們需要刪除之前建立的.gitlab-ci.yml 檔案中的 Deployment 階段,其它內容則保持不變。現在的.gitlab-ci.yml 如下所示,其中與叢集 API Server 互動的 kubectl 已經被刪除:

stages:
- package
- test
- pushbuild:
image: docker:stable
stage: package
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:tmp .
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:tmp
only:
- mastertest:
image: docker:stable
stage: test
services:
- docker:dind
script:
- docker run -d --name hello $CI_REGISTRY_IMAGE:tmp
- sleep 10s
- TEST=$(docker run --link hello lucj/curl -s http://hello:8000)
- $([ "${TEST:0:5}" = "Hello" ])
only:
- masterpush:
image: docker:stable
stage: push
services:
- docker:dind
script:
- docker image pull $CI_REGISTRY_IMAGE:tmp
- docker image tag $CI_REGISTRY_IMAGE:tmp $CI_REGISTRY_IMAGE:$CI_BUILD_REF
- docker image tag $CI_REGISTRY_IMAGE:tmp $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_BUILD_REF
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
only:
- master

此外,我們也可以刪除 k8s/deploy.tpl 模板檔案,因為我們不再需要利用該檔案對 Deployment manifest 進行更新。相反,我們將在 Deployment 中使用以下 k8s/deploy.yml,確保 Flux 在每次檢測到新的映象標籤時都會執行更新。

apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
annotations:
flux.weave.works/automated: "true"
flux.weave.works/tag.hello: regexp:^((?!tmp).)*$
labels:
app: hello
spec:
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: registry.gitlab.com/lucj/hello:master

用於該 Deployment 的 Flux 配置在 annotations 鍵內完成:

  • flux.weave.works/automated: “true”, 用於啟用該資源的自動重新部署

  • flux.weave.works/tag.hello: regexp:^((?!tmp).)*$, 確保具有 tmp 標籤的臨時映象不會被納入使用

實際測試  

我們對 app.py 中的程式碼進行了如下變更,因此其現在會返回“Hello from Flux”。

from flask import Flask
app = Flask(__name__)@app.route("/")
def hello():
return "Hello from Flux"if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000)

接下來將修改後的內容 push 至 GitLab。

$ git rm k8s/deploy.tpl
$ git add k8s/deploy.yml .gitlab-ci.yml app.py
$ git commit -m 'CD with Flux'
$ git push origin master

檢視 GitLab 介面,我們會看到該流水線已經被觸發了多次。

已經建立的多條流水線(其中幾條被直接跳過)

有條流水線的觸發原因是我們做出了變更,其它幾條則由 Flux 在對 master 分支上的 Deployment manifest(k8s/deploy.yml)以及 flux-sync 分支上的標籤進行更新時觸發。除了這兩項操作之外的其它被觸發流水線被直接跳過(相關操作並未執行),這是因為我們在 Flux 配置當中使用了—git-ci-skip 選項(如果不這樣,流水線將一直迴圈執行)。

然後,我們可以再次重新整理瀏覽器以檢視應用程式的最新版本。

可以看到,當 Flux 操作程式定期檢查新的映象標籤時,其會發現 CI 流水線執行期間出現的程式碼變更,並據此自動更新 Deployment。

   總結     

在本文當中,我希望向大家介紹 GitOps,並通過一個簡單的示例說明它如何與 GitLab CI 流水線配合起效。大家也可以根據需求增強其中某些功能,例如在流水線當中定義更多階段,使用 sermver 命名映象標籤等……總之,我希望這篇簡單的文章能夠讓大家對整個方法擁有基本的瞭解。

GitOps 在很長一段時間內得到了行業的高度關注,感興趣的朋友可以點選此處通過官方文件瞭解更多細節資訊。

您已經開始使用 GitOps 方案了嗎?希望在評論中看到您的分享心得。




活動推薦

已同步到看一看



熱點新聞