外观
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: manualGitHub 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四、密钥管理
| 平台 | 方式 |
|---|---|
| GitLab | Settings → CI/CD → Variables(勾 Masked + Protected) |
| GitHub | Settings → Secrets and variables → Actions |
| Jenkins | Credentials |
yaml
# GitLab
before_script:
- echo "$REGISTRY_PASS" | docker login registry.internal:5000 -u "$REGISTRY_USER" --password-stdinyaml
# 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 daysyaml
# 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 | 多个分支用同一 tag | tag 里带分支名或 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手动按钮就是故障时的第一反应,比翻文档找命令快。
流水线的验收标准
不是"能构建成功",而是:
- 任意一次提交都能产出唯一 tag 的镜像。
- 部署失败会自动回到上一个可用版本。
- 回滚是一个按钮,不是一段需要现场编写的命令。