前言

如果你用的是 GitLab 管理代码,那 GitLab CI 就是天然的选择——不需要额外部署 Jenkins,CI/CD 配置直接写在仓库里。本篇带你从零开始:部署 Runner、编写 .gitlab-ci.yml、跑通第一条流水线。


一、GitLab CI 架构


┌──────────────────────────────────────┐
│            GitLab 实例                │
│  ┌──────────┐  ┌──────────────────┐  │
│  │ Git 仓库  │  │  CI/CD 调度引擎   │  │
│  │          │  │  - 解析 .gitlab-ci│  │
│  │          │  │  - 分发 Job 给 Runner│  │
│  └──────────┘  └────────┬─────────┘  │
└─────────────────────────┼────────────┘
                           │ 分发 Job
            ┌──────────────┼──────────────┐
            ▼              ▼              ▼
       ┌─────────┐    ┌─────────┐    ┌─────────┐
       │ Runner 1│    │ Runner 2│    │ Runner 3│
       │ (shell) │    │ (docker)│    │ (k8s)   │
       └─────────┘    └─────────┘    └─────────┘

关键概念:

| 概念 | 说明 |

|------|------|

| Pipeline | 一条流水线,包含多个 Stage |

| Stage | 阶段,同一阶段的 Job 并行执行 |

| Job | 具体任务,是流水线的基本执行单元 |

| Runner | 执行 Job 的 Agent 进程 |

| Executor | Runner 的执行方式:shell / docker / kubernetes |


二、Runner 部署

方式一:Docker 部署(推荐)


# 拉取 Runner 镜像
docker pull gitlab/gitlab-runner:latest

# 启动 Runner
docker run -d \
  --name gitlab-runner \
  --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

# 注册 Runner(从 GitLab → Settings → CI/CD → Runners 获取 Token)
docker exec -it gitlab-runner gitlab-runner register

# 交互式配置:
# 1. GitLab instance URL: https://gitlab.com (或你的 GitLab 地址)
# 2. Registration token: 从 GitLab 获取
# 3. Executor: docker
# 4. Default Docker image: alpine:latest

方式二:Kubernetes 部署


# gitlab-runner-values.yaml
image:
  registry: docker.io
  image: gitlab/gitlab-runner
  tag: alpine-v16.9.1

gitlabUrl: https://gitlab.com

runnerToken: "YOUR_RUNNER_TOKEN"

rbac:
  create: true

runners:
  config: |
    [[runners]]
      name = "k8s-runner"
      executor = "kubernetes"
      [runners.kubernetes]
        namespace = "gitlab-runner"
        image = "alpine:latest"
        privileged = true
        [[runners.kubernetes.volumes.empty_dir]]
          name = "docker-certs"
          mount_path = "/certs/client"
          medium = ""

# 用 Helm 部署
helm repo add gitlab https://charts.gitlab.io
helm install gitlab-runner gitlab/gitlab-runner -f gitlab-runner-values.yaml -n gitlab-runner

Executor 类型选择

| Executor | 隔离性 | 性能 | 适用场景 |

|---------|--------|------|---------|

| shell | 无 | 最快 | 简单脚本、不需要隔离 |

| docker | 好 | 中等 | 需要环境隔离的标准构建 |

| kubernetes | 好 | 弹性 | 大规模 CI、按需扩缩 |

| docker+machine | 好 | 中等 | 需要自动扩缩的 Docker 环境 |

**培训要点**:生产环境推荐 docker executor——每次 Job 在独立容器中运行,环境干净、互不干扰。shell executor 虽然快但环境会互相污染。

三、.gitlab-ci.yml 语法详解

基本结构


# .gitlab-ci.yml
# 定义阶段(按顺序执行,同阶段 Job 并行)
stages:
  - build
  - test
  - deploy

# 全局变量
variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
  DOCKER_REGISTRY: "registry.mycompany.com"

# 构建阶段
build:
  stage: build
  image: maven:3.9-eclipse-temurin-17
  cache:
    key: maven-${CI_COMMIT_REF_SLUG}
    paths:
      - .m2/repository
  script:
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 hour
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

