文章段落
DevOps 組合自 Development 和 Operations,代表文化、實踐和工具的結合。而 CI/CD 是實踐 DevOps 持續改善原則的方法,代表持續整合與持續交付或部署。本次示範如何將 DevOps 流程放上 Google Cloud(原 GCP),以更輕鬆地達成 CI/CD!
DevOps 教學前言:CI/CD 是什麼?如何達成?
CI:即「持續整合」
CI(Continuous Integration)指的是持續整合,也就是將新版的程式碼自動建置和測試,確保程式碼沒有問題。CI 的主要目的是更快地發現和解決錯誤、改善軟體品質,並不斷進行小幅度的改動,進一步減少驗證和發布更新所需的時間。因此 CI 讓開發人員能在流程的早期發現整合問題和錯誤,並簡化代碼分支和建置。
CD:即「持續交付」或「持續部署」
CD 可視團隊狀況定義為持續交付(Continuous Delivery)或持續部署(Continuous Deployment)。其中持續交付代表將程式變更推送到測試環境,確保它是一個隨時可以部署到正式環境的狀態,而持續部署則是讓程式完全自動部署到正式環境(不用手動部署),也是更成熟的 CD Pipeline。
如何達成 CI/CD?
在《DevOps 教學:利用 GitLab、GKE 五步驟達成 CI/CD》中我們除了要自己辛苦打造 GitLab Server,過程中還超多地雷…….。這次要輕鬆一點,把整個 DevOps 流程都放在全代管 GCP 服務上,不要再自己建Server了。除此之外,會再善用 Git 分版本來達成 GitOps,把整個環境用 Code 來處理,也就是所謂 “environments-as-code”。
今天的實作是參考 “GitOps-style continuous delivery with Cloud Build” 來實作的,各位有疑問可以深入研究這篇文章。
整個 DevOps 環境如下:
![DevOps (GitOps) Process](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/001-Cloud-Build-DevOps-1024x279.png)
過程中會建2個 Git Repositories,首先程式碼Push到 app repository,Cloud Build 會跑測試、建image然後push到 Container Registry。接下來 Cloud Build 又會更新 Deployment 設定檔,並且把設定檔push到 env repository,這個動作會觸發另一個 Cloud Build pipeline去套用到 GKE Cluster,如果成功,會把設定檔存在env repository的另一個Git分支。
我們會讓 app 和 env repo 分開,因為它們的用途不同。App repo 的使用者是「真人」,env 的使用者是「假人」?咦不是啦!是自動化系統如 Cloud Build 和其他會用到它的應用程式。而且這 env repo有幾個分支分別對應到特定的環境,也會參考特定的 container image,而 app repo 則沒有。
看完之後,你應該會有一個系統能做到:
- 在 Cloud Build 的history區分出成功和失敗的deployment
- 在env repo的production分支存取當前的設定檔
- 重新執行對應的 build 動作回滾到先前的版本
整個 DevOps 流程如下:
![DevOps (GitOps) Pipeline](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/002-Cloud-Build-DevOps-Process-1024x499.png)
DevOps 教學步驟一:基本環境建立
建立 GKE Cluster
gcloud container clusters create hello-cloudbuild –num-nodes 1 –zone us-central1-b
因為要等幾分鐘讓Cluster開好,等不及可以開另一個 Cloud Shell 登入 Git
git config –global user.email “xxx@xxx.com”
git config –global user.name “Aaron Lee”
![git config](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/git-config-1024x206.png)
執行 credential helper,它讓你的帳號連到 Cloud Source Repositories
![](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-32-1024x54.png)
接著建立2個 repo,app和env
![](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/create_repo-1024x129.png)
然後去
https://github.com/GoogleCloudPlatform/gke-gitops-tutorial-cloudbuild
把範例程式碼拉下來
![clone sample code](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/clone-sample-repo-1024x128.png)
接下來設環境變數,一定要設好,後面就不用每次key source repo那個長長的網址:
cd ~/hello-cloudbuild-app
PROJECT_ID=$(gcloud config get-value project)
git remote add google \ “https://source.developers.google.com/p/${PROJECT_ID}/r/hello-cloudbuild-app”
![設定環境變數](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/set-eve-variable-1024x179.png)
進app.py稍微改一下名字,證明自己真的有做過~
vim app.py
![app.py](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/modify-app_py.png)
來看一下Dockerfile
![app.py Dockerfile](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-33.png)
我們會用 Cloud Build 建image並儲存在 Container Registry。
然後我們要先把 commit id取出來,之後建image會用到它。
COMMIT_ID=”$(git rev-parse –short=7 HEAD)”
DevOps 教學步驟二:建立 Container Image
現在我們要建 image了!
加 tag 包含 commit id (這樣放到 GCR也可以識別)
gcloud builds submit –tag=”gcr.io/${PROJECT_ID}/hello-cloudbuild:${COMMIT_ID}” .
![gcloud build tag](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-34-1024x96.png)
完成!
![gcloud build tag successfully](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-35-1024x112.png)
DevOps 教學步驟三:建立 CI pipeline (就是建trigger)
直接去 Cloud Build 的頁面點擊 Create Trigger
![create trigger](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-36-1024x382.png)
取名 hello-cloudbuild
![edit cloud build trigger](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-37-1024x337.png)
在 Event 選擇 Push to a branch
![cloud build trigger event](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-38.png)
在 Source 部分選擇 hello-cloudbuild-app ,以及 ^master$ 做為 Branch
![cloud build source repository](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-39-1024x210.png)
![cloud build source branch](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-40-1024x215.png)
Build Configuration選擇 Cloud Build configuration file
檔名取 cloudbuild.yaml
![CI cloudbuild.yaml](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-41-1024x413.png)
OK的話就按 Create。
看一下這個檔案內容,這是 cloudbuild.yaml第一版的檔,只做CI
![CI cloudbuild.yaml details](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-42.png)
Trigger建好了,那我們來push code看看
![create trigger successfully](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/create_trigger-1024x382.png)
但是因為 code 沒有變動,所以沒有觸發 Cloud Build 的Trigger來建image
那我故意再改一下:
![Modify app.py](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-43.png)
送出了:
![commit app.py](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-44-1024x365.png)
結果有問題
![commit app.py and error](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-45-1024x154.png)
測試失敗?
![commit app.py and error](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-46-1024x336.png)
我們仔細看一下 Log:
Step #0 – “Test”: self.assertEqual(hello(), “Hello World!\n”)
Step #0 – “Test”: AssertionError: “Hello World! I’m GKE Master Aaron Lee 1228\n” != ‘Hello World!\n’
Step #0 – “Test”: – Hello World! I’m GKE Master Aaron Lee 1228 Step #0 – “Test”: + Hello World!
原來是它檢查我的句子是不是”Hello World!”,只不要是都算錯。
那我還是乖乖改回來:
![modify app.py](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-47.png)
再commit並push出去:
![commit app.py again](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-48-1024x341.png)
快去 Cloud Build 看看:
![check cloud build](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-49-1024x406.png)
果然成功了!
所以那個測試,是有寫一個規則在裡面嗎?
我找到 test_app.py ,來看看裡面有什麼:
![test_app.py details](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-50-1024x338.png)
原來就是檢查句子是不是 “Hello World!”
確認Image上來了:
![confirm image created](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-51-1024x269.png)
DevOps 教學步驟五:建立 CD Pipeline
接下來開始CD流程。
Cloud build 也可以用在CD pipeline, 每次commit push到 candidate分支就會開始跑。這個pipeline也會套用新的設定檔到GKE,如果成功的話,它會copy設定檔到production分支。整個流程有一些特性如下:
- Candidate分支會放所有要部署的歷史記錄
- Production分支則是只有成功的部署
- 你會在 Cloud Build 看到所有成功和失敗部署的細節
- 你可以重新執行 build 用來回滾到先前的版本,同時回滾也會更新production分支來真實反應deployment的歷史記錄。
接下來要注意的地方是,你會改掉前面建的 CI Pipeline,去更新candidate分支,觸發 CD pipeline。
授權 Cloud Build 來存取 GKE,像上次授權GitLab存取GCR和GKE是一樣的意思。
![grant permissions to Cloud Build service account](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/grant-cloud-build-service-account-1024x197.png)
接下來初始化 hello-cloudbuild-env repo,做2個分支
cd ~
gcloud source repos clone hello-cloudbuild-env
cd ~/hello-cloudbuild-env
git checkout -b production
“-b” 代表我建了一個分支叫 production,並且切換到這個分支
![create another branch production](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/create-and-swtich-branch-1024x207.png)
目前在production分支 把app repo的cloudbuild-delivery.yaml copy過來 變成cloudbuild.yaml 只是把檔案copy一份,然後就commit上去
cd ~/hello-cloudbuild-env
cp ~/hello-cloudbuild-app/cloudbuild-delivery.yaml ~/hello-cloudbuild-env/cloudbuild.yaml
目前沒有trigger,不會有任何影響。
這個檔的內容主要是部署設定檔到GKE,成功的話再把檔案放到production分支。
![cloudbuild.yaml details](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-52.png)
git add
.git commit -m “Create cloudbuild.yaml for deployment”
![commit cloudbuild.yaml](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/commit-and-push-cloudbuild-yaml-1024x160.png)
再建一個 candidate 分支,並且切換過去:
git checkout -b candidate
把這兩個都push,注意 candidate 是空的,沒錯喔不要懷疑。
git push origin production git push origin candidate
然後授權 Source Repository Writer角色權限給 Cloud Build service account:
PROJECT_NUMBER="$(gcloud projects describe ${PROJECT_ID} \
--format='get(projectNumber)')"
cat >/tmp/hello-cloudbuild-env-policy.yaml <<EOF
bindings:
- members:
- serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com
role: roles/source.writer
EOF
gcloud source repos set-iam-policy \
hello-cloudbuild-env /tmp/hello-cloudbuild-env-policy.yaml
![grant Source Repository Writer role Cloud Build service account](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/grant-source-repo-writer-1024x301.png)
接下來建立第二個Trigger:
![create trigger hello-cloudbuild-deploy](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-53-1024x273.png)
![trigger event](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-54.png)
![cloud build trigger source repository](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-55-1024x214.png)
![cloud build trigger source branch](https://storage.googleapis.com/cloudace-tw-blog/1/2021/03/image-56.png)
它的意思是,只要在env這個repo裡,有push到candidate這個分支,就執行cloudbuild.yaml (主要會部署 GKE, 並且在production 分支產生k8s.yaml做一個commit)
那這支 cloudbuild.yaml 目前還沒產生,接下來才會出現。(感覺在鋪一個很長的梗……)
DevOps 教學步驟六:更改 CI pipeline 去觸發 CD
現在要改檔名,把 cloudbuild-trigger-cd.yaml 改成 cloudbuild.yaml
就像上面說的,它會部署 GKE 並且copy到 production
我們看一下這個檔案,因為內容很多又雜,我分段貼出來並且加了一些註解,方便大家看懂(閱讀課開始……):
![cloudbuild-trigger-cd.yaml details part1](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/截圖-2021-04-06-上午11.05.52.png)
後面還有…
![cloudbuild-trigger-cd.yaml details part2](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/截圖-2021-04-06-上午11.07.00.png)
還有…
![cloudbuild-trigger-cd.yaml details part3](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/截圖-2021-04-06-上午11.08.24.png)
而我們的另一個Trigger,是在 ENV 裡提交 Candidate 之後所觸發執行的工作,來看一下內容(第二堂閱讀課……):
![another trigger cloudbuild.yaml part1](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/截圖-2021-04-06-上午11.12.39.png)
後面還有…
![another trigger cloudbuild.yaml part2](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/截圖-2021-04-06-上午11.13.15-1.png)
再忍耐一下,快看完了…
![another trigger cloudbuild.yaml part3](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/截圖-2021-04-06-上午11.13.43-1.png)
然後把這個push到 app 自己的 repo:
cd ~/hello-cloudbuild-app
git add cloudbuild.yaml
git commit -m “Trigger CD pipeline”
git push google master
這樣把app自己的檔案commit出來,就會Trigger 原本app裡的cloudbuild.yaml (只有CI),以及Env裡的Candidate Trigger (CD: 部署到GKE):
![commit cloudbuild.yaml to trigger CICD pipeline](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/commmit-trigger-cd-pipeline-1024x298.png)
接下來看到 Cloud Build 開始跑了:
![Cloud Build running](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/running-cloudbuild-1024x285.png)
過幾秒之後看到成功:
![Cloud Build running successfully](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/Build-success-1024x279.png)
後來發現 Cluster 忘記建了,快來建一下。
(忘記是小事,因為前面就可以花好幾天的時間確認流程,太早建Cluster就只能放在那邊浪費錢,就是等到它提示再建就好了)
![create GKE cluster](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/create-hello-cloudbuild-cluster-1024x185.png)
等開好,來按一下retry (貼心的功能不用再重新commit或push)
![retry cloud build](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/retry_commit-1024x334.png)
成功了!
![retry cloud build successfully](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/deploy-successfully-1024x258.png)
來看一下Service,看到 Load Balancer 有建起來,並且有一個外部IP:
![confirm GKE service](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/GKE-service-1024x357.png)
點擊下去看到網頁,終於成功了!
![confirm GKE service successfully](https://storage.googleapis.com/cloudace-tw-blog/1/2021/04/gke-cicd-hello-world.png)
以上就是完整的CI/CD流程,過程中有任何錯誤都可以看得到,因為在 GCP 上多了 DevOps 提到的「透明化」原則,而且都是用代管式的 GCP 服務來達成,其中比較複雜的是 Google 又加入了 Git 管理 (所以在過程中看起來有點亂),又形成了 GitOps,透過Infra structure as a cod 提高了自動化,並減少大量手工(Toil)出錯的風險,對我們未來的產品開發速度大有幫助。
如果還不習慣全代管,要使用 GitLab 的話,可以回來看這篇文章: