在企業級的軟體開發環境中,持續整合與持續部署系統的複雜度往往隨著專案規模而指數級增長。單一設定檔的管理模式逐漸無法滿足大型團隊的協作需求,效能瓶頸與維護困境接踵而至。本文將深入探討 GitLab CI/CD 的進階應用技術,透過模組化架構設計、遠端設定引入、父子管線機制等核心概念,建構出既靈活又高效的企業級 CI/CD 解決方案。我們將從基礎的 include 關鍵字應用開始,逐步深入到複雜的父子管線協作模式,探討如何建置符合安全標準的定製化容器,最終整合完整的效能測試與負載測試機制。透過系統化的技術實踐與深度的原理剖析,本文將協助讀者掌握建構高可維護性 CI/CD 系統的核心能力。
模組化管線架構的設計理念與實踐
隨著專案複雜度的提升,傳統的單一 .gitlab-ci.yml 檔案往往會累積成百上千行的設定內容。在這種情況下,任何細微的修改都可能影響到不相關的功能模組,增加了維護風險與協作衝突的機率。模組化管線架構的核心理念,在於將整體 CI/CD 流程依據功能職責進行合理的切分,讓每個模組都擁有清晰的邊界與獨立的責任範圍。這種設計不僅提升了可讀性,更為團隊協作提供了良好的隔離機制。
GitLab 提供的 include 關鍵字是實現模組化架構的基礎工具。這個機制允許我們在主要設定檔中引入其他 YAML 檔案的內容,讓這些分散的設定檔在執行時被合併為一個完整的管線定義。這種合併是在管線啟動前的預處理階段完成,因此從執行效能的角度來看,模組化設計並不會帶來額外的效能開銷。更重要的是,這種設計讓我們能夠根據不同的維度來組織設定檔,例如按照執行階段劃分、按照技術棧分類,或是按照環境區分。
在實際應用中,最常見的模組化方式是依據 CI/CD 的執行階段進行切分。典型的軟體開發流程包含建置、測試、打包、部署等階段,每個階段都有其特定的工具鏈需求與執行邏輯。透過將這些階段的設定獨立為不同的檔案,我們能夠讓專注於特定領域的團隊成員更容易找到需要修改的內容,同時降低了不同領域修改之間相互影響的風險。
# 主要設定檔 .gitlab-ci.yml
# 定義管線的整體結構與引入各功能模組
# 定義管線執行階段
# 階段順序決定了作業的執行先後關係
stages:
- prepare # 準備階段:環境設定與依賴準備
- build # 建置階段:原始碼編譯與打包
- test # 測試階段:各類型測試執行
- package # 封裝階段:建立部署產物
- deploy # 部署階段:發布至各環境
- verify # 驗證階段:部署後的品質確認
# 定義全域變數
# 這些變數可在所有引入的設定檔中使用
variables:
# 專案基本資訊
PROJECT_NAME: "enterprise-application"
PROJECT_VERSION: "1.0.0"
# Docker 映像倉庫設定
DOCKER_REGISTRY: "registry.example.com"
DOCKER_IMAGE_PREFIX: "${DOCKER_REGISTRY}/${PROJECT_NAME}"
# 部署環境 URL
STAGING_URL: "https://staging.example.com"
PRODUCTION_URL: "https://example.com"
# 引入建置相關設定
# 將建置階段的所有作業定義獨立管理
include:
# 本地檔案引入
# local 類型用於同一個專案倉庫內的檔案引用
- local: 'ci/build-backend.yml'
- local: 'ci/build-frontend.yml'
- local: 'ci/build-docker.yml'
# 引入測試相關設定
# 測試階段通常包含多種類型的測試作業
include:
- local: 'ci/test-unit.yml'
- local: 'ci/test-integration.yml'
- local: 'ci/test-performance.yml'
- local: 'ci/test-security.yml'
# 引入部署相關設定
# 不同環境的部署邏輯分別管理
include:
- local: 'ci/deploy-staging.yml'
- local: 'ci/deploy-production.yml'
# 引入共用範本
# 將可重用的作業範本集中管理
include:
- local: 'ci/templates.yml'
這個主要設定檔展現了清晰的架構組織方式。透過將不同功能領域的設定分散到獨立檔案中,主設定檔的職責被簡化為定義整體架構與引入必要模組。這種設計讓新加入團隊的成員能夠快速理解專案的 CI/CD 結構,也讓經驗豐富的維護者能夠迅速定位需要修改的檔案位置。
在建置模組的設計中,我們通常會依據不同的技術棧或建置目標來劃分檔案。前端與後端的建置流程往往使用不同的工具鏈,有著截然不同的依賴管理方式與產出格式。將它們獨立為不同的設定檔,不僅符合職責分離的原則,也讓專精於特定技術的團隊成員能夠專注於自己熟悉的領域。
# 建置模組 ci/build-backend.yml
# 專門處理後端應用程式的建置流程
# 編譯後端應用程式
build_backend_application:
# 指定執行階段
stage: build
# 使用 Maven 官方映像檔
# 版本選擇需考慮專案的 Java 版本需求
image: maven:3.9-eclipse-temurin-17
# 設定快取以加速建置
cache:
# 快取鍵使用分支名稱與 pom.xml 的檢查碼
# 確保不同分支與依賴版本使用獨立快取
key:
files:
- pom.xml
prefix: ${CI_COMMIT_REF_SLUG}-maven
# 快取 Maven 本地倉庫
paths:
- .m2/repository/
# 建置作業不需要更新快取
# 依賴安裝應該在獨立的準備階段完成
policy: pull
# 定義執行指令
script:
# 清理舊的建置產物
- mvn clean
# 執行建置流程
# -Dmaven.repo.local 指定使用快取的本地倉庫
# -DskipTests 跳過測試,測試在獨立階段執行
# -Dmaven.test.skip=true 完全跳過測試編譯
- mvn package -Dmaven.repo.local=.m2/repository -DskipTests
# 顯示建置產出資訊
- ls -lh target/*.jar
# 產生建置資訊檔案供追蹤使用
- |
cat > target/build-info.txt <<EOF
Build Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)
Git Commit: ${CI_COMMIT_SHA}
Git Branch: ${CI_COMMIT_REF_NAME}
Pipeline ID: ${CI_PIPELINE_ID}
EOF
# 定義建置產物
artifacts:
# 產物命名包含版本與提交資訊
name: "backend-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}"
# 設定產物保存期限
expire_in: 1 week
# 指定要保存的路徑
paths:
# 保存編譯後的 JAR 檔案
- target/*.jar
# 保存建置資訊
- target/build-info.txt
# 定義產物報告
reports:
# 如果有依賴掃描報告
# dependency_scanning: target/dependency-check-report.json
# 如果有測試報告也可以在這裡定義
# junit: target/surefire-reports/TEST-*.xml
# 定義執行規則
rules:
# 僅在主要分支與合併請求中執行
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_MERGE_REQUEST_ID
# 也可以在標籤發布時執行
- if: $CI_COMMIT_TAG
# 建立 Docker 映像檔
build_backend_container:
stage: package
# 使用 Docker in Docker
image: docker:24
services:
- docker:24-dind
# Docker in Docker 所需的環境變數
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
# 依賴後端建置作業
dependencies:
- build_backend_application
before_script:
# 驗證 Docker 環境
- docker info
# 登入 Docker 映像倉庫
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
script:
# 驗證建置產物已正確載入
- test -f target/*.jar || exit 1
# 建立映像檔標籤
- export IMAGE_TAG="${DOCKER_IMAGE_PREFIX}/backend:${CI_COMMIT_SHA}"
- export IMAGE_LATEST="${DOCKER_IMAGE_PREFIX}/backend:latest"
# 建置 Docker 映像檔
- docker build -f docker/Dockerfile.backend -t $IMAGE_TAG .
# 標記為最新版本
- docker tag $IMAGE_TAG $IMAGE_LATEST
# 推送到映像倉庫
- docker push $IMAGE_TAG
- docker push $IMAGE_LATEST
# 產生映像檔資訊檔案
- echo "IMAGE=$IMAGE_TAG" > container-info.env
# 儲存映像檔資訊供部署使用
artifacts:
reports:
# 使用 dotenv 報告類型
# 讓後續作業能夠使用這些變數
dotenv: container-info.env
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
這個建置模組展示了完整的後端建置流程,從原始碼編譯到容器映像檔建立。每個作業都有明確的輸入與輸出,透過 artifacts 機制在作業間傳遞建置產物。快取策略的設定確保了依賴套件不會在每次建置時重新下載,而是從快取中快速載入。執行規則的定義則控制了作業在什麼情況下應該被觸發,避免在開發分支上執行不必要的建置操作。
測試模組的設計同樣遵循職責分離的原則。不同類型的測試有著不同的執行時間與資源需求,將它們獨立管理能夠更靈活地控制測試策略。
# 測試模組 ci/test-unit.yml
# 專門處理單元測試的執行
# 執行後端單元測試
test_backend_unit:
stage: test
image: maven:3.9-eclipse-temurin-17
# 依賴建置作業的產物
dependencies:
- build_backend_application
# 使用快取加速測試執行
cache:
key:
files:
- pom.xml
prefix: ${CI_COMMIT_REF_SLUG}-maven
paths:
- .m2/repository/
# 測試作業只需要讀取快取
policy: pull
script:
# 執行單元測試
# -Dmaven.repo.local 使用快取的本地倉庫
# -Dtest 可以指定特定測試類別或方法
- mvn test -Dmaven.repo.local=.m2/repository
# 產生測試覆蓋率報告
- mvn jacoco:report -Dmaven.repo.local=.m2/repository
# 測試完成後保存報告
artifacts:
# 無論測試成功或失敗都保存報告
when: always
# 測試報告保存較短時間
expire_in: 3 days
paths:
# 保存測試報告
- target/surefire-reports/
# 保存覆蓋率報告
- target/site/jacoco/
# 定義報告類型讓 GitLab 能夠解析
reports:
# JUnit 測試報告
junit:
- target/surefire-reports/TEST-*.xml
# 覆蓋率報告
coverage_report:
coverage_format: cobertura
path: target/site/jacoco/jacoco.xml
# 從覆蓋率報告中提取覆蓋率數據
coverage: '/Total.*?([0-9]{1,3})%/'
# 允許在特定情況下失敗
allow_failure:
# 當覆蓋率低於門檻時允許失敗但標記為警告
exit_codes: 1
# 執行前端單元測試
test_frontend_unit:
stage: test
image: node:18-alpine
dependencies:
- build_frontend_application
cache:
key:
files:
- package-lock.json
prefix: ${CI_COMMIT_REF_SLUG}-node
paths:
- node_modules/
- .npm/
policy: pull
script:
# 執行測試並產生覆蓋率報告
- npm run test:unit -- --coverage --ci
artifacts:
when: always
expire_in: 3 days
paths:
- coverage/
- junit.xml
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
coverage: '/Statements\s*:\s*([0-9.]+)%/'
測試模組的設計重點在於確保測試結果的完整性與可追溯性。透過 artifacts 的 reports 機制,GitLab 能夠解析測試報告並在使用者介面上呈現詳細的測試結果與覆蓋率資訊。這些資訊不僅能幫助開發者快速定位問題,也為程式碼審查提供了客觀的品質指標。
遠端設定引入與跨專案協作機制
在大型組織的軟體開發環境中,多個專案往往共享相同的技術棧與開發流程。如果每個專案都獨立維護自己的 CI/CD 設定,不僅會產生大量重複的設定內容,更會在需要統一調整流程時面臨難以同步的困境。GitLab 提供的遠端設定引入機制,讓我們能夠建立中央化的 CI/CD 範本倉庫,實現設定的統一管理與版本控制。
遠端引入機制支援多種來源類型。最常見的是從其他 GitLab 專案引入設定檔,這種方式特別適合組織內部的範本共享。透過指定專案路徑、分支或標籤,以及檔案路徑,我們能夠精確地引用特定版本的範本設定。這種機制不僅提供了版本控制的能力,也確保了不同專案使用範本時的一致性。
# 從其他專案引入設定範例
# 展示如何引用中央範本倉庫的設定
include:
# 從指定專案引入範本
- project: 'platform/ci-templates'
# 指定引用的分支或標籤
# 建議使用語意化版本標籤以確保穩定性
ref: 'v2.1.0'
# 指定要引入的檔案
# 可以使用陣列語法引入多個檔案
file:
# 基礎範本定義
- '/templates/base.yml'
# Docker 建置範本
- '/templates/docker-build.yml'
# Kubernetes 部署範本
- '/templates/k8s-deploy.yml'
# 安全掃描範本
- '/templates/security-scan.yml'
# 從不同專案引入特定領域的範本
- project: 'security/scanning-templates'
ref: 'main'
file: '/sast-scanning.yml'
# 從公開 URL 引入範本
# 適用於引用社群維護的標準範本
- remote: 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml'
# 引入 GitLab 內建範本
# GitLab 提供多種預定義的範本
- template: 'Jobs/Code-Quality.gitlab-ci.yml'
- template: 'Security/Dependency-Scanning.gitlab-ci.yml'
# 覆寫引入範本中的設定
# 本地設定會覆蓋引入範本中的相同鍵值
variables:
# 覆寫範本中定義的變數
DOCKER_DRIVER: overlay2
SECURE_LOG_LEVEL: info
遠端引入機制的強大之處在於它允許設定的層次化組織。中央範本倉庫可以定義組織級別的標準流程,個別專案則可以在引入這些標準範本後,根據自身需求進行客製化調整。這種模式確保了標準流程的一致性,同時保留了必要的彈性空間。
在實際應用中,中央範本倉庫通常會提供不同層級的範本。基礎層範本定義了最通用的設定,例如 Docker 建置的標準流程、安全掃描的基本設定等。這些範本會被多數專案直接使用或作為進一步客製化的基礎。領域特定範本則針對特定技術棧或應用類型提供最佳化的設定,例如 Java 應用程式的建置範本、React 前端的測試範本等。
# 中央範本倉庫範例 - templates/docker-build.yml
# 定義標準的 Docker 建置流程
# 定義可重用的 Docker 建置範本
.docker_build_template:
# 使用官方 Docker 映像檔
image: docker:24
# 啟用 Docker in Docker 服務
services:
- docker:24-dind
# 設定必要的環境變數
variables:
# Docker TLS 憑證目錄
DOCKER_TLS_CERTDIR: "/certs"
# Docker 主機位址
DOCKER_HOST: tcp://docker:2376
# 啟用 TLS 驗證
DOCKER_TLS_VERIFY: 1
# 憑證路徑
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
# 預設的 Dockerfile 路徑
DOCKERFILE_PATH: "Dockerfile"
# 預設的建置上下文路徑
BUILD_CONTEXT: "."
# 前置指令
before_script:
# 驗證 Docker 環境
- docker info
# 登入容器映像倉庫
# 使用 GitLab CI/CD 預定義變數
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
# 主要建置指令
script:
# 建立映像檔標籤
# 使用提交 SHA 作為唯一識別
- export IMAGE_TAG="${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}"
- export IMAGE_LATEST="${CI_REGISTRY_IMAGE}:latest"
# 建置 Docker 映像檔
# 使用 BuildKit 提升建置效能
- |
docker build \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${CI_COMMIT_SHA} \
--build-arg VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} \
-f ${DOCKERFILE_PATH} \
-t ${IMAGE_TAG} \
${BUILD_CONTEXT}
# 標記為最新版本
- docker tag ${IMAGE_TAG} ${IMAGE_LATEST}
# 推送到映像倉庫
- docker push ${IMAGE_TAG}
- docker push ${IMAGE_LATEST}
# 清理本地映像檔以節省空間
- docker rmi ${IMAGE_TAG} ${IMAGE_LATEST} || true
# 後置指令
after_script:
# 登出容器映像倉庫
- docker logout ${CI_REGISTRY}
# 定義執行規則
rules:
# 僅在主要分支執行
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# 或在標籤發布時執行
- if: $CI_COMMIT_TAG
# 定義重試策略
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
# 定義進階的多架構建置範本
.docker_multiarch_build_template:
extends: .docker_build_template
# 覆寫建置指令以支援多架構
script:
# 啟用 Docker Buildx
- docker buildx create --use --name multiarch-builder
# 建置並推送多架構映像檔
- |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${CI_COMMIT_SHA} \
--build-arg VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} \
-f ${DOCKERFILE_PATH} \
-t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} \
-t ${CI_REGISTRY_IMAGE}:latest \
--push \
${BUILD_CONTEXT}
after_script:
# 清理 Buildx 建置器
- docker buildx rm multiarch-builder || true
- docker logout ${CI_REGISTRY}
這個中央範本展示了如何定義可重用且具有高度彈性的建置流程。範本使用變數來參數化關鍵設定,讓使用範本的專案能夠透過覆寫變數來調整行為,而不需要修改範本本身。這種設計確保了範本的穩定性,同時提供了必要的客製化能力。
專案在使用中央範本時,只需要引入範本並繼承對應的作業定義,再根據需求覆寫特定的設定即可。
# 專案層級的設定檔
# 展示如何使用中央範本並進行客製化
# 引入中央範本
include:
- project: 'platform/ci-templates'
ref: 'v2.1.0'
file: '/templates/docker-build.yml'
# 定義專案的 Docker 建置作業
# 繼承中央範本的設定
build_api_container:
# 繼承範本
extends: .docker_build_template
# 指定執行階段
stage: package
# 覆寫變數以客製化建置流程
variables:
# 指定專案特定的 Dockerfile
DOCKERFILE_PATH: "docker/Dockerfile.api"
# 指定建置上下文
BUILD_CONTEXT: "."
# 依賴建置作業
dependencies:
- build_backend_application
# 定義多架構建置作業
build_multiarch_container:
extends: .docker_multiarch_build_template
stage: package
variables:
DOCKERFILE_PATH: "docker/Dockerfile.production"
# 僅在標籤發布時執行多架構建置
rules:
- if: $CI_COMMIT_TAG
這種架構設計帶來多重優勢。中央範本的統一管理確保了組織內所有專案都遵循相同的標準流程,當需要更新建置流程或修復安全問題時,只需要更新中央範本即可。專案層級的設定則保持簡潔,專注於專案特定的配置,降低了維護負擔。版本化的範本引用機制確保了穩定性,專案可以選擇在適當的時機升級到新版本的範本,而不會被強制更新而導致非預期的行為變化。
父子管線機制的深度應用
當 CI/CD 流程的複雜度達到一定程度時,即使透過模組化設計將設定拆分為多個檔案,整體管線的執行時間與資源消耗仍然可能成為瓶頸。父子管線機制提供了一種更進階的架構模式,讓我們能夠將管線依據邏輯功能或執行條件進行動態切分,實現更細粒度的並行控制與條件執行。
父子管線的核心概念在於將管線的不同部分定義為獨立的子管線,由父管線根據特定條件來觸發這些子管線的執行。與 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 minClassWidth 140
start
:父管線啟動;
partition "條件判斷階段" {
:評估執行條件;
if (需要執行前端子管線?) then (是)
:觸發前端子管線;
fork
:前端建置;
:前端測試;
:前端打包;
end fork
else (否)
:跳過前端流程;
endif
if (需要執行後端子管線?) then (是)
:觸發後端子管線;
fork
:後端建置;
:後端測試;
:後端打包;
end fork
else (否)
:跳過後端流程;
endif
}
partition "整合階段" {
:等待所有子管線完成;
:收集子管線產物;
:執行整合測試;
}
partition "部署階段" {
:部署至測試環境;
:執行驗證測試;
}
:父管線完成;
stop
@enduml此流程圖展示了父子管線機制的完整執行邏輯。父管線在啟動後首先評估執行條件,決定哪些子管線需要被觸發。不同的子管線可以並行執行,彼此之間互不影響。當所有需要的子管線都完成後,父管線進入整合階段,收集子管線產生的產物並執行跨模組的整合測試。最終,父管線執行部署流程並完成整體任務。
在實際應用中,父子管線機制特別適合處理專案中的獨立模組。例如,在一個包含前端與後端的全端應用專案中,前端與後端的建置測試流程可以完全獨立執行。當開發者只修改了前端程式碼時,後端的子管線可以被跳過,節省了大量的執行時間與運算資源。
# 父管線設定檔 .gitlab-ci.yml
# 定義主要管線流程與子管線觸發邏輯
stages:
- trigger # 子管線觸發階段
- integrate # 整合階段
- deploy # 部署階段
# 定義變數用於控制子管線觸發
variables:
# 檢測檔案變更來決定是否觸發對應的子管線
FRONTEND_CHANGED: "false"
BACKEND_CHANGED: "false"
# 檢測前端程式碼變更
detect_frontend_changes:
stage: .pre
# 使用輕量級映像檔執行檢測
image: alpine/git:latest
script:
# 比對當前提交與上一次提交的差異
# 檢查 frontend/ 目錄下是否有變更
- |
if git diff --name-only ${CI_COMMIT_BEFORE_SHA} ${CI_COMMIT_SHA} | grep -q '^frontend/'; then
echo "FRONTEND_CHANGED=true" >> detect.env
else
echo "FRONTEND_CHANGED=false" >> detect.env
fi
# 將檢測結果儲存為環境變數
artifacts:
reports:
dotenv: detect.env
# 檢測後端程式碼變更
detect_backend_changes:
stage: .pre
image: alpine/git:latest
script:
- |
if git diff --name-only ${CI_COMMIT_BEFORE_SHA} ${CI_COMMIT_SHA} | grep -q '^backend/'; then
echo "BACKEND_CHANGED=true" >> detect.env
else
echo "BACKEND_CHANGED=false" >> detect.env
fi
artifacts:
reports:
dotenv: detect.env
# 觸發前端子管線
trigger_frontend_pipeline:
stage: trigger
# 使用 trigger 關鍵字定義子管線
trigger:
# 引入子管線的設定檔
include:
- local: 'ci/pipelines/frontend-pipeline.yml'
# 子管線的執行策略
strategy: depend
# 依賴變更檢測作業
needs:
- detect_frontend_changes
# 定義觸發規則
rules:
# 僅在前端有變更時觸發
- if: $FRONTEND_CHANGED == "true"
# 觸發後端子管線
trigger_backend_pipeline:
stage: trigger
trigger:
include:
- local: 'ci/pipelines/backend-pipeline.yml'
strategy: depend
needs:
- detect_backend_changes
rules:
- if: $BACKEND_CHANGED == "true"
# 整合測試作業
integration_tests:
stage: integrate
image: node:18-alpine
# 等待所有子管線完成
needs:
- job: trigger_frontend_pipeline
optional: true
- job: trigger_backend_pipeline
optional: true
script:
# 執行端到端整合測試
- npm install
- npm run test:integration
# 僅在至少一個子管線執行時才執行整合測試
rules:
- if: $FRONTEND_CHANGED == "true" || $BACKEND_CHANGED == "true"
父管線的設計關鍵在於靈活的條件控制。透過檢測程式碼變更範圍,我們能夠精確決定哪些子管線需要被執行。這種智慧型的觸發機制大幅減少了不必要的建置與測試時間,特別是在大型專案中,其效益更為顯著。
子管線的設定檔則定義了完整的執行流程,與一般的管線設定並無二致。子管線可以包含多個階段與作業,擁有自己的快取策略與產物管理。
# 前端子管線設定檔 ci/pipelines/frontend-pipeline.yml
# 定義前端相關的完整建置測試流程
# 定義子管線的執行階段
stages:
- prepare
- build
- test
- package
# 定義子管線專用的變數
variables:
# 前端專案路徑
FRONTEND_DIR: "frontend"
# Node.js 快取目錄
NPM_CACHE_DIR: "${CI_PROJECT_DIR}/.npm"
# 安裝前端依賴
install_frontend_deps:
stage: prepare
image: node:18-alpine
cache:
key:
files:
- ${FRONTEND_DIR}/package-lock.json
prefix: frontend-deps
paths:
- ${FRONTEND_DIR}/node_modules/
- ${NPM_CACHE_DIR}
policy: push
script:
# 切換到前端目錄
- cd ${FRONTEND_DIR}
# 設定 npm 快取目錄
- npm config set cache ${NPM_CACHE_DIR} --global
# 安裝依賴
- npm ci --prefer-offline
artifacts:
paths:
- ${FRONTEND_DIR}/node_modules/
expire_in: 1 hour
# 建置前端應用程式
build_frontend:
stage: build
image: node:18-alpine
needs:
- install_frontend_deps
cache:
key:
files:
- ${FRONTEND_DIR}/package-lock.json
prefix: frontend-deps
paths:
- ${FRONTEND_DIR}/node_modules/
- ${NPM_CACHE_DIR}
policy: pull
script:
- cd ${FRONTEND_DIR}
# 執行建置
- npm run build
# 產生建置報告
- npm run analyze || true
artifacts:
name: "frontend-build-${CI_COMMIT_SHORT_SHA}"
paths:
- ${FRONTEND_DIR}/dist/
- ${FRONTEND_DIR}/build-report.html
expire_in: 1 week
# 執行前端單元測試
test_frontend_unit:
stage: test
image: node:18-alpine
needs:
- install_frontend_deps
cache:
key:
files:
- ${FRONTEND_DIR}/package-lock.json
prefix: frontend-deps
paths:
- ${FRONTEND_DIR}/node_modules/
policy: pull
script:
- cd ${FRONTEND_DIR}
# 執行單元測試
- npm run test:unit -- --coverage --ci
coverage: '/Statements\s*:\s*([0-9.]+)%/'
artifacts:
when: always
reports:
junit: ${FRONTEND_DIR}/junit.xml
coverage_report:
coverage_format: cobertura
path: ${FRONTEND_DIR}/coverage/cobertura-coverage.xml
# 執行前端 Lint 檢查
lint_frontend:
stage: test
image: node:18-alpine
needs:
- install_frontend_deps
cache:
key:
files:
- ${FRONTEND_DIR}/package-lock.json
prefix: frontend-deps
paths:
- ${FRONTEND_DIR}/node_modules/
policy: pull
script:
- cd ${FRONTEND_DIR}
# 執行程式碼風格檢查
- npm run lint
# 執行 TypeScript 型別檢查
- npm run type-check
allow_failure: true
# 建置前端容器映像檔
package_frontend:
stage: package
image: docker:24
services:
- docker:24-dind
needs:
- build_frontend
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker info
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
script:
- cd ${FRONTEND_DIR}
# 建置前端容器映像檔
- docker build -f Dockerfile -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
子管線的完整定義展現了模組化設計的優勢。前端相關的所有作業都集中在這個子管線中,與後端的建置測試流程完全隔離。這種隔離不僅提升了可維護性,也為並行執行提供了基礎。當前端與後端的子管線同時執行時,整體的管線執行時間能夠大幅縮短。
父子管線機制還支援跨專案的子管線觸發。這種能力讓我們能夠在不同倉庫間建立協作關係,例如當核心函式庫更新時自動觸發相依專案的測試管線,確保變更不會破壞現有功能。
# 跨專案觸發子管線範例
# 當核心函式庫更新時觸發相依專案測試
# 觸發相依專案的測試
trigger_dependent_projects:
stage: notify
trigger:
# 指定要觸發的專案
project: applications/web-frontend
# 指定分支
branch: main
# 定義觸發策略
strategy: depend
# 僅在主分支的標籤發布時觸發
rules:
- if: $CI_COMMIT_TAG && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
定製化容器的安全性與效能最佳化
在 GitLab CI/CD 的執行環境中,所有作業都在容器內執行。容器的設計品質直接影響管線的執行效率與安全性。定製化容器的建置需要在功能完整性、安全性與執行效能之間取得平衡。一個設計良好的 CI/CD 容器應該只包含執行特定任務所需的最小元件集,避免不必要的軟體套件造成的安全風險與映像檔膨脹。
容器大小的控制是效能最佳化的首要考量。過大的容器映像檔不僅增加了下載時間,也消耗了更多的儲存空間與網路頻寬。在設計容器時,應該遵循單一職責原則,讓每個容器專注於特定的工具鏈。例如,Java 建置容器應該只包含 JDK 與 Maven,而不應該混雜 Node.js 或其他無關的工具。這種專一性的設計不僅減小了容器大小,也降低了安全風險暴露面。
# 定製化 Node.js CI/CD 容器
# 專門用於前端專案的建置與測試
# 使用輕量級的 Alpine Linux 作為基礎映像檔
# Alpine 相較於 Debian 系統大幅縮小了映像檔體積
FROM node:18-alpine
# 設定維護者資訊與映像檔元資料
# 這些標籤有助於映像檔管理與追蹤
LABEL maintainer="devops@example.com" \
description="Purpose-built Node.js container for CI/CD pipelines" \
version="1.0.0"
# 安裝必要的系統套件
# 使用 && 串接指令減少映像檔層數
# --no-cache 參數避免 APK 快取被包含在映像檔中
RUN apk update && \
apk add --no-cache \
# Git 用於版本控制操作
git \
# OpenSSH 用於 Git over SSH
openssh-client \
# Python 與 make 用於某些 npm 套件的原生編譯
python3 \
make \
g++ && \
# 清理 APK 快取
rm -rf /var/cache/apk/*
# 設定 npm 全域目錄
# 避免權限問題並提供更好的快取控制
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global \
PATH=/home/node/.npm-global/bin:$PATH
# 建立必要的目錄結構
# 確保目錄權限正確設定
RUN mkdir -p /home/node/.npm-global && \
mkdir -p /home/node/.npm && \
# 設定目錄權限讓 node 使用者組可以存取
chown -R node:node /home/node/.npm-global && \
chown -R node:node /home/node/.npm
# 切換到非 root 使用者
# 這是重要的安全實踐,避免容器內的權限升級攻擊
# 使用 node 使用者 (UID 1000) 而非隨機 UID
USER node
# 設定工作目錄
WORKDIR /app
# 設定容器啟動時的預設指令
# 使用 echo 指令明確表示此容器用於 CI/CD
# 避免在非 CI 環境中被誤用
CMD ["echo", "This container is designed for CI/CD pipelines. It should not be run directly."]
# 設定健康檢查
# 雖然 CI 容器通常不需要健康檢查
# 但在某些情況下有助於除錯
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node --version || exit 1
這個 Dockerfile 展示了多個最佳化技巧。使用 Alpine Linux 作為基礎映像檔大幅縮小了容器大小,相較於完整的 Debian 系統能節省數百 MB 的空間。透過串接安裝指令並在同一個 RUN 指令中完成清理工作,我們將多個操作壓縮為單一映像檔層,進一步減少了容器大小。切換到非 root 使用者執行是關鍵的安全措施,確保即使容器被入侵,攻擊者也無法在容器內獲得 root 權限。
在建置支援多種工具鏈的容器時,需要特別注意避免過度膨脹。雖然在某些情況下需要在同一個容器中安裝多種工具,但應該謹慎評估每個工具的必要性。
# 多用途建置容器範例
# 包含多種常用的 CI/CD 工具
FROM ubuntu:22.04
# 設定非互動式安裝模式
# 避免安裝過程中出現互動式提示
ENV DEBIAN_FRONTEND=noninteractive
# 安裝基礎工具與多種語言環境
# 注意:這種多工具容器應該謹慎使用
# 僅在確實需要多種工具的場景中使用
RUN apt-get update && \
apt-get install -y --no-install-recommends \
# 版本控制工具
git \
ca-certificates \
# 網路工具
curl \
wget \
# 建置工具
build-essential \
# Python 環境
python3 \
python3-pip \
# Node.js 環境
nodejs \
npm \
# Docker CLI (用於 Docker in Docker)
docker.io && \
# 清理 APT 快取
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 安裝全域的 Python 工具
# 使用 --break-system-packages 在系統 Python 中安裝
RUN pip3 install --no-cache-dir \
--break-system-packages \
# YAML 處理工具
pyyaml \
# HTTP 客戶端
requests
# 安裝全域的 Node.js 工具
RUN npm install -g \
# Linter 工具
eslint \
# 格式化工具
prettier
# 建立非 root 使用者
# 避免使用 root 權限執行 CI 作業
RUN useradd -m -u 1001 -s /bin/bash ciuser && \
# 設定必要的目錄權限
mkdir -p /workspace && \
chown -R ciuser:ciuser /workspace
# 切換使用者
USER ciuser
# 設定工作目錄
WORKDIR /workspace
CMD ["echo", "Multi-purpose CI container ready"]
多用途容器雖然提供了便利性,但應該避免濫用。過大的容器不僅增加了下載時間,也提高了安全風險。理想的做法是為不同的工具鏈建立專用容器,只在確實需要多種工具協同工作時才建立多用途容器。
容器的安全性最佳化還包含定期更新基礎映像檔、掃描已知漏洞,以及最小化權限設定。在 CI/CD 環境中,應該建立定期重建容器映像檔的機制,確保容器內的軟體套件保持最新版本。
# 容器映像檔維護管線
# 定期重建與掃描 CI/CD 容器
stages:
- build
- scan
- publish
# 建置容器映像檔
build_ci_container:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
CONTAINER_NAME: "ci-node"
script:
# 建置容器
- docker build -t ${CONTAINER_NAME}:${CI_COMMIT_SHA} -f dockerfiles/Dockerfile.node .
# 標記為測試版本
- docker tag ${CONTAINER_NAME}:${CI_COMMIT_SHA} ${CONTAINER_NAME}:test
# 儲存映像檔供後續掃描使用
- docker save ${CONTAINER_NAME}:test -o ${CONTAINER_NAME}.tar
artifacts:
paths:
- ${CONTAINER_NAME}.tar
expire_in: 1 day
# 掃描容器安全漏洞
scan_container:
stage: scan
image: aquasec/trivy:latest
needs:
- build_ci_container
variables:
CONTAINER_NAME: "ci-node"
script:
# 載入映像檔
- docker load -i ${CONTAINER_NAME}.tar
# 執行 Trivy 掃描
# --severity 指定要報告的漏洞嚴重程度
# --exit-code 1 在發現高危漏洞時失敗
- trivy image --severity HIGH,CRITICAL --exit-code 1 ${CONTAINER_NAME}:test
# 產生完整報告
- trivy image --format json --output trivy-report.json ${CONTAINER_NAME}:test
artifacts:
reports:
# 將掃描結果整合到 GitLab 安全報告
container_scanning: trivy-report.json
allow_failure: false
# 發布容器映像檔
publish_container:
stage: publish
image: docker:24
services:
- docker:24-dind
needs:
- build_ci_container
- scan_container
variables:
DOCKER_TLS_CERTDIR: "/certs"
CONTAINER_NAME: "ci-node"
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
# 載入映像檔
- docker load -i ${CONTAINER_NAME}.tar
# 重新標記為正式版本
- docker tag ${CONTAINER_NAME}:test ${CI_REGISTRY_IMAGE}/${CONTAINER_NAME}:${CI_COMMIT_SHA}
- docker tag ${CONTAINER_NAME}:test ${CI_REGISTRY_IMAGE}/${CONTAINER_NAME}:latest
# 推送到映像倉庫
- docker push ${CI_REGISTRY_IMAGE}/${CONTAINER_NAME}:${CI_COMMIT_SHA}
- docker push ${CI_REGISTRY_IMAGE}/${CONTAINER_NAME}:latest
rules:
# 僅在掃描通過後發布
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
這個容器維護管線確保了所有用於 CI/CD 的容器都經過安全掃描,並且定期更新。透過自動化的建置與掃描流程,我們能夠及時發現並修復容器中的安全漏洞,降低供應鏈攻擊的風險。
效能測試與負載測試的整合實作
在現代軟體開發流程中,效能測試應該與功能測試享有同等的重視程度。將效能測試整合到 CI/CD 管線中,能夠在開發早期階段就發現效能退化問題,避免問題累積到生產環境才被發現。GitLab 提供了內建的效能測試整合能力,支援瀏覽器效能測試與負載測試兩種主要類型。
瀏覽器效能測試專注於前端應用程式的載入速度與使用者體驗指標。這類測試會模擬真實使用者訪問網頁的過程,測量首次內容繪製時間、首次有意義繪製時間、可互動時間等關鍵指標。這些指標直接反映了使用者感知到的應用程式回應速度,對於提升使用者體驗至關重要。
# 瀏覽器效能測試整合
# 測量前端應用程式的載入效能
# 引入 GitLab 提供的效能測試範本
include:
# 使用內建的瀏覽器效能測試範本
- template: Verify/Browser-Performance.gitlab-ci.yml
# 客製化效能測試設定
browser_performance:
# 指定測試目標 URL
# 這應該是已部署的應用程式位址
variables:
# 測試目標位址
URL: "https://staging.example.com"
# 效能測試工具的額外選項
# 可以調整測試參數以符合需求
SITESPEED_OPTIONS: "--spa --mobile"
# 定義執行階段
stage: verify
# 僅在部署完成後執行
needs:
- deploy_staging
# 定義執行規則
rules:
# 僅在主要分支與合併請求中執行
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_MERGE_REQUEST_ID
# 允許效能測試失敗但仍繼續管線
# 這避免效能問題阻斷整個部署流程
allow_failure: true
這個簡單的設定就能啟用完整的瀏覽器效能測試功能。GitLab 會使用 Sitespeed.io 工具執行測試,並將結果整合到合併請求的介面中。開發者能夠直接在合併請求中看到程式碼變更對效能的影響,及早發現效能退化問題。
負載測試則關注應用程式在高並發情境下的表現。透過模擬大量使用者同時訪問系統,我們能夠評估應用程式的承載能力、找出效能瓶頸,並驗證系統的可擴展性設計。k6 是一個強大的負載測試工具,GitLab 提供了完整的整合支援。
# k6 負載測試整合
# 評估應用程式的承載能力
# 引入 k6 負載測試範本
include:
- template: Verify/Load-Performance-Testing.gitlab-ci.yml
# 客製化負載測試設定
load_performance:
# 指定測試腳本路徑
variables:
# k6 測試腳本檔案路徑
K6_TEST_FILE: "performance-tests/load-test.js"
# k6 執行選項
K6_OPTIONS: "--out json=results.json"
stage: verify
# 僅在測試環境部署後執行
needs:
- deploy_staging
# 定義執行規則
rules:
# 僅在主要分支執行負載測試
# 避免在開發分支上消耗過多資源
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# 負載測試可能需要較長時間
timeout: 30m
allow_failure: true
k6 測試腳本需要定義測試情境、虛擬使用者行為,以及效能指標的閾值。一個設計良好的負載測試腳本應該模擬真實的使用者行為模式,而不僅僅是發送大量的簡單請求。
// k6 負載測試腳本
// 定義測試情境與效能評估標準
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// 定義自訂效能指標
// 這些指標會在測試報告中顯示
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
const successfulRequests = new Counter('successful_requests');
// 定義測試選項
// 控制測試的執行方式與效能閾值
export const options = {
// 定義測試階段
// 模擬使用者數量的漸進式增長與穩定階段
stages: [
// 第一階段:5 分鐘內逐步增加到 50 個虛擬使用者
// 這個暖機階段讓系統有時間啟動快取與連線池
{ duration: '5m', target: 50 },
// 第二階段:維持 50 個虛擬使用者 10 分鐘
// 這是穩定負載階段,評估系統的持續效能
{ duration: '10m', target: 50 },
// 第三階段:增加到 100 個虛擬使用者並維持 10 分鐘
// 評估系統在更高負載下的表現
{ duration: '5m', target: 100 },
{ duration: '10m', target: 100 },
// 第四階段:5 分鐘內逐步減少到 0
// 冷卻階段,觀察系統資源釋放情況
{ duration: '5m', target: 0 },
],
// 定義效能閾值
// 當這些條件不滿足時測試會被標記為失敗
thresholds: {
// HTTP 請求失敗率必須低於 1%
'http_req_failed': ['rate<0.01'],
// 95% 的請求回應時間必須低於 500ms
'http_req_duration': ['p(95)<500'],
// 99% 的請求回應時間必須低於 1000ms
'http_req_duration': ['p(99)<1000'],
// 自訂的錯誤率指標
'errors': ['rate<0.05'],
},
// 設定測試的並發限制
// 避免測試工具本身成為瓶頸
batch: 10,
batchPerHost: 5,
};
// 測試設定函式
// 在測試開始前執行一次
export function setup() {
// 可以在這裡執行準備工作
// 例如建立測試資料、取得認證令牌等
console.log('Test setup completed');
return {};
}
// 預設測試函式
// 每個虛擬使用者會重複執行這個函式
export default function() {
// 定義測試情境分組
// 模擬使用者的典型操作流程
// 情境 1:訪問首頁
group('Homepage Visit', function() {
// 發送 HTTP GET 請求
const response = http.get('https://staging.example.com/');
// 檢查回應狀態
// check 函式用於驗證回應是否符合預期
const checkResult = check(response, {
// 驗證 HTTP 狀態碼
'status is 200': (r) => r.status === 200,
// 驗證回應內容
'contains expected content': (r) => r.body.includes('Welcome'),
// 驗證回應時間
'response time < 500ms': (r) => r.timings.duration < 500,
});
// 記錄檢查結果
errorRate.add(!checkResult);
if (checkResult) {
successfulRequests.add(1);
}
// 記錄回應時間
responseTime.add(response.timings.duration);
// 模擬使用者閱讀時間
// 真實使用者不會立即發送下一個請求
sleep(1);
});
// 情境 2:搜尋功能
group('Search Functionality', function() {
// 準備搜尋請求的參數
const searchParams = {
query: 'test product',
limit: 20,
};
// 發送搜尋請求
const response = http.get(
'https://staging.example.com/api/search',
{ params: searchParams }
);
// 驗證搜尋回應
const checkResult = check(response, {
'status is 200': (r) => r.status === 200,
'results count > 0': (r) => {
try {
const data = JSON.parse(r.body);
return data.results && data.results.length > 0;
} catch (e) {
return false;
}
},
'response time < 1000ms': (r) => r.timings.duration < 1000,
});
errorRate.add(!checkResult);
responseTime.add(response.timings.duration);
sleep(2);
});
// 情境 3:API 端點測試
group('API Operations', function() {
// 準備 API 請求的標頭
const headers = {
'Content-Type': 'application/json',
// 在實際測試中應該使用真實的認證令牌
// 'Authorization': `Bearer ${authToken}`,
};
// 準備請求內容
const payload = JSON.stringify({
action: 'test',
data: { id: 123 },
});
// 發送 POST 請求
const response = http.post(
'https://staging.example.com/api/action',
payload,
{ headers: headers }
);
// 驗證 API 回應
check(response, {
'status is 200 or 201': (r) => r.status === 200 || r.status === 201,
'has response body': (r) => r.body.length > 0,
});
sleep(1);
});
}
// 測試結束後執行
// 可以在這裡執行清理工作
export function teardown(data) {
console.log('Test teardown completed');
}
這個完整的 k6 測試腳本展示了如何設計真實的負載測試情境。測試階段的漸進式設計讓系統有時間適應負載變化,避免突然的負載增加造成的誤判。效能閾值的定義提供了明確的效能標準,當系統無法滿足這些標準時測試會自動失敗。情境分組與使用者行為模擬確保測試能夠反映真實的使用模式。
將效能測試整合到 CI/CD 管線後,每次程式碼變更都會經過效能評估。這種持續的效能監控機制能夠及早發現效能退化問題,避免效能債務的累積。同時,歷史效能資料的累積也為效能最佳化提供了客觀的評估基準,幫助團隊做出有資料支持的決策。
透過本文深入探討的模組化架構設計、遠端設定引入、父子管線機制、定製化容器建置,以及效能測試整合等進階技術,我們建構了一套完整的企業級 GitLab CI/CD 解決方案。這些技術的綜合運用不僅提升了管線的執行效率與可維護性,更為團隊協作提供了良好的基礎設施。在實際應用中,建議從基礎的模組化設計開始,逐步導入更進階的功能,確保團隊能夠充分理解與掌握每個技術的細節。持續的最佳化與改進是 CI/CD 系統成功的關鍵,唯有不斷審視流程、收集回饋並調整策略,才能建構出真正符合組織需求的高效能 CI/CD 系統。