在當代軟體開發流程中,持續整合與持續部署系統的執行效能直接影響開發團隊的生產力。隨著專案規模擴大與複雜度提升,CI/CD Pipeline 往往面臨執行時間過長、設定檔維護困難等挑戰。本文將深入探討如何運用 GitLab CI/CD 的核心功能來系統性地解決這些問題。我們將從快取與製品的本質差異出發,探討如何設計高效的快取策略、建立合理的製品傳遞機制,進而透過 YAML 範本模式減少重複設定,最終達成大型專案的模組化管線架構。透過實務案例與技術細節的深入分析,本文將協助讀者建構既快速又易於維護的企業級 CI/CD 解決方案。

快取與製品的本質差異與應用策略

在 GitLab CI/CD 的架構中,快取與製品是兩個經常被混淆卻有著本質差異的機制。理解這兩者的特性與適用場景,是建構高效 Pipeline 的首要關鍵。快取的設計初衷在於加速重複性的依賴安裝與建置過程,透過保存執行過程中產生的中間檔案,避免後續作業重複執行相同的準備工作。相對地,製品則著重於作業成果的傳遞與保存,確保建置產出能夠在不同階段間正確流轉,並提供追溯與下載的能力。

深入探討快取機制的特性,我們可以發現 GitLab 採用了靈活的作業層級快取設計。每個作業都能獨立定義需要快取的路徑,這些快取資料能夠在同一管線的不同階段間共享使用。然而,快取的範圍嚴格限制在單一專案內部,不同專案的管線無法共用快取資料,這是基於安全性與隔離性的考量。在時效性方面,GitLab 預設會將閒置超過三十天的快取自動清除,但透過啟用最新快取保留機制,可以確保最近使用的快取不會因為時間因素而被移除。此外,快取的取得過程受到作業依賴關係的控制,開發者能夠精確指定哪些作業應該使用特定的快取資料。

製品機制則呈現出更為豐富的功能集合。製品不僅能在作業間傳遞資料,更支援完整的版本追蹤與歷史記錄查詢。當作業執行完成後,產生的製品會被永久保存在 GitLab 伺服器上,直到專案管理員手動刪除或達到設定的保留期限。這種持久化的特性使得製品特別適合儲存建置成果、測試報告、部署套件等需要長期保存的檔案。製品還提供了精細的存取控制機制,能夠設定哪些作業可以下載特定的製品,確保資料流向的可控性。更重要的是,製品能夠透過 GitLab 的使用者介面直接下載,方便開發團隊在需要時快速取得歷史版本的建置產出。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi 300
skinparam shadowing false
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam roundcorner 8
skinparam minClassWidth 140

package "快取機制特性" {
  rectangle "作業層級定義" as Cache1
  rectangle "跨階段共享" as Cache2
  rectangle "專案內隔離" as Cache3
  rectangle "自動過期機制" as Cache4
  rectangle "依賴關係控制" as Cache5
}

package "製品機制特性" {
  rectangle "永久性儲存" as Artifact1
  rectangle "版本追蹤" as Artifact2
  rectangle "精細存取控制" as Artifact3
  rectangle "使用者介面下載" as Artifact4
  rectangle "跨專案參照" as Artifact5
}

package "應用場景比較" {
  usecase "依賴套件快取" as UC1
  usecase "編譯中間產物" as UC2
  usecase "建置成果傳遞" as UC3
  usecase "測試報告保存" as UC4
  usecase "部署套件產出" as UC5
}

Cache1 --> UC1
Cache2 --> UC2
Cache3 --> UC2
Artifact1 --> UC3
Artifact2 --> UC4
Artifact3 --> UC5

@enduml

此架構圖清楚呈現了快取與製品在特性層面的本質差異。快取機制強調的是執行效能的最佳化,透過保存可重複利用的中間檔案來縮短管線執行時間。製品機制則聚焦在成果管理與可追溯性,確保每次建置的產出都能被妥善保存並在需要時被正確取得。在實際應用中,這兩種機制往往需要搭配使用,快取負責加速建置過程,製品負責傳遞建置成果,形成完整的 CI/CD 資料流轉體系。

快取策略的深度設計與實作技巧

建立高效的快取策略需要從基礎設定開始,逐步深入到進階的快取管理技巧。在最基本的層面,快取的設定需要明確指定哪些目錄或檔案應該被保存下來供後續使用。這個過程看似簡單,實則需要仔細思考專案的依賴結構與建置流程。

