【CICD】GitLab Runner 開始跑吧!

GitLab Runner 搭配.gitlab-ci.yml完成CICD

Martin
13 min readMay 16, 2023

CICD 是所有仍有更新需求專案的必經之路,因此熟悉至少一套CICD流程我認為也是後端工程師必備的技能之一!而這次有幸在公司整合專案的過程中,自告奮勇地接下這份工作,得到這難能可貴的成長機會,讓我能從頭建立一套完整的建置流程,讓我對CICD整體的流程有更深刻的認識

簡介

在動手開幹前,當然要先了解這次的任務目標為何(What),才會進展到怎麼實做(How)的階段。CICD 其實是三個不同概念的縮寫:

  • CI(Continuous Integration): 持續整合,透過撰寫自動化測試、自動建置環境及服務,將新功能頻繁且穩定地合併進專案
  • CD(Continuous Delivery): 持續交付,將整合過新功能的專案持續交付至正式上線前的環境(dev/sit/uat…)
  • CD(Continuous Deployment): 持續部署,將整合過新功能的專案持續部署至各環境,包含正式上線(prod)
    從兩個CD 的概念可以發現,Continuous Deployment 其實隱含著Continuous Delivery 的概念,只是相較自動部署(Deployment),持續交付的概念強調最後一步的正式環境部署仍靠人工手動執行,多了一層把關的意味(個人淺見,各路大神歡迎提出其他想法討論)

因此不管CICD 最後的D 指的是Delivery 還是Deployment,這整套流程的核心概念仍是圍繞在自動化整合、發佈程式至各環境的主機上,以降低人工流程的不可靠性。

在了解CICD 為何以後,動手前要先調研有哪些工具可以達成這項任務,首先時至今日其實各Git Server 早就各自有整合相對應的CICD 工具,諸如GitHub 有GitHub Action、GitLab 有Pipeline,除了相依於Git Server 底下的工具外,CICD 界不可不知的工具就屬Jenkins 莫屬。當然除了這些,還有好多好多工具呢...

source

但因為公司的專案就是放在GitLab 上,因此來到本篇主題: CICD in GitLab

實做

runner

在GitLab 上的CICD 流程是由兩個部分組成:.gitlab-ci.yml(腳本)+runner(執行腳本的工人),儘管沒手動設runner,GitLab 上本來就有預設的runner,因此只要專案有.gitlab-ci.yml 腳本,推上GitLab 時就可以跑CICD。

Settings > CICD > Runners > Shared runners,GitLab 的預設runner

GitLab 預設的runner 是用docker machine 作為executor,因此若不自己設定一個runner 的話,就需要按照docker machine 執行指令的命令來撰寫,雖然用預設很方便,但缺點就是較為綁手綁腳,那要如何建一個自己的runner 呢?

找一個想架設runner 的server,在該機器上安裝runner。連結這有列出不同作業系統要如何安裝runner,舉linux 環境為例

curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
sudo yum install gitlab-runner

安裝好runner 後,下一步就是要在專案中註冊該runner:sudo gitlab-runner register,過程中需要輸入專案的URL 及token,在上面的圖其實就有註冊runner 所需要的資訊(只是我把資訊碼掉了)

過程中還會需要為這runner 設定tag,這tag 是用在之後腳本是否要指名特定runner 來執行。最後須要為這runner 建立一個executor,之後用來跑腳本,常用的有docker、shell、Kubernetes……。至此已經成功為專案註冊一個自己架設的runner。更詳細的教學可參考底下的影片

.gitlab-ci.yml

有了runner 後,要使用GitLab 內建的CICD 功能的第一步,就是撰寫一個.gitlab-ci.yml 的yaml檔案,就像要用docker compose 必須要先有一個docker-compose.yml (廢話)。

stages

有了yaml 檔以後,下一步就是思考在這個CICD 流程中你預期總共要做哪些事,一個較為完整的流程應該是先進行功能測試,測試過了才進行環境整合,最後才部署至各環境上,因此流程應該是test => build => deploy,若在任一階段失敗就不必進到下一階段,在yaml 檔上有相對應的實做:stages

stages:
- test
- build
- deploy

接這就是列出各階段所要做的事情(job),名字可任取,用tags 指定要哪些runner 來執行此任務

# job1
dev-deploy:
stage: deploy
tags:
- dev
script: ...

若同一階段有許多事情要做,就讓各job 的stage 設為同一個,如此這些工作就會一起跑

# job1
dev1-deploy:
stage: deploy
script: ...

# job2
dev2-deploy:
stage: deploy
script: ...

若在同一階段的工作也想要設定一個先後相依順序,則可用needs 來設定

# job1
dev1-deploy:
stage: deploy
script: ...

# job2
dev2-deploy:
stage: deploy
needs:
- dev1-deploy # 如此dev2就會等dev1成功後才開始跑,若dev1失敗則dev2也不會執行
script: ...

variables

在一整份yaml 檔案中難免有些字是會重複地使用,此時就可以用變數來設定這些重複用到的字。這些variables 也有作用域的概念,若是定義在job 之外可理解為全域變數,每個job 都可以取用;job 之內則為該工作區域之變數,僅存在於該job 動作之內

stages:
- test
variables:
global_x: '1000'

test-job:
stage: test
variables:
job_x: '5487'