# 测试阶段
test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  needs: [build]
  script:
    - mvn test
  artifacts:
    reports:
      junit: target/surefire-reports/TEST-*.xml

# 部署阶段
deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: [test]
  environment:
    name: production
    url: https://myapp.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script:
    - kubectl set image deployment/myapp app=$DOCKER_REGISTRY/myapp:$CI_COMMIT_SHORT_SHA -n prod
    - kubectl rollout status deployment/myapp -n prod --timeout=180s

关键语法元素

1. image — 指定执行 Job 的容器镜像


job:
  image: maven:3.9-eclipse-temurin-17
  # 或指定 entrypoint
  image:
    name: maven:3.9-eclipse-temurin-17
    entrypoint: [""]  # 覆盖镜像默认 entrypoint

2. variables — 变量定义


variables:
  # 全局变量
  APP_NAME: "myapp"
  
  # 引用 GitLab 内置变量
  IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
  DEPLOY_ENV: "$CI_COMMIT_BRANCH == 'main' ? 'prod' : 'dev'"  # 不支持三元,用 rules
  
# Job 级变量(覆盖全局)
deploy-prod:
  variables:
    K8S_NAMESPACE: "prod"

3. script — 执行脚本


job:
  script:
    - echo "Hello"           # 单行
    - |                       # 多行脚本
      if [ "$CI_COMMIT_BRANCH" = "main" ]; then
        echo "Deploying to prod"
      else
        echo "Deploying to dev"
      fi
    - >                       # 折叠多行
      docker build -t myapp .
      && docker push myapp

  before_script:
    - echo "Before main script"    # 在 script 之前执行
    - docker login $REGISTRY -u $REG_USER -p $REG_PASS

  after_script:
    - echo "After main script"     # 在 script 之后执行(即使 script 失败也执行)

4. rules — 条件触发(推荐替代 only/except)


job:
  rules:
    # PR 时触发
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: on_success
    
    # main 分支推送时触发
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_success
    
    # Tag 发布时触发
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual      # 手动触发
    
    # 定时触发
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: always
    
    # 其他情况不执行
    - when: never

5. needs — 跨阶段依赖(可跳过阶段顺序)


# 不用 needs:按 stages 顺序执行
stages:
  - build
  - test       # build 全部完成后才开始
  - deploy

# 用 needs:Job 级依赖,可以跨阶段
test-unit:
  stage: test
  needs: [build]        # 只等 build 完成就开始,不等其他 test Job

test-integration:
  stage: test
  needs: [build, deploy-test]  # 需要 build 和 deploy-test 都完成

6. cache — 缓存


build:
  cache:
    # 按分支缓存
    key: ${CI_COMMIT_REF_SLUG}
    # 按文件内容缓存(文件不变则用缓存)
    key:
      files:
        - package-lock.json
        - pom.xml
    paths:
      - .m2/repository
      - node_modules/
    policy: pull-push    # 拉取并推送

7. artifacts — 产物传递