# 基礎快取設定範例
# 此作業展示了最簡單的快取使用方式
build_application:
  stage: build
  # 定義快取設定區塊
  cache:
    # 指定需要快取的路徑清單
    # 支援萬用字元來批次選取檔案
    paths:
      # 快取整個 node_modules 目錄
      # 此目錄通常包含專案的所有依賴套件
      - node_modules/
      # 快取建置過程產生的暫存目錄
      # 許多建置工具會使用 .cache 來儲存中間檔案
      - .cache/
      # 快取特定的設定檔
      # 某些工具會在執行時產生設定快取
      - package-lock.json
  # 實際執行的建置指令
  script:
    # 安裝專案依賴
    # npm ci 相較於 npm install 更適合 CI 環境
    # 它會嚴格依照 package-lock.json 安裝,確保版本一致性
    - npm ci
    # 執行建置流程
    # 此指令會將原始碼編譯為可執行的應用程式
    - npm run build

這個基礎範例展示了快取的核心概念,但在實際應用中,我們往往需要更精細的控制。多個作業可能需要共享同一份快取資料,或是某些作業需要使用不同版本的快取。這時候快取鍵的機制就顯得格外重要。

# 進階快取鍵設定範例
# 展示如何使用快取鍵來管理多個快取版本

# 定義全域的快取設定
# 這些設定會作為所有作業的預設值
default:
  cache:
    # 使用 CI_COMMIT_REF_SLUG 變數作為快取鍵
    # 這個變數代表當前分支名稱的安全版本
    # 不同分支會擁有獨立的快取空間
    key: ${CI_COMMIT_REF_SLUG}
    # 定義快取路徑
    paths:
      - node_modules/
      - .npm/

# 安裝依賴的作業
install_dependencies:
  stage: prepare
  # 使用特定的快取鍵組合
  cache:
    # 組合多個變數來產生唯一的快取鍵
    # CI_COMMIT_REF_SLUG: 分支名稱
    # package-lock.json 的檢查碼: 依賴版本識別
    key:
      # files 關鍵字會計算指定檔案的檢查碼
      # 當 package-lock.json 變更時,快取鍵也會改變
      files:
        - package-lock.json
      # prefix 為快取鍵加上前綴,便於識別
      prefix: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
      - .npm/
  script:
    # 安裝依賴套件
    - npm ci
    # 顯示快取資訊供除錯使用
    - echo "Dependencies cached for ${CI_COMMIT_REF_SLUG}"

# 建置應用程式的作業
build_application:
  stage: build
  # 此作業的快取設定
  cache:
    # 使用相同的快取鍵確保能取得前一個作業的快取
    key:
      files:
        - package-lock.json
      prefix: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
      - .npm/
    # 設定快取策略為僅下載
    # 此作業不需要更新快取,只需要讀取
    policy: pull
  script:
    # 執行建置指令
    # 由於依賴已經從快取中載入,此步驟會更快完成
    - npm run build
  # 將建置產出儲存為製品
  artifacts:
    paths:
      - dist/

快取鍵的設計是快取策略中最精妙的部分。透過結合不同的 GitLab 預定義變數與檔案檢查碼,我們能夠建立出既精確又彈性的快取管理機制。使用分支名稱作為快取鍵的一部分,確保了不同開發分支擁有獨立的快取空間,避免相互干擾。而引入檔案檢查碼的機制,則讓快取能夠在依賴版本變更時自動失效,確保永遠使用正確版本的依賴套件。

快取策略的另一個關鍵面向是下載與上傳策略的控制。在預設情況下,GitLab 會在作業開始時下載快取,結束時上傳更新後的快取。然而,並非所有作業都需要這種雙向的快取操作。某些作業只需要讀取快取而不需要更新,某些則專門負責建立快取供其他作業使用。透過精確設定快取策略,我們能夠減少不必要的網路傳輸,進一步提升管線執行效率。

# 快取策略的精細控制範例
# 展示不同作業如何使用不同的快取策略

# 第一階段:建立快取
create_cache:
  stage: prepare
  cache:
    key: project-dependencies
    paths:
      - vendor/
      - .composer/
    # 設定為僅推送模式
    # 此作業負責建立快取,不需要下載既有快取
    policy: push
  script:
    # 使用 Composer 安裝 PHP 依賴
    # --prefer-dist 參數會優先使用已編譯的套件
    # --no-interaction 參數避免互動式提示
    - composer install --prefer-dist --no-interaction --optimize-autoloader
    # 顯示安裝的套件資訊
    - composer show