除了自定義的變數外,GitLab 在CICD 的流程也有預設定義一些變數,諸如:發commit 的branch(CI_COMMIT_BRANCH)、觸發CICD 的trigger 種類(CI_PIPELINE_SOURCE)...等等,更多可參考連結

workflow

可以透過workflow 控制整個流程,像是如果要限制CICD 僅在develop分支上進行的話

workflow:
rules:
- if: $CI_COMMIT_BRANCH == 'develop'
when: always
- when: never

也可以在workflow 中替換全域變數的值

variables:
PROJECT1_PIPELINE_NAME: 'Default pipeline name' # A default is not required.

workflow:
name: '$PROJECT1_PIPELINE_NAME'
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
PROJECT1_PIPELINE_NAME: 'MR pipeline: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME'

特別要注意在workflow 中設定rules 若沒有任一條件為true 的話,則整個CICD 流程就都不會執行

job

終於進到整個CICD 流程的重點,CICD 流程細節該做甚麼事情,都是寫在各個job 裡,更細節地說要做的事都是寫在script 裡。
以這次工作上為例:需要在develop 分支有新commit 時,將develop 分支推至遠端 server 上,並用pm2 重啟服務。

要更新遠端機器上的服務,首先當然是要連進伺服器,這次選擇用ssh 來連線。可以選擇每次連線時都輸入一次使用者、密碼,當然也可以用更簡單的設定公私鑰的方式一勞永逸,聰(懶)明(惰)的工程師當然選擇後者XP
首先進入runner server:sudo su — gitlab-runner,透過指令在CICD runner 的伺服器上產生一對公私鑰:ssh-keygen。過程中設定的passphrase 之後也需要在GitLab 設為環境變數,方便之後連線使用。
接下來可以手動將公鑰存進欲連進的server 裡( ~/.ssh/authorized_keys),或是輸入指令:ssh-copy-id -i your_key_path username@server_host
最後將私鑰設為一個變數方便之後使用。(建議設定在環境變數裡,不然明白地寫在yaml 檔裡算甚麼私鑰XD)

Settings > CICD > Variables 設定環境變數

設定好私鑰變數後,下一步就是要在跑CICD 時取用,這部分要設置在job 開始跑流程(script)之前,也就是設定在before_script 階段

# job
dev1-deploy:
stage: deploy
before_script:
- eval $(ssh-agent -s)
- ssh-add <(echo "SERVER_PRIVATE_KEY")

script:
- ssh SERVER_USER@SERVER_URL

這樣”正常”來說就可以連進遠端server,但這樣寫通常會碰到一個問題:第一次遠端連線終端機都會再次確認你是否要連線至該server,通常人工連線時會需要輸入yes,但在這沒給出任何回應,因此會連線失敗。所以上面連線的指令需在ssh 後方加上-o StrictHostKeyChecking=no參數,就可以避免遇到第一次連線會被問的檢查
"正常"來說這樣應該就真的可以成功連線進server,然而我當初在執行時甚至還沒碰到連線就先噴錯了orz 和這篇遇到的問題一模一樣

我上網找了許多方法,最後成功讓我在這篇找到解方!

  before_script:
- eval $(ssh-agent -s)
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo 'echo $SERVER_SSH_PASSPHRASE' > ~/.ssh/tmp && chmod 700 ~/.ssh/tmp
- echo "$SERVER_PRIVATE_KEY" | tr -d '\r' | DISPLAY=None SSH_ASKPASS=~/.ssh/tmp ssh-add

上面這串主要是在ssh-add 新增private key 之前,先將產生公私鑰過程中設定的passphrase 設定在~/.ssh/tmp 檔案裡,並刪除private key 的行尾回車字符(避免系統不同的問題,詳細可參考這篇),最後將passphrase 及private key 透過ssh-add 加入runner server

當我連進server、成功從origin/develop pull最新的版本,要用pm2 重啟服務時,卻又遇到一個問題:

pm2: command not found…

明明該server 一定有安裝過pm2,為了避免這錯誤我還手動連進該server,全域安裝一次。在跑一次CICD 依然噴錯...在我確認過其他server 的linux 環境後,終於發現問題所在:當初node 被安裝在/usr/local 路徑下,因此即使是全域安裝pm2 也是在local 下,只要設定個軟連接就可以了:ln -s /usr/local/node/lib/node_modules/pm2 /usr/lib

最後因為要部署的機器不只一台,也就是說除了環境變數外,重複的指令需要打不只一次,此時就可以應用類似物件導向的概念,先列出一個類別,在類別內輸入我們需要執行的指令,最後再用各環境要執行的job 去繼承類別,並輸入各變數就可以

.deploy_class:
stage: deploy
variables:
SERVER_PRIVATE_KEY: ""
SERVER_SSH_PASSPHRASE: ""
SERVER_USER: ""
SERVER_URL: ""
before_script:
...
script:
...
# job for dev
dev_deploy:
extends:
.deploy_class
variables:
SERVER_PRIVATE_KEY: ...
# job for sit
sit_deploy:
extends:
.deploy_class
variables:
SERVER_PRIVATE_KEY: ...

最後大推薦想更了解更多GitLab CICD 的人可參考高見龍大大的影片或文章

--

--

Martin

我們都要努力成為一個,當時間過去後,能夠感動自己的那個人