build:
  artifacts:
    paths:
      - target/*.jar        # 传递给下游 Job
    reports:
      junit: target/surefire-reports/TEST-*.xml   # 测试报告
      coverage_report:
        coverage_format: jacoco
        path: target/site/jacoco/jacoco.xml      # 覆盖率报告
    expire_in: 1 week       # 产物保留时间
    exclude:
      - target/tmp/**       # 排除不需要的文件

四、模板与扩展

隐藏 Job 模板(Anchor 语法)


# 定义模板(以 . 开头的 Job 不会执行)
.build-template: &build-template
  image: maven:3.9-eclipse-temurin-17
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - .m2/repository
  before_script:
    - echo "Building $APP_NAME"

# 使用模板
build-service-a:
  <<: *build-template
  variables:
    APP_NAME: "service-a"
  script:
    - mvn clean package -DskipTests

build-service-b:
  <<: *build-template
  variables:
    APP_NAME: "service-b"
  script:
    - mvn clean package -DskipTests

include — 引用外部配置


# 引用同一仓库的文件
include:
  - local: '/ci/build-template.yml'
  - local: '/ci/deploy-template.yml'

# 引用其他仓库的文件
include:
  - project: 'devops/ci-templates'
    ref: 'main'
    file: '/java-build.yml'

# 引用远程 URL
include:
  - remote: 'https://raw.githubusercontent.com/myorg/ci-templates/main/java-build.yml'

extends — 继承扩展


.build:
  variables:
    BUILD_DIR: "target"
  script:
    - mvn clean package

build-prod:
  extends: .build
  variables:
    BUILD_DIR: "target"
    PROFILES: "prod"
  script:
    - mvn clean package -P$PROFILES

五、实战:完整 Spring Boot 项目流水线


# .gitlab-ci.yml
stages:
  - build
  - test
  - scan
  - package
  - deploy-test
  - deploy-staging
  - deploy-prod

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
  REGISTRY: "registry.mycompany.com"
  IMAGE_NAME: "$REGISTRY/myapp"
  IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"

# === 构建阶段 ===
build:
  stage: build
  image: maven:3.9-eclipse-temurin-17
  cache:
    key:
      files: [pom.xml]
    paths: [.m2/repository]
    policy: pull-push
  script:
    - mvn clean compile -DskipTests
  artifacts:
    paths: [target/]
    expire_in: 1 hour
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

# === 测试阶段(并行)===
unit-test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  needs: [build]
  cache:
    key: { files: [pom.xml] }
    paths: [.m2/repository]
    policy: pull
  script: mvn test
  artifacts:
    reports:
      junit: target/surefire-reports/TEST-*.xml

integration-test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  needs: [build]
  services:
    - name: postgres:16-alpine
      alias: postgres
      variables:
        POSTGRES_PASSWORD: test
  variables:
    SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/test"
  script: mvn verify -Pintegration
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# === 安全扫描 ===
sonar-scan:
  stage: scan
  image: sonarsource/sonar-scanner-cli:latest
  needs: [unit-test]
  script:
    - sonar-scanner
      -Dsonar.projectKey=myapp
      -Dsonar.sources=src
      -Dsonar.host.url=$SONAR_HOST
      -Dsonar.login=$SONAR_TOKEN
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# === 镜像构建 ===
package:
  stage: package
  image: docker:24
  needs: [unit-test]
  services: [docker:24-dind]
  script:
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - echo $REG_PASS | docker login $REGISTRY -u $REG_USER --password-stdin
    - docker push $IMAGE_NAME:$IMAGE_TAG
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# === 部署测试环境 ===
deploy-test:
  stage: deploy-test
  image: bitnami/kubectl:latest
  needs: [package]
  environment:
    name: test
  script:
    - kubectl set image deployment/myapp app=$IMAGE_NAME:$IMAGE_TAG -n test
    - kubectl rollout status deployment/myapp -n test --timeout=180s
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# === 部署预发环境(手动触发)===
deploy-staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  needs: [deploy-test]
  environment:
    name: staging
  when: manual
  script:
    - kubectl set image deployment/myapp app=$IMAGE_NAME:$IMAGE_TAG -n staging
    - kubectl rollout status deployment/myapp -n staging --timeout=180s
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# === 部署生产环境(手动触发 + Tag 限制)===
deploy-prod:
  stage: deploy-prod
  image: bitnami/kubectl:latest
  needs: [deploy-staging]
  environment:
    name: production
  when: manual
  script:
    - kubectl set image deployment/myapp app=$IMAGE_NAME:$IMAGE_TAG -n prod
    - kubectl rollout status deployment/myapp -n prod --timeout=300s
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

六、本篇要点回顾

1. GitLab CI 的核心概念:Pipeline → Stage → Job,Runner 是执行者

2. Runner 推荐 docker executor,用 Docker 部署或 Helm 部署到 K8s

3. .gitlab-ci.yml 用 rules 控制触发条件,needs 控制 Job 依赖

4. cache 复用依赖,artifacts 传递产物,include 复用配置

5. 环境部署用 when: manual 控制人工审批

下一篇预告:《进阶实战:多环境部署与 CI/CD 变量管理》——从基础语法进入工程实践,学习如何管理多环境配置和变量。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