# 第二階段:使用快取執行測試
run_tests:
  stage: test
  cache:
    key: project-dependencies
    paths:
      - vendor/
      - .composer/
    # 設定為僅拉取模式
    # 此作業只需要使用快取,不需要更新
    policy: pull
  script:
    # 執行單元測試
    # 由於依賴已從快取載入,可以直接執行測試
    - ./vendor/bin/phpunit --coverage-text

# 第三階段:使用快取執行程式碼檢查
code_quality:
  stage: test
  cache:
    key: project-dependencies
    paths:
      - vendor/
      - .composer/
    # 同樣設定為僅拉取模式
    policy: pull
  script:
    # 執行程式碼風格檢查
    - ./vendor/bin/phpcs --standard=PSR12 src/
    # 執行靜態分析
    - ./vendor/bin/phpstan analyse src/ --level=max

這個範例展示了快取策略的分工協作模式。第一個作業負責建立完整的快取,設定為僅推送模式以避免不必要的下載操作。後續的測試與檢查作業則都設定為僅拉取模式,它們只需要使用既有的快取資料,不需要進行更新。這種模式不僅提升了執行效率,也確保了所有並行執行的測試作業都使用完全相同版本的依賴套件,避免了因快取更新時機差異而產生的不一致問題。

製品管理的完整實作與依賴關係設計

製品機制在 GitLab CI/CD 中扮演著建置成果傳遞的核心角色。相較於快取的暫時性與最佳化導向,製品提供了完整的檔案生命週期管理能力。從產生、儲存、版本控制到最終的下載使用,每個環節都經過精心設計以確保建置產出的完整性與可追溯性。

製品的基礎設定雖然看似簡單,但背後蘊含的是對建置流程的深刻理解。我們需要明確知道哪些檔案是建置的最終產出,哪些是中間過程的副產物。前者應該被保存為製品供後續階段使用,後者則可以透過快取機制來加速建置過程。這種清晰的界定是建構可靠 CI/CD 流程的基礎。

# 完整的製品生命週期管理範例
# 展示從建置到部署的製品流轉過程

# 編譯前端應用程式
build_frontend:
  stage: build
  image: node:18-alpine
  # 使用快取加速依賴安裝
  cache:
    key: ${CI_COMMIT_REF_SLUG}-node
    paths:
      - node_modules/
      - .npm/
  script:
    # 安裝依賴套件
    # --cache 參數指定快取目錄位置
    - npm ci --cache .npm --prefer-offline
    # 執行建置流程
    # 產生最佳化的生產環境程式碼
    - npm run build
    # 產生建置資訊檔案
    # 記錄建置時間、提交版本等資訊供追蹤使用
    - echo "Build Time:$(date)" > dist/build-info.txt
    - echo "Commit SHA:${CI_COMMIT_SHA}" >> dist/build-info.txt
  # 製品設定區塊
  artifacts:
    # 指定製品名稱
    # 使用變數組合讓每個建置都有獨特的識別
    name: "frontend-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}"
    # 設定製品的儲存條件
    # on_success: 僅在作業成功時儲存
    # on_failure: 僅在作業失敗時儲存(用於保存錯誤日誌)
    # always: 無論成功失敗都儲存
    when: on_success
    # 設定製品的保存期限
    # 使用期限設定可以避免儲存空間無限制增長
    # 格式支援: 1 hour, 1 day, 1 week, 1 month
    expire_in: 1 week
    # 指定要保存的路徑
    paths:
      # 儲存完整的建置產出目錄
      - dist/
    # 設定製品報告
    # 某些類型的檔案可以被 GitLab 特別處理
    reports:
      # 如果有程式碼品質報告,可以這樣設定
      # codequality: gl-code-quality-report.json
      # 如果有測試覆蓋率報告
      # coverage_report:
      #   coverage_format: cobertura
      #   path: coverage/cobertura-coverage.xml

# 執行端到端測試
e2e_testing:
  stage: test
  image: cypress/base:18
  # 依賴前端建置作業
  # 這會確保測試使用的是最新的建置產出
  dependencies:
    - build_frontend
  script:
    # 啟動測試伺服器
    # 使用建置產出的靜態檔案
    - npx serve dist -p 3000 &
    # 等待伺服器啟動
    - sleep 5
    # 執行 Cypress 測試
    - npx cypress run
  # 測試截圖與影片也儲存為製品
  artifacts:
    when: always
    expire_in: 3 days
    paths:
      - cypress/screenshots/
      - cypress/videos/

