GitLab CI/CD
GitLab CI/CD 是 GitLab 内置的持续集成 / 持续交付系统,通过项目根目录的 .gitlab-ci.yml 文件定义流水线,无需额外安装服务器,推送代码即可自动触发。
核心概念:
| 概念 | 说明 |
|---|---|
| Pipeline | 一次完整的 CI/CD 执行过程,由多个 Stage 组成 |
| Stage | 阶段,同一 Stage 内的 Job 并行执行,Stage 之间串行 |
| Job | 最小执行单元,定义具体执行什么命令 |
| Runner | 执行 Job 的代理程序,分 Shared / Group / Project Runner |
| Artifact | Job 产物,可在后续 Job 中下载使用 |
| Cache | 跨 Job / Pipeline 复用的缓存(如 node_modules) |
| Environment | 部署目标环境,如 dev / staging / production |
| Variable | 变量,可在 UI 中配置敏感信息,Job 中通过 $VAR 引用 |
| Trigger | 触发器,支持 Push、MR、Tag、Schedule、API 等 |
| Include | 引入外部 YAML 片段,实现配置复用 |
.gitlab-ci.yml 基本结构
# 定义 Stage 执行顺序(同 Stage 并行,Stage 间串行)
stages:
- build
- test
- scan
- package
- deploy
# 全局变量
variables:
APP_NAME: my-app
DOCKER_REGISTRY: registry.gitlab.com/$CI_PROJECT_PATH
# 全局默认配置(所有 Job 继承)
default:
image: ubuntu:22.04
tags:
- docker
before_script:
- echo "开始执行:$CI_JOB_NAME"
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
# ── Job 定义 ──────────────────────────────────────────
build:
stage: build
image: maven:3.9-eclipse-temurin-17
script:
- mvn clean package -DskipTests
artifacts:
paths:
- target/*.jar
expire_in: 1 hour
unit-test:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- mvn test
artifacts:
when: always
reports:
junit: target/surefire-reports/**/*.xml
deploy-prod:
stage: deploy
script:
- ./deploy.sh production
environment:
name: production
url: https://app.example.com
when: manual # 手动触发
only:
- main
内置变量(Predefined Variables)
| 变量 | 说明 |
|---|---|
$CI_PIPELINE_ID |
Pipeline 唯一 ID |
$CI_PIPELINE_IID |
Pipeline 项目内编号 |
$CI_JOB_ID |
Job 唯一 ID |
$CI_JOB_NAME |
Job 名称 |
$CI_JOB_STAGE |
Job 所在 Stage |
$CI_COMMIT_SHA |
Commit SHA(完整) |
$CI_COMMIT_SHORT_SHA |
Commit SHA(前 8 位) |
$CI_COMMIT_BRANCH |
分支名 |
$CI_COMMIT_TAG |
Tag 名(Tag 触发时) |
$CI_COMMIT_MESSAGE |
Commit 信息 |
$CI_DEFAULT_BRANCH |
默认分支(通常是 main) |
$CI_PROJECT_NAME |
项目名 |
$CI_PROJECT_PATH |
项目路径(group/project) |
$CI_PROJECT_URL |
项目 URL |
$CI_REGISTRY |
GitLab Container Registry 地址 |
$CI_REGISTRY_IMAGE |
当前项目的 Registry 镜像地址 |
$CI_REGISTRY_USER |
Registry 登录用户名 |
$CI_REGISTRY_PASSWORD |
Registry 登录密码 |
$CI_MERGE_REQUEST_IID |
MR 编号(MR Pipeline 中) |
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME |
MR 源分支 |
$CI_ENVIRONMENT_NAME |
部署环境名 |
$CI_RUNNER_DESCRIPTION |
Runner 描述 |
$GITLAB_USER_NAME |
触发构建的用户名 |
Job 配置详解
image 与 services
build:
image: node:20-alpine # 使用指定镜像运行 Job
test-with-db:
image: python:3.12
services:
- name: postgres:15 # 启动附加服务容器
alias: db # 通过 hostname "db" 访问
- name: redis:7
alias: cache
variables:
POSTGRES_DB: test_db
POSTGRES_USER: runner
POSTGRES_PASSWORD: secret
DATABASE_URL: postgresql://runner:secret@db/test_db
script:
- pip install -r requirements.txt
- pytest
script、before_script、after_script
deploy:
before_script:
- apt-get update -qq && apt-get install -y curl
- curl -LO "https://dl.k8s.io/release/$(curl -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl && mv kubectl /usr/local/bin/
script:
- kubectl set image deployment/$APP_NAME $APP_NAME=$IMAGE_TAG
after_script:
- echo "部署完成,清理临时文件"
- rm -f /tmp/kubeconfig
artifacts(产物)
build:
script:
- mvn clean package
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" # 产物名称
paths:
- target/*.jar
- dist/
exclude:
- target/**/*.tmp
expire_in: 7 days # 过期时间(不设置则根据实例配置)
when: on_success # on_success | on_failure | always
reports:
junit: target/surefire-reports/**/*.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
sast: gl-sast-report.json
dast: gl-dast-report.json
cache(缓存)
# 全局缓存配置
cache:
key:
files:
- package-lock.json # 以锁文件内容为 key(内容变化才失效)
paths:
- node_modules/
- .npm/
policy: pull-push # pull-push(默认)| pull | push
# Job 级别覆盖缓存
build:
cache:
key: "$CI_COMMIT_REF_SLUG" # 以分支名为 key
paths:
- .gradle/
policy: pull # 只拉不推(只读缓存,加速)
rules(触发规则)— 推荐方式
deploy-prod:
script: ./deploy.sh prod
rules:
# 在 main 分支且不是 MR 时运行(手动确认)
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE != "merge_request_event"'
when: manual
# 在 Tag 上自动运行
- if: '$CI_COMMIT_TAG'
when: on_success
# 其他情况不运行
- when: never
# 文件变更触发
frontend-build:
script: npm run build
rules:
- changes:
- src/frontend/**/*
- package*.json
when: on_success
- when: never
# 变量条件
integration-test:
script: npm run test:e2e
rules:
- if: '$RUN_E2E == "true"'
- if: '$CI_COMMIT_BRANCH == "main"'
only / except(旧写法,了解即可)
# only:只在满足条件时运行
deploy:
only:
- main
- /^release\/.*/ # 正则匹配 release/* 分支
- tags
# except:排除条件
test:
except:
- schedules
- triggers
when(执行时机)
| 值 | 说明 |
|---|---|
on_success |
前置 Job 全部成功(默认) |
on_failure |
有 Job 失败时运行(常用于通知) |
always |
无论成功失败都运行 |
manual |
手动触发(Pipeline 暂停等待点击) |
delayed |
延迟执行(配合 start_in) |
never |
不运行 |
notify-on-failure:
stage: .post
script: ./notify.sh "构建失败"
when: on_failure
delayed-deploy:
stage: deploy
script: ./deploy.sh canary
when: delayed
start_in: 30 minutes
needs(DAG 依赖,打破 Stage 限制)
# 不等待同 Stage 其他 Job,直接依赖指定 Job 完成后运行
# 实现有向无环图(DAG)调度,大幅缩短 Pipeline 时长
build-backend:
stage: build
script: mvn package
build-frontend:
stage: build
script: npm run build
# 不等 test Stage 所有 Job,只要 build-backend 完成即可运行
test-backend:
stage: test
needs: [build-backend]
script: mvn test
# 跨 Stage 依赖,直接在 build-frontend 完成后运行(跳过 test Stage 等待)
deploy-frontend:
stage: deploy
needs:
- job: build-frontend
artifacts: true # 下载该 Job 的产物
script: aws s3 sync dist/ s3://bucket/
parallel(矩阵并行)
# 同一 Job 以不同参数并行运行
test:
stage: test
parallel:
matrix:
- PYTHON_VERSION: ["3.10", "3.11", "3.12"]
DB: ["postgres", "mysql"]
image: python:$PYTHON_VERSION
script:
- pip install -r requirements.txt
- pytest --db=$DB
# 简单并行(拆分测试)
test:
parallel: 5 # 自动将测试分成 5 份并行跑
script: pytest --splits $CI_NODE_TOTAL --group $CI_NODE_INDEX
environment(部署环境)
deploy-staging:
stage: deploy
script: ./deploy.sh staging
environment:
name: staging
url: https://staging.example.com
on_stop: stop-staging # 关联停止 Job
stop-staging:
stage: deploy
script: ./teardown.sh staging
environment:
name: staging
action: stop
when: manual
变量管理
定义变量的优先级(高→低)
- 触发 Pipeline 时传入的变量(API / Trigger)
- Pipeline 级别变量(
.gitlab-ci.yml中的variables) - Project / Group / Instance 级别变量(UI 配置)
.gitlab-ci.yml中default.variables
在 UI 中配置变量
Settings → CI/CD → Variables
| 类型 | 说明 |
|---|---|
| Variable | 普通键值对 |
| File | 将内容写入临时文件,变量值为文件路径 |
| 勾选 Masked | 日志中自动打码 |
| 勾选 Protected | 只在受保护分支 / Tag 的 Pipeline 中可用 |
# 引用变量
deploy:
script:
- echo $APP_ENV
- kubectl --server=$K8S_SERVER --token=$K8S_TOKEN apply -f k8s/
# File 类型变量:$KUBECONFIG 是文件路径
- kubectl --kubeconfig=$KUBECONFIG get pods
组变量与继承
组级别(Group → Settings → CI/CD → Variables)定义的变量会自动被所有子项目和子组继承,适合在多项目中共享 Registry 地址、公共 Token 等配置。
触发器(Triggers)
Push / MR / Tag 触发(默认)
# 只在 MR 时运行(MR Pipeline)
lint:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# 只在 Tag 时运行
release:
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
定时触发(Scheduled Pipeline)
CI/CD → Schedules → New schedule
# 定时 Pipeline 中才运行的 Job
nightly-build:
script: ./full-regression-test.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
API 触发
# 触发指定 ref 的 Pipeline
curl -X POST \
-F "token=YOUR_TRIGGER_TOKEN" \
-F "ref=main" \
-F "variables[ENV]=production" \
"https://gitlab.example.com/api/v4/projects/123/trigger/pipeline"
上游 / 下游 Pipeline(Multi-project Pipeline)
# 在当前 Pipeline 触发另一个项目的 Pipeline
trigger-deploy:
stage: deploy
trigger:
project: devops/deploy-repo
branch: main
strategy: depend # 等待子 Pipeline 完成,状态同步
variables:
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
GitLab Container Registry
GitLab 内置容器镜像仓库,无需额外配置。
variables:
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build-image:
stage: package
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE .
- docker push $IMAGE
# 同时打 latest
- docker tag $IMAGE $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
使用 Kaniko(无需特权模式)
build-image:
stage: package
image:
name: gcr.io/kaniko-project/executor:v1.23.0-debug
entrypoint: [""]
script:
- /kaniko/executor
--context=$CI_PROJECT_DIR
--dockerfile=$CI_PROJECT_DIR/Dockerfile
--destination=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--destination=$CI_REGISTRY_IMAGE:latest
--cache=true
include(配置复用)
# 引入本仓库其他文件
include:
- local: '.gitlab/ci/build.yml'
- local: '.gitlab/ci/test.yml'
# 引入其他项目的模板
- project: 'devops/ci-templates'
ref: main
file: '/templates/docker-build.yml'
# 引入 GitLab 官方模板
- template: Security/SAST.gitlab-ci.yml
- template: Code-Quality.gitlab-ci.yml
# 引入远程 URL
- remote: 'https://raw.githubusercontent.com/example/ci/main/template.yml'
模板 Job(使用 & 锚点和 extends)
# 定义模板(以 . 开头的 Job 不会被执行)
.deploy-template:
image: alpine:3.19
before_script:
- apk add --no-cache curl
- curl -LO https://dl.k8s.io/release/.../kubectl
script:
- kubectl set image deployment/$APP_NAME $APP_NAME=$IMAGE_TAG -n $NAMESPACE
environment:
url: https://$APP_NAME.$NAMESPACE.example.com
# 继承模板
deploy-staging:
extends: .deploy-template
variables:
NAMESPACE: staging
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
deploy-prod:
extends: .deploy-template
variables:
NAMESPACE: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
GitLab Runner
安装 Runner(Linux)
# 添加仓库
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner
# 注册 Runner(交互式)
sudo gitlab-runner register
# 注册 Runner(非交互式)
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.example.com/" \
--registration-token "YOUR_TOKEN" \
--executor "docker" \
--docker-image "alpine:latest" \
--description "docker-runner" \
--tag-list "docker,linux" \
--run-untagged="true" \
--locked="false"
# 启动服务
sudo gitlab-runner start
sudo gitlab-runner status
安装 Runner(Docker)
docker run -d \
--name gitlab-runner \
--restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gitlab-runner-config:/etc/gitlab-runner \
gitlab/gitlab-runner:latest
# 注册
docker exec -it gitlab-runner gitlab-runner register
Runner Executor 类型
| Executor | 说明 | 适用场景 |
|---|---|---|
docker |
每个 Job 在独立容器运行 | 推荐,隔离性好 |
shell |
直接在 Runner 机器上运行 | 简单场景 |
kubernetes |
在 K8s Pod 中运行 | 云原生环境 |
docker+machine |
自动伸缩 Docker 机器 | 大规模构建 |
virtualbox |
在虚拟机中运行 | 需要完整 OS |
Runner 配置(/etc/gitlab-runner/config.toml)
concurrent = 4 # 最大并发 Job 数
[[runners]]
name = "docker-runner"
url = "https://gitlab.example.com"
token = "YOUR_TOKEN"
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = false
disable_cache = false
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
shm_size = 0
pull_policy = ["if-not-present"] # 优先使用本地镜像
[runners.cache]
Type = "s3"
[runners.cache.s3]
ServerAddress = "minio.example.com"
AccessKey = "ACCESS_KEY"
SecretKey = "SECRET_KEY"
BucketName = "gitlab-runner-cache"
安全扫描(DevSecOps)
GitLab 内置多种安全扫描,Ultimate 版本全部免费,CE/EE 版本可使用模板。
include:
- template: Security/SAST.gitlab-ci.yml # 静态代码安全分析
- template: Security/Secret-Detection.gitlab-ci.yml # 敏感信息检测
- template: Security/Dependency-Scanning.gitlab-ci.yml # 依赖漏洞扫描
- template: Security/Container-Scanning.gitlab-ci.yml # 容器镜像扫描
- template: Security/DAST.gitlab-ci.yml # 动态应用安全测试
variables:
SAST_EXCLUDED_PATHS: "spec,test,tests,tmp"
DS_EXCLUDED_PATHS: "node_modules"
完整示例
Java Spring Boot 项目
stages:
- build
- test
- scan
- package
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
default:
image: maven:3.9-eclipse-temurin-17
cache:
key:
files: [pom.xml]
paths: [.m2/repository/]
# ── Build ────────────────────────────────────────────
compile:
stage: build
script:
- mvn compile -DskipTests
artifacts:
paths: [target/classes/]
expire_in: 1 hour
# ── Test ─────────────────────────────────────────────
unit-test:
stage: test
script:
- mvn test
artifacts:
when: always
reports:
junit: target/surefire-reports/**/*.xml
paths: [target/jacoco.exec]
expire_in: 1 day
code-quality:
stage: test
image: sonarsource/sonar-scanner-cli:latest
variables:
SONAR_USER_HOME: "$CI_PROJECT_DIR/.sonar"
GIT_DEPTH: "0"
cache:
key: sonar-$CI_COMMIT_REF_SLUG
paths: [.sonar/cache]
script:
- sonar-scanner
-Dsonar.projectKey=$CI_PROJECT_NAME
-Dsonar.host.url=$SONAR_HOST_URL
-Dsonar.login=$SONAR_TOKEN
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
# ── Package ──────────────────────────────────────────
package:
stage: package
script:
- mvn package -DskipTests
artifacts:
paths: [target/*.jar]
expire_in: 7 days
build-image:
stage: package
image: docker:24
services: [docker:24-dind]
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $IMAGE_TAG .
- docker push $IMAGE_TAG
- |
if [ "$CI_COMMIT_BRANCH" == "main" ]; then
docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
docker push $CI_REGISTRY_IMAGE:latest
fi
needs: [package]
# ── Deploy ───────────────────────────────────────────
.deploy-base:
image: bitnami/kubectl:latest
script:
- kubectl config set-cluster k8s --server=$K8S_SERVER --certificate-authority=$K8S_CA
- kubectl config set-credentials ci --token=$K8S_TOKEN
- kubectl config set-context ci --cluster=k8s --user=ci --namespace=$NAMESPACE
- kubectl config use-context ci
- sed -i "s|IMAGE_TAG|$IMAGE_TAG|g" k8s/deployment.yaml
- kubectl apply -f k8s/
- kubectl rollout status deployment/$APP_NAME -n $NAMESPACE --timeout=120s
deploy-dev:
extends: .deploy-base
stage: deploy
variables:
NAMESPACE: dev
APP_NAME: my-app
environment:
name: development
url: https://dev.example.com
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
deploy-staging:
extends: .deploy-base
stage: deploy
variables:
NAMESPACE: staging
APP_NAME: my-app
environment:
name: staging
url: https://staging.example.com
rules:
- if: '$CI_COMMIT_BRANCH =~ /^release\//'
deploy-prod:
extends: .deploy-base
stage: deploy
variables:
NAMESPACE: production
APP_NAME: my-app
environment:
name: production
url: https://app.example.com
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: on_success
Node.js 前端项目
stages:
- install
- lint-test
- build
- deploy
variables:
NODE_ENV: production
default:
image: node:20-alpine
cache:
key:
files: [package-lock.json]
paths: [node_modules/, .npm/]
install:
stage: install
script:
- npm ci --cache .npm --prefer-offline
artifacts:
paths: [node_modules/]
expire_in: 1 hour
lint:
stage: lint-test
needs: [install]
script:
- npm run lint
- npm run type-check
test:
stage: lint-test
needs: [install]
script:
- npm run test -- --coverage --watchAll=false
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
build:
stage: build
needs: [lint, test]
script:
- npm run build
artifacts:
paths: [dist/]
expire_in: 7 days
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
pages: # GitLab Pages 自动部署
stage: deploy
needs: [build]
script:
- mv dist public
artifacts:
paths: [public]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
最佳实践
- rules 替代 only/except:
rules语法更灵活、可读性更好,新项目应统一使用rules。 - 合理使用 needs:通过 DAG 依赖消除不必要的等待,尤其是并行的 build + test 阶段。
- 缓存策略:用锁文件(
package-lock.json/pom.xml)作为缓存 key,保证缓存精确失效。 - 产物精简:只将后续 Job 需要的文件声明为 artifact,减少传输开销。设置合理的
expire_in。 - 敏感变量:密码、Token 一律在 GitLab UI 中配置为 Masked + Protected 变量,禁止写入
.gitlab-ci.yml。 - 使用 include 模板:公共的 build / deploy 逻辑抽取到共享模板库,各项目通过
include引入。 - MR Pipeline 快速反馈:lint、单测等轻量 Job 在 MR Pipeline 中运行,完整流程只在主干分支触发。
- 手动审批生产部署:
when: manual+ 配置 Protected Environment(只允许特定角色操作)。 - 限制并发:在 Runner 配置中设置合理的
concurrent,防止资源耗尽。 - Job 超时:为长 Job 设置
timeout防止僵尸任务,默认超时为 60 分钟。