Skip to content

CI 流水线:构建、测试、推镜像 ​

流水线的目标不是"跑起来很酷",而是每次提交都能产出一个确定版本的镜像,并且失败时不留半成品。

一、最小可用流水线 ​

三个阶段:测试 → 构建镜像 → 推送。

GitLab CI ​

yaml
# .gitlab-ci.yml
stages: [test, build, deploy]

variables:
  IMAGE: registry.internal:5000/shop/order-api
  TAG: $CI_COMMIT_SHORT_SHA
  DOCKER_TLS_CERTDIR: "/certs"

test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn -B -q test
  cache:
    key: maven-$CI_COMMIT_REF_SLUG
    paths:
      - .m2/repository
  variables:
    MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

build:
  stage: build
  image: docker:27-cli
  services:
    - docker:27-dind
  before_script:
    - echo "$REGISTRY_PASS" | docker login registry.internal:5000 -u "$REGISTRY_USER" --password-stdin
  script:
    - docker buildx build
        --cache-from type=registry,ref=$IMAGE:buildcache
        --cache-to type=registry,ref=$IMAGE:buildcache,mode=max
        -t $IMAGE:$TAG -t $IMAGE:latest --push .
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:
  stage: deploy
  image: alpine/helm:3.16.2
  script:
    - helm upgrade --install order-api ./chart
        -n prod -f chart/values.yaml -f chart/values-prod.yaml
        --set image.tag=$TAG --atomic --wait --timeout 5m
  environment:
    name: prod
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual

GitHub Actions ​

yaml
# .github/workflows/build.yml
name: build

on:
  push:
    branches: [main]
  pull_request:

env:
  IMAGE: ghcr.io/${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: temurin
          cache: maven
      - run: mvn -B test

  build:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ env.IMAGE }}:${{ github.sha }}
            ${{ env.IMAGE }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

二、镜像 tag 怎么定 ​

方式可追溯可读建议
latest否一般禁止用于生产
$CI_COMMIT_SHORT_SHA是一般首选
语义版本 1.4.2是好打 tag 时用
时间戳是一般临时环境
分支名否(会覆盖)好只用于 dev

推荐同时打两个 tag:

bash
docker build -t $IMAGE:$SHA -t $IMAGE:latest .
docker push $IMAGE:$SHA
docker push $IMAGE:latest

生产部署只用 $SHA,latest 只方便本地拉取。

三、构建缓存 ​

没有缓存时,每次 CI 都要重新下载依赖,构建时间翻倍。

bash
# BuildKit 的 registry 缓存(跨 runner 复用)
docker buildx build \
  --cache-from type=registry,ref=$IMAGE:buildcache \
  --cache-to type=registry,ref=$IMAGE:buildcache,mode=max \
  -t $IMAGE:$TAG --push .

mode=max 缓存所有层(包括中间层),mode=min 只缓存最终镜像用到的层。CI 里用 max。

Maven 也可以用 cache mount(需要 Dockerfile 支持):

dockerfile
RUN --mount=type=cache,target=/root/.m2 \
    mvn -B -q clean package -DskipTests

四、密钥管理 ​

平台方式
GitLabSettings → CI/CD → Variables(勾 Masked + Protected)
GitHubSettings → Secrets and variables → Actions
JenkinsCredentials
yaml
# GitLab
before_script:
  - echo "$REGISTRY_PASS" | docker login registry.internal:5000 -u "$REGISTRY_USER" --password-stdin
yaml
# GitHub
- run: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GHCR_TOKEN }}

不要把密码写在 .gitlab-ci.yml / workflow 里

即使是私有仓库,流水线文件也会被所有有读权限的人看到。而且一旦仓库泄露,密码就泄露。全部走平台的 Secrets 机制。

--password-stdin 也要用起来:docker login -p xxx 会把密码留在进程列表和 shell history 里。

五、失败处理 ​

yaml
# GitLab: 失败时保留构建产物便于排查
build:
  artifacts:
    when: on_failure
    paths:
      - target/surefire-reports/
    expire_in: 3 days
yaml
# GitHub
- name: Upload reports
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: test-reports
    path: target/surefire-reports/

--atomic --wait 让部署失败自动回滚,比"失败了再手动救"可靠得多。

六、一个完整的 GitLab 示例(带环境) ​

yaml
stages: [test, build, deploy-staging, deploy-prod]

default:
  retry:
    max: 1
    when: [runner_system_failure, stuck_or_timeout_failure]

.deploy_template: &deploy
  image: alpine/helm:3.16.2
  script:
    - helm upgrade --install "$APP" ./chart
        -n "$NS" -f chart/values.yaml -f "chart/values-$NS.yaml"
        --set image.tag=$TAG --atomic --wait --timeout 5m
    - kubectl -n "$NS" rollout status deploy/"$APP" --timeout=180s

deploy-staging:
  <<: *deploy
  stage: deploy-staging
  variables:
    APP: order-api
    NS: staging
    TAG: $CI_COMMIT_SHORT_SHA
  environment:
    name: staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy-prod:
  <<: *deploy
  stage: deploy-prod
  variables:
    APP: order-api
    NS: prod
    TAG: $CI_COMMIT_SHORT_SHA
  environment:
    name: prod
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

七、踩过的坑 ​

现象根因处置
每次构建都重新下载依赖没配缓存registry cache / cache mount
镜像 tag 全是 latest脚本写死用 commit sha
部署成功但服务没起来只 kubectl apply 没等就绪--wait / rollout status
流水线里 docker 命令找不到runner 没装 docker用 docker:dind service
dind 报 TLS 错误没设 DOCKER_TLS_CERTDIR设成 "/certs" 或空字符串
Secret 在日志里被打印用了 echo $PASS平台 Masked,或别 echo
缓存越来越大缓存 key 没带分支/依赖版本用 lockfile 的 hash 做 key
并行任务互相覆盖镜像 tag多个分支用同一 tagtag 里带分支名或 sha

八、把"能回滚"写进流水线 ​

yaml
rollback-prod:
  stage: deploy-prod
  image: alpine/helm:3.16.2
  script:
    - helm rollback order-api -n prod
    - kubectl -n prod rollout status deploy/order-api --timeout=180s
  when: manual
  environment:
    name: prod
    action: stop

手动按钮就是故障时的第一反应,比翻文档找命令快。

流水线的验收标准

不是"能构建成功",而是:

  1. 任意一次提交都能产出唯一 tag 的镜像。
  2. 部署失败会自动回到上一个可用版本。
  3. 回滚是一个按钮,不是一段需要现场编写的命令。