接下來即將進入的新公司與職位,會需要大量與GitLab CI接觸,為了做好準備,這幾天花了些時間自己跑了一遍CI流程。
GitLab CI Process
根據GitLab官方文件所述,所有CI/CD任務的執行都必須仰賴gitlab-ci.yml
這個文件:
To use GitLab CI/CD, you start with a .gitlab-ci.yml file at the root of your project. This file specifies the stages, jobs, and scripts to be executed during your CI/CD pipeline.
當定義好gitlab-ci.yml
後,整個GitLab執行CI/CD的流程就會變成:
- 當指定分支的程式碼更新(commit or push or pull request)
- GitLab根據gitlab-ci.yml的定義,同步平行地觸發job (編按:在沒有任何其他設定之下,job都是平行執行的,除非另外在yml設定stage或者needs參數)
- GitLab server會去檢查每個job所指名的Runner,並把job分配給適當的Runner執行
- Runner執行後的結果(CI Logs)會回傳到GitLab server,顯示於Pipeline上
在本篇文章中,會逐一介紹GitLab中Runner的種類,並且嘗試自定義Runner,並在上面運行單元測試。
GitLab Runner
當GitLab在運行gitlab-ci.yml
中定義的Jobs時,實際上是在GitLab Runner上運作的。在這裡,Runner又能根據作用範圍分作三種類型,分別是:
- Shared Runner: 在該GitLab server下的所有group或者project都能夠使用。當我們建立project時,系統會自動產生Shared Runner,可以用來處理無標籤(tag)的任務。
- Group Runner: 只有在該group下的所有project可以使用。
- Specific Runner: 只有在特定的project下可以被使用。本次會使用Specific Runner作為實驗範例。
Executor
這時,Runner會根據我們選擇的Executor決定要採用何種方式與工作環境來完成CI Job。換言之,一個Runner可以被允許擁有多個executor,讓一個Job能運行在多種環境之中。
Executor包含了我們所常用的Shell、Docker等環境,詳細內容可以參照這篇官方文件:Executors
使用AWS EC2 架設Specific Runner
到這邊才算是進入了本篇文章的正題。我會在同一台AWS EC2上架設兩個Runner,一個Runner負責跑單元測試、另一個Runner則負責build image以及push image到container registry上。為了在CI Pipeline中將專案打包成Docker Image,我也在EC2上另外安裝了Docker。
環境準備
- EC2 (t2.micro, 8G SSD, Ubuntu 24.04 x86)
- Docker version 27.1.1
Runner Installation & Information
Runner的創建與註冊可以參考官方文件:Tutorial: Create, register, and run your own project runner
Runner Name | Tag | Executor | Image |
---|---|---|---|
Runner for Testing | aws | Docker | alpine |
Runner for Image | build | Shell | Null |
Tag的用途是讓我們在撰寫yml檔時,可以根據tag來將job分配到合適的Runner。如果想要得到在這台機器所有Runner的資訊,預設可以到/etc/gitlab-runner/config.toml
進行查看。
為了讓每次的測試環境不受其他外部因素影響,針對跑測試的Runner我選用Docker Executor,而為了避免Docker in Docker(DinD)的複雜配置,在build image的Runner我則是使用shell executor,並直接在EC2上安裝Docker Engine使用。
.gitlab-ci.yml
此次的CI Process我切分為三個階段,分別是test(單元測試)、build(建構Docker Image)、publish(將Image推上GitLab Container Registry)
stages:
- test
- build
- publish
這次要拿來做CI的專案包含兩隻RESTful API,分別是會員的登入與登出功能,主要使用的技術如下:
- 後端:Python FastAPI
- 資料庫:mysql:8.0, redis:7.2.5
根據以上需求,我先將資料庫等服務的相關環境變數(當然,僅供測試使用,與正式環境的環境變數無關)建立成全域的variables
variables:
MYSQL_DATABASE: auth
MYSQL_USER: authuser
MYSQL_PASSWORD: mysqltest123
MYSQL_ROOT_PASSWORD: rootpassword
DATABASE_URL: mysql+mysqlconnector://authuser:mysqltest123@mysql:3306/auth
REDIS_URL: redis://redis:6379
為了能正確運行測試,我在script啟動之前,先將專案的環境、dependencies、MySQL指令先安裝完成,接著將test的結果匯出成artifacts(產物),這樣我們就能在GitLab server上下載每個pipeline對應的測試結果。
測試工作的yml檔設定如下:
tests:
stage: test
tags:
- aws
services:
- name: mysql:5.7
alias: mysql
image: python:3.12-alpine
before_script:
- apk add --no-cache gcc musl-dev mysql-client
- pip install --no-cache-dir -r requirements.txt
- pip install --no-cache-dir -r test-requirements.txt
- until nc -z -v -w30 mysql 3306; do echo "Waiting for database connection..."; sleep 1; done
- echo "MySQL started"
- mysql -h mysql -u root -prootpassword -e "CREATE DATABASE IF NOT EXISTS auth;"
- mysql -h mysql -u root -prootpassword auth < tests/init_db.sql
script:
- pytest > test-report.txt
artifacts:
paths:
- test-report.txt
expire_in: "30 days"
測試通過以後,會進入Image的建構與發佈流程,在這邊比較值得一提的是管理Docker Image名稱的方式。如果每次Image的版本做更動,我們就要到yml檔裡面去修改版號,這樣不僅麻煩,也很容易會有人為錯誤發生。所幸gitlab提供了許多已定義好的CI/CD變數,我們可以在yml檔直接使用這些變數來進行Image名稱的管理。在這裡,我使用CI_REGISTRY_IMAGE
作為Image名稱,CI_PIPELINE_IID
作為Image的版本號。
綜合以上,最終的yml檔會是這樣:
stages:
- test
- build
- publish
variables:
MYSQL_DATABASE: auth
MYSQL_USER: authuser
MYSQL_PASSWORD: mysqltest123
MYSQL_ROOT_PASSWORD: rootpassword
DATABASE_URL: mysql+mysqlconnector://authuser:mysqltest123@mysql:3306/auth
REDIS_URL: redis://redis:6379
IMAGE_VERSION: ${CI_PIPELINE_IID}
tests:
stage: test
tags:
- aws
services:
- name: mysql:5.7
alias: mysql
image: python:3.12-alpine
before_script:
- echo "DATABASE_URL is set to:$DATABASE_URL"
- apk add --no-cache gcc musl-dev mysql-client
- pip install --no-cache-dir -r requirements.txt
- pip install --no-cache-dir -r test-requirements.txt
- until nc -z -v -w30 mysql 3306; do echo "Waiting for database connection..."; sleep 1; done
- echo "MySQL started"
- mysql -h mysql -u root -prootpassword -e "CREATE DATABASE IF NOT EXISTS auth;"
- mysql -h mysql -u root -prootpassword auth < tests/init_db.sql
script:
- pytest > test-report.txt
artifacts:
paths:
- test-report.txt
expire_in: "30 days"
build:
stage: build
tags:
- build
before_script:
- echo "Building Docker image"
- docker info
script:
- docker build -t $CI_REGISTRY_IMAGE:$IMAGE_VERSION .
- docker tag $CI_REGISTRY_IMAGE:$IMAGE_VERSION $CI_REGISTRY_IMAGE:latest
- docker images
only:
- main
publish-to-registry:
stage: publish
tags:
- build
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker push $CI_REGISTRY_IMAGE:$IMAGE_VERSION
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
Trigger CI Pipeline
以目前的設置來說,只要在main
branch上進行程式碼的更動,三個stage會一起被觸發,否則,就只會觸發test
stage。為了執行簡單的測試,我直接在main
branch 進行commit,前往Build->Pipelines
就可以看到Pipeline被成功觸發了:
接著,前往Deploy->Container Registry
也能看到方才Pipeline產生的Image被成功上傳,可以看到最新Image的版本號與Pipeline的ID一致,且最新產生的Image有被加上latest標籤。到這邊,一個完整的CI流程就結束了。
坑:403 forbidden
註冊完gitlab runner並啟動後,console出現以下錯誤:
ERROR: Checking for jobs... forbidden runner=CsjMisCJ- status=POST https://gitlab.com/api/v4/jobs/request: 403 Forbidden
後來發現是因為我先前有註冊過標籤一模一樣的Runner,而這個Runner在Gitlab server中已經被刪除了,我卻忘記在EC2上把該Runner給取消註冊,又由於這兩個Runner的標籤相同,導致Gitlab在分配Job時出現錯誤。
解決方法當然就是每次要記得把不需要的Runner給unregistered:
# 取得當前機器上的Runner以及對應的executor、token
sudo gitlab-runner list
# 根據以上指令獲得的Runner, token, URL來做刪除
sudo gitlab-runner verify --delete -t [token] -u [URL]
Reference
GitLab Docs https://docs.gitlab.com/
gitlab-runner run-single always fails with ‘forbidden’ with docker executor https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4919
How do I delete/unregister a GitLab runner https://stackoverflow.com/questions/66616014/how-do-i-delete-unregister-a-gitlab-runner