接下來即將進入的新公司與職位,會需要大量與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的流程就會變成:

  1. 當指定分支的程式碼更新(commit or push or pull request)
  2. GitLab根據gitlab-ci.yml的定義,同步平行地觸發job (編按:在沒有任何其他設定之下,job都是平行執行的,除非另外在yml設定stage或者needs參數)
  3. GitLab server會去檢查每個job所指名的Runner,並把job分配給適當的Runner執行
  4. Runner執行後的結果(CI Logs)會回傳到GitLab server,顯示於Pipeline上

在本篇文章中,會逐一介紹GitLab中Runner的種類,並且嘗試自定義Runner,並在上面運行單元測試。

GitLab Runner

當GitLab在運行gitlab-ci.yml中定義的Jobs時,實際上是在GitLab Runner上運作的。在這裡,Runner又能根據作用範圍分作三種類型,分別是:

  1. Shared Runner: 在該GitLab server下的所有group或者project都能夠使用。當我們建立project時,系統會自動產生Shared Runner,可以用來處理無標籤(tag)的任務。
  2. Group Runner: 只有在該group下的所有project可以使用。
  3. 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

以目前的設置來說,只要在mainbranch上進行程式碼的更動,三個stage會一起被觸發,否則,就只會觸發test stage。為了執行簡單的測試,我直接在main branch 進行commit,前往Build->Pipelines就可以看到Pipeline被成功觸發了: pipeline

接著,前往Deploy->Container Registry也能看到方才Pipeline產生的Image被成功上傳,可以看到最新Image的版本號與Pipeline的ID一致,且最新產生的Image有被加上latest標籤。到這邊,一個完整的CI流程就結束了。

registry

坑: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