# 建置 Docker 映像檔
build_docker_image:
  stage: package
  image: docker:24-dind
  services:
    - docker:24-dind
  # 依賴前端建置作業
  dependencies:
    - build_frontend
  # 設定 Docker in Docker 所需的變數
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_VERIFY: 1
    DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
  script:
    # 驗證建置產出已正確載入
    - ls -la dist/
    # 登入容器映像倉庫
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
    # 建置 Docker 映像檔
    # 標記為最新提交的 SHA 值
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    # 同時標記為 latest
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
    # 推送到映像倉庫
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
    # 產生映像檔資訊
    - echo "Image:$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" > image-info.txt
  # 儲存映像檔資訊供部署使用
  artifacts:
    paths:
      - image-info.txt

# 部署到測試環境
deploy_staging:
  stage: deploy
  image: alpine:latest
  # 依賴 Docker 建置作業
  dependencies:
    - build_docker_image
  # 僅在主要分支執行部署
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  # 定義環境資訊
  environment:
    name: staging
    url: https://staging.example.com
  before_script:
    # 安裝部署所需的工具
    - apk add --no-cache curl
  script:
    # 讀取映像檔資訊
    - cat image-info.txt
    # 觸發部署流程
    # 實際場景可能是呼叫 Kubernetes API 或其他部署工具
    - echo "Deploying to staging environment"
    # 執行健康檢查
    - curl --fail https://staging.example.com/health || exit 1

這個完整的範例展示了製品在整個 CI/CD 流程中的流轉過程。前端建置作業產生編譯後的靜態檔案,這些檔案被儲存為製品並設定了一週的保存期限。測試作業透過依賴宣告取得這些製品,在隔離的環境中執行端到端測試。Docker 建置作業同樣使用這些製品來產生容器映像檔,並將映像檔的參考資訊儲存為新的製品。最終,部署作業使用這個映像檔資訊來執行實際的部署操作。

製品的依賴關係設計需要特別注意作業執行的順序與資料流向。GitLab 會根據依賴宣告自動建立作業間的執行順序約束,確保依賴的作業先執行完成,產生的製品才會被後續作業使用。這種機制讓我們能夠建構出複雜的建置與部署流程,而不需要擔心時序問題。

# 複雜依賴關係的管理範例
# 展示多個作業如何協同工作

# 編譯後端 API
build_backend:
  stage: build
  image: maven:3.9-eclipse-temurin-17
  cache:
    key: ${CI_COMMIT_REF_SLUG}-maven
    paths:
      - .m2/repository/
  script:
    # 設定 Maven 使用本地快取目錄
    - mvn -Dmaven.repo.local=.m2/repository clean package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 week

# 編譯前端應用
build_frontend:
  stage: build
  image: node:18-alpine
  cache:
    key: ${CI_COMMIT_REF_SLUG}-node
    paths:
      - node_modules/
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

# 執行後端測試
test_backend:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  # 依賴後端建置
  dependencies:
    - build_backend
  cache:
    key: ${CI_COMMIT_REF_SLUG}-maven
    paths:
      - .m2/repository/
    policy: pull
  script:
    # 執行整合測試
    - mvn -Dmaven.repo.local=.m2/repository verify
  artifacts:
    when: always
    reports:
      junit:
        - target/surefire-reports/TEST-*.xml

# 執行前端測試
test_frontend:
  stage: test
  image: node:18-alpine
  # 依賴前端建置
  dependencies:
    - build_frontend
  cache:
    key: ${CI_COMMIT_REF_SLUG}-node
    paths:
      - node_modules/
    policy: pull
  script:
    - npm run test:unit
  artifacts:
    when: always
    reports:
      junit:
        - junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

# 整合建置產出
package_application:
  stage: package
  image: alpine:latest
  # 同時依賴前端與後端的建置產出
  dependencies:
    - build_backend
    - build_frontend
  script:
    # 建立發布目錄結構
    - mkdir -p release/backend release/frontend
    # 複製後端 JAR 檔案
    - cp target/*.jar release/backend/
    # 複製前端靜態檔案
    - cp -r dist/* release/frontend/
    # 產生版本資訊檔案
    - echo "Version:${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}" > release/version.txt
    - echo "Build Date:$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> release/version.txt
    # 建立壓縮檔
    - tar -czf application-${CI_COMMIT_SHORT_SHA}.tar.gz release/
  artifacts:
    name: "full-application-${CI_COMMIT_SHORT_SHA}"
    paths:
      - application-*.tar.gz
      - release/version.txt
    expire_in: 1 month

這個範例展示了如何管理多個建置產出的整合流程。後端與前端分別獨立建置,各自產生製品。測試作業分別依賴對應的建置產出,確保測試使用的是正確的程式碼版本。最終的打包作業則同時依賴前端與後端的製品,將它們整合成完整的發布套件。這種模組化的設計不僅提升了建置的平行度,也讓整個流程更容易理解與維護。

YAML 範本模式的實務應用技巧

隨著 CI/CD 設定的複雜度增加,YAML 設定檔往往會累積大量重複的程式碼區塊。相同的映像檔設定、類似的指令碼片段、重複的規則條件,這些冗餘不僅增加了維護成本,也提高了設定錯誤的風險。GitLab CI/CD 支援多種 YAML 範本機制,讓我們能夠有效地組織與重用設定內容。

YAML Anchors 是最基本的範本機制,它允許我們定義一個可重用的設定區塊,然後在多個地方引用。這種機制特別適合處理結構相同但內容略有差異的設定。Anchors 的核心概念是定義與參照,透過 & 符號定義一個命名的區塊,再使用 * 符號在其他地方引用這個區塊。

# YAML Anchors 的基礎應用
# 展示如何定義與使用可重用的設定區塊

# 定義一個隱藏的作業範本
# 以點號開頭的作業不會被 GitLab 執行
.node_job_template: &node_job_definition
  # 定義統一的執行環境
  image: node:18-alpine
  # 定義統一的前置指令
  before_script:
    # 顯示 Node.js 與 npm 版本資訊
    - node --version
    - npm --version
    # 設定 npm 使用快取目錄
    - npm config set cache .npm --global
  # 定義統一的快取設定
  cache:
    key: ${CI_COMMIT_REF_SLUG}-node
    paths:
      - node_modules/
      - .npm/

# 使用範本的作業 - 安裝依賴
install_dependencies:
  # 繼承範本定義的所有設定
  <<: *node_job_definition
  stage: prepare
  # 僅定義此作業特有的指令
  script:
    - npm ci
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

# 使用範本的作業 - 執行測試
run_unit_tests:
  # 繼承相同的範本
  <<: *node_job_definition
  stage: test
  dependencies:
    - install_dependencies
  script:
    # 執行單元測試並產生覆蓋率報告
    - npm run test:unit -- --coverage
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

# 使用範本的作業 - 程式碼檢查
code_linting:
  # 繼承相同的範本
  <<: *node_job_definition
  stage: test
  dependencies:
    - install_dependencies
  script:
    # 執行 ESLint 檢查
    - npm run lint
    # 執行 TypeScript 型別檢查
    - npm run type-check

Anchors 機制雖然簡單有效,但在處理更複雜的繼承關係時會顯得力不從心。這時候 GitLab 提供的 extends 關鍵字就成為更好的選擇。extends 不僅支援單一繼承,還能進行多層級的繼承鏈,讓設定的組織更加靈活。

# extends 關鍵字的進階應用
# 展示多層級繼承與設定覆寫

# 第一層:定義基礎範本
.base_job:
  # 定義所有作業共用的基礎設定
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure
  # 定義標籤以指定執行的 Runner
  tags:
    - docker
  # 定義逾時設定
  timeout: 1h

# 第二層:定義語言特定範本
.node_job:
  # 繼承基礎範本
  extends: .base_job
  # 新增 Node.js 特定設定
  image: node:18-alpine
  before_script:
    - npm ci --prefer-offline --no-audit
  cache:
    key:
      files:
        - package-lock.json
      prefix: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
      - .npm/

# 第二層:定義 Docker 建置範本
.docker_job:
  # 繼承基礎範本
  extends: .base_job
  # 新增 Docker 特定設定
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker info
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY

# 第三層:定義具體的測試作業
unit_tests:
  # 繼承 Node.js 範本
  extends: .node_job
  stage: test
  # 可以覆寫繼承的設定
  timeout: 30m
  script:
    - npm run test:unit
  coverage: '/Statements\s+:\s+([0-9.]+)%/'

# 第三層:定義具體的建置作業
build_application:
  # 繼承 Node.js 範本
  extends: .node_job
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

# 第三層:定義具體的 Docker 建置作業
build_container:
  # 繼承 Docker 範本
  extends: .docker_job
  stage: package
  dependencies:
    - build_application
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  # 只在主要分支執行
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

extends 機制的強大之處在於它支援設定的覆寫與合併。當子作業定義了與父範本相同的鍵值時,子作業的設定會覆寫父範本的設定。這種特性讓我們能夠在保持通用設定的同時,靈活地調整個別作業的行為。在上述範例中,unit_tests 作業覆寫了從 .base_job 繼承的逾時設定,將其從一小時調整為三十分鐘,以符合單元測試執行時間較短的特性。

對於更精細的設定重用需求,GitLab 提供了參照標籤機制。這個機制允許我們從任何作業中選取特定的設定區塊進行重用,而不需要繼承整個作業的所有設定。這種精準的重用能力特別適合處理複雜的多專案或多環境設定。

# 參照標籤的進階應用
# 展示跨檔案的精確設定重用

# 定義可重用的指令碼片段
.scripts:
  # 定義部署前的檢查指令
  pre_deploy: &pre_deploy_script
    - echo "Checking deployment prerequisites..."
    - test -f deployment-config.yml || exit 1
    - test -n "$DEPLOY_TOKEN" || exit 1
  # 定義健康檢查指令
  health_check: &health_check_script
    - echo "Performing health check..."
    - curl --fail --retry 3 --retry-delay 5 $HEALTH_CHECK_URL
  # 定義通知指令
  notification: &notification_script
    - echo "Sending deployment notification..."
    - 'curl -X POST $SLACK_WEBHOOK -d "payload={\"text\": \"Deployment completed: $CI_COMMIT_SHORT_SHA\"}"'

# 部署到測試環境
deploy_staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
  variables:
    HEALTH_CHECK_URL: https://staging.example.com/health
  before_script:
    # 參照預定義的部署檢查指令
    - !reference [.scripts, pre_deploy]
  script:
    - kubectl apply -f k8s/staging/
    - kubectl rollout status deployment/app -n staging
  after_script:
    # 參照預定義的健康檢查指令
    - !reference [.scripts, health_check]
    # 參照預定義的通知指令
    - !reference [.scripts, notification]

# 部署到生產環境
deploy_production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  variables:
    HEALTH_CHECK_URL: https://example.com/health
  # 需要手動觸發
  when: manual
  before_script:
    # 重用相同的部署檢查指令
    - !reference [.scripts, pre_deploy]
    # 加入額外的生產環境檢查
    - echo "Additional production checks..."
    - test -n "$PRODUCTION_APPROVAL" || exit 1
  script:
    - kubectl apply -f k8s/production/
    - kubectl rollout status deployment/app -n production
  after_script:
    # 重用健康檢查指令
    - !reference [.scripts, health_check]
    # 重用通知指令
    - !reference [.scripts, notification]

參照標籤的使用讓設定的重用達到了最細粒度的層級。我們不再需要為了重用某個指令碼片段而繼承整個作業的設定,只需要精確地參照需要的部分即可。這種機制特別適合管理部署流程,因為不同環境的部署往往共享大部分的操作步驟,只在少數細節上有所差異。

大型專案的模組化管線架構設計

當 CI/CD 設定的規模持續擴大,單一設定檔的維護難度會急遽上升。數百行甚至上千行的 YAML 設定檔不僅難以閱讀,更容易在修改時引入錯誤。這時候,將設定檔拆分成多個模組化的子檔案成為必然的選擇。GitLab 的 include 機制提供了完整的多檔案組織能力,讓我們能夠建構出清晰且易於維護的管線架構。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi 300
skinparam shadowing false
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam roundcorner 8
skinparam packageStyle rectangle

package "主要設定檔" {
  [.gitlab-ci.yml] as MainConfig
}

package "建置模組" {
  [build-backend.yml] as BuildBackend
  [build-frontend.yml] as BuildFrontend
  [build-docker.yml] as BuildDocker
}

package "測試模組" {
  [test-unit.yml] as TestUnit
  [test-integration.yml] as TestIntegration
  [test-e2e.yml] as TestE2E
}

package "部署模組" {
  [deploy-staging.yml] as DeployStaging
  [deploy-production.yml] as DeployProduction
}

package "共用範本" {
  [templates.yml] as Templates
}

MainConfig --> BuildBackend
MainConfig --> BuildFrontend
MainConfig --> BuildDocker
MainConfig --> TestUnit
MainConfig --> TestIntegration
MainConfig --> TestE2E
MainConfig --> DeployStaging
MainConfig --> DeployProduction
MainConfig --> Templates

BuildBackend ..> Templates : 使用
BuildFrontend ..> Templates : 使用
TestUnit ..> Templates : 使用
TestIntegration ..> Templates : 使用
DeployStaging ..> Templates : 使用
DeployProduction ..> Templates : 使用

@enduml

這個架構圖展示了大型專案的管線組織方式。主要設定檔作為入口點,引入各個功能模組的子設定檔。建置、測試、部署各自形成獨立的模組,每個模組內部還可以進一步細分。共用範本檔案被其他模組廣泛引用,提供統一的基礎設定。這種模組化的架構不僅提升了可維護性,也讓團隊成員能夠專注於特定模組的開發,減少相互干擾的機會。

主要設定檔的職責是定義管線的整體結構與引入所需的模組。它應該保持簡潔,避免包含過多的具體實作細節。

# 主要設定檔 .gitlab-ci.yml
# 定義管線架構並引入各個功能模組

# 定義管線的執行階段
# 階段定義了作業的執行順序
stages:
  - prepare      # 準備階段:依賴安裝、環境設定
  - build        # 建置階段:編譯原始碼
  - test         # 測試階段:執行各類測試
  - package      # 打包階段:建立部署套件
  - deploy       # 部署階段:發布到各環境

# 定義全域變數
# 這些變數在所有作業中都可以使用
variables:
  # 專案識別資訊
  PROJECT_NAME: "example-application"
  # 部署相關變數
  STAGING_URL: "https://staging.example.com"
  PRODUCTION_URL: "https://example.com"

# 引入共用範本
# 這個檔案包含了所有可重用的作業範本
include:
  # 本地檔案引入
  - local: '.gitlab/ci/templates.yml'

# 引入建置模組
include:
  # 後端建置設定
  - local: '.gitlab/ci/build-backend.yml'
  # 前端建置設定
  - local: '.gitlab/ci/build-frontend.yml'
  # Docker 映像檔建置設定
  - local: '.gitlab/ci/build-docker.yml'

# 引入測試模組
include:
  # 單元測試設定
  - local: '.gitlab/ci/test-unit.yml'
  # 整合測試設定
  - local: '.gitlab/ci/test-integration.yml'
  # 端到端測試設定
  - local: '.gitlab/ci/test-e2e.yml'

# 引入部署模組
include:
  # 測試環境部署設定
  - local: '.gitlab/ci/deploy-staging.yml'
  # 生產環境部署設定
  - local: '.gitlab/ci/deploy-production.yml'

# 定義全域的預設設定
default:
  # 所有作業的預設重試策略
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure
  # 預設的逾時時間
  timeout: 1h

共用範本檔案集中定義所有可重用的作業範本,確保設定的一致性。

# 共用範本檔案 .gitlab/ci/templates.yml
# 定義所有可重用的作業範本

# 基礎作業範本
.base_job:
  # 指定執行的 Runner 標籤
  tags:
    - docker
  # 定義失敗時的重試策略
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

# Node.js 作業範本
.node_job:
  extends: .base_job
  image: node:18-alpine
  before_script:
    # 顯示版本資訊
    - node --version
    - npm --version
    # 設定 npm 快取
    - npm config set cache .npm --global
  cache:
    key:
      files:
        - package-lock.json
      prefix: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
      - .npm/

# Java 作業範本
.java_job:
  extends: .base_job
  image: maven:3.9-eclipse-temurin-17
  before_script:
    # 顯示版本資訊
    - mvn --version
    - java --version
  cache:
    key: ${CI_COMMIT_REF_SLUG}-maven
    paths:
      - .m2/repository/

# Docker 建置範本
.docker_job:
  extends: .base_job
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_VERIFY: 1
  before_script:
    - docker info
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY

# 部署作業範本
.deploy_job:
  extends: .base_job
  image: bitnami/kubectl:latest
  before_script:
    # 設定 kubectl 認證
    - kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
    - kubectl config set-credentials admin --token="$KUBE_TOKEN"
    - kubectl config set-context default --cluster=k8s --user=admin
    - kubectl config use-context default
  # 部署作業只在受保護分支執行
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

各個功能模組的子設定檔則專注於特定領域的實作細節。

# 後端建置模組 .gitlab/ci/build-backend.yml
# 負責 Java 後端應用程式的建置

# 編譯後端應用程式
build_backend_app:
  extends: .java_job
  stage: build
  script:
    # 執行 Maven 建置
    # -Dmaven.repo.local 指定本地倉庫位置以使用快取
    # -DskipTests 跳過測試,測試在獨立階段執行
    - mvn -Dmaven.repo.local=.m2/repository clean package -DskipTests
    # 顯示建置產出資訊
    - ls -lh target/*.jar
  artifacts:
    name: "backend-${CI_COMMIT_SHORT_SHA}"
    paths:
      - target/*.jar
      - target/classes/
    expire_in: 1 week
    reports:
      # 產生依賴掃描報告
      dependency_scanning: target/dependency-check-report.json

# 建置後端 Docker 映像檔
build_backend_docker:
  extends: .docker_job
  stage: package
  dependencies:
    - build_backend_app
  script:
    # 建置映像檔
    - docker build -f Dockerfile.backend -t $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA .
    # 標記為最新版本
    - docker tag $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE/backend:latest
    # 推送到映像倉庫
    - docker push $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE/backend:latest
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# 前端建置模組 .gitlab/ci/build-frontend.yml
# 負責 React 前端應用程式的建置

# 編譯前端應用程式
build_frontend_app:
  extends: .node_job
  stage: build
  script:
    # 安裝依賴
    - npm ci --prefer-offline
    # 執行建置
    - npm run build
    # 產生建置報告
    - npm run build:report
  artifacts:
    name: "frontend-${CI_COMMIT_SHORT_SHA}"
    paths:
      - dist/
      - build-report.html
    expire_in: 1 week

# 建置前端 Docker 映像檔
build_frontend_docker:
  extends: .docker_job
  stage: package
  dependencies:
    - build_frontend_app
  script:
    # 建置 Nginx 映像檔包含前端靜態檔案
    - docker build -f Dockerfile.frontend -t $CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA .
    - docker tag $CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE/frontend:latest
    - docker push $CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE/frontend:latest
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# 部署模組 .gitlab/ci/deploy-staging.yml
# 負責測試環境的部署作業

# 部署後端到測試環境
deploy_backend_staging:
  extends: .deploy_job
  stage: deploy
  environment:
    name: staging/backend
    url: $STAGING_URL/api
  variables:
    KUBE_NAMESPACE: staging
  script:
    # 更新 Kubernetes 部署的映像檔
    - kubectl set image deployment/backend backend=$CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA -n $KUBE_NAMESPACE
    # 等待部署完成
    - kubectl rollout status deployment/backend -n $KUBE_NAMESPACE --timeout=5m
    # 執行健康檢查
    - kubectl exec -n $KUBE_NAMESPACE deployment/backend -- curl -f http://localhost:8080/health

# 部署前端到測試環境
deploy_frontend_staging:
  extends: .deploy_job
  stage: deploy
  environment:
    name: staging/frontend
    url: $STAGING_URL
  variables:
    KUBE_NAMESPACE: staging
  script:
    - kubectl set image deployment/frontend frontend=$CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA -n $KUBE_NAMESPACE
    - kubectl rollout status deployment/frontend -n $KUBE_NAMESPACE --timeout=5m

這種模組化的架構帶來多重優勢。首先,每個子設定檔都有明確的職責範圍,團隊成員能夠快速定位需要修改的檔案。其次,模組化設計使得設定的重用更加容易,同樣的建置或測試邏輯可以在不同專案間共享。再者,版本控制的粒度更加精細,當某個模組發生變更時,Git 歷史記錄能夠清楚顯示影響範圍。最後,模組化架構為管線的擴展預留了空間,新增功能時只需要建立新的模組檔案並在主設定檔中引入即可。

在大型企業環境中,多專案間的設定共享是常見需求。GitLab 支援從其他專案引入設定檔,這讓我們能夠建立中央化的 CI/CD 範本倉庫。

# 引入遠端專案的設定檔
include:
  # 從專案倉庫引入
  - project: 'devops/ci-templates'
    ref: main
    file:
      - '/templates/base.yml'
      - '/templates/security-scan.yml'
      - '/templates/deploy-k8s.yml'

  # 從公開的 URL 引入
  - remote: 'https://gitlab.com/example/ci-templates/-/raw/main/docker-build.yml'

  # 組合本地與遠端設定
  - local: '.gitlab/ci/project-specific.yml'

這種集中化的範本管理機制讓組織能夠建立統一的 CI/CD 標準,確保所有專案都遵循相同的最佳實務。當需要更新安全掃描流程或部署策略時,只需要修改中央範本倉庫,所有引用的專案都能自動受益於這些改進。

透過本文深入探討的快取與製品機制、YAML 範本模式、模組化架構設計,我們建立了一套完整的 GitLab CI/CD 效能最佳化方法論。這些技術的綜合運用能夠顯著縮短管線執行時間,同時大幅提升設定檔的可維護性。在實務應用中,建議從小規模的最佳化開始,逐步導入更複雜的架構模式,確保團隊能夠充分理解並掌握每個技術的細節。隨著專案規模的成長與團隊經驗的累積,持續精進 CI/CD 系統的效能與架構,將成為保持開發效率的關鍵因素。