CI/CD 集成实战
CI/CD 集成实战
写在前面
前面的文章把镜像怎么写、怎么编排、怎么存都讲完了,但生产环境真正的工作流是:代码 push → 自动测试 → 自动构建镜像 → 推送到 Harbor → 自动部署到测试/生产环境。这就是 CI/CD。
本篇会用三个主流 CI 平台(GitHub Actions / GitLab CI / Jenkins)各跑一个完整流水线,讲清楚关键节点和性能优化。
一、CI/CD 的关键节点
任何 Docker 化的 CI/CD 都包含这几个核心步骤:
[Push Code]
│
▼
[Checkout] → 拉代码
│
▼
[Test] → 跑单测、lint、类型检查
│
▼
[Build Image] → 构建 Docker 镜像
│
▼
[Push Image] → 推送到 Harbor
│
▼
[Deploy] → 触发部署
│
▼
[Notify] → 通知开发者
每一步都要:
- 快速反馈(失败立即停止,不要跑完才发现编译不过)
- 可缓存(没变的层不要重建)
- 可追踪(每个镜像能反查到哪个 commit / PR / 哪个测试环境)
二、镜像标签策略
在开始写 CI 之前,先确定怎么给镜像打 tag。
| Tag 来源 | 适用 |
|---|---|
| Git 分支名 | 开发环境,如 develop feature/foo |
| Git tag | 生产版本,如 v1.2.3 |
| PR/MR 编号 | 预览环境,如 pr-123 |
| Commit SHA(短) | 精确追溯,如 a1b2c3d |
| 时间戳 | 备份/归档 |
推荐策略:多 tag 同时打:
# 比如 commit a1b2c3d 在 main 分支,PR #123
docker tag myapp:a1b2c3d myapp:main
docker tag myapp:a1b2c3d myapp:pr-123
docker tag myapp:a1b2c3d myapp:a1b2c3d # SHA 永远保留
三、GitHub Actions 完整流水线
3.1 项目结构
.
├── .github/
│ └── workflows/
│ ├── ci.yml # 主流程
│ └── release.yml # 发布流程
├── Dockerfile
├── docker-compose.yml
└── src/
3.2 基础流水线
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: harbor.company.com
IMAGE_NAME: myproject/myapp
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
build:
name: Build & Push
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC
steps:
- uses: actions/checkout@v4
# Docker Buildx
- uses: docker/setup-buildx-action@v3
# 登录 Harbor (用用户名密码)
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.HARBOR_USER }}
password: ${{ secrets.HARBOR_PASSWORD }}
# 镜像元数据(自动生成 tag)
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=git-
type=raw,value=latest,enable={{is_default_branch}}
# 构建并推送(支持缓存)
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
3.3 缓存优化:从 8 分钟到 90 秒
第一次跑:8 分钟(因为没有缓存,完全 cold start)。
开启 cache-from 和 cache-to 后:
- 第二次跑(package.json 没变):1 分 40 秒
- 改一行代码:90 秒(绝大部分层命中缓存)
关键是:缓存层单独打 tag(buildcache),跟应用镜像分开放。
3.4 多架构镜像(ARM + AMD64)
公司有 ARM 服务器(Mac mini M2 集群)+ AMD 服务器,需要多架构镜像:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Build multi-arch
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
setup-qemu 会启动一个 QEMU 模拟器,慢(ARM64 镜像在 AMD runner 上跑要 2-3 倍时间)。生产中我们后来换了 ARM runner(GitHub 还没出,自建或者用 Cirrus),速度大幅提升。
3.5 触发部署
测试 + 构建完成后,自动部署到测试环境:
deploy:
name: Deploy to Staging
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Trigger Argo CD sync
run: |
curl -X POST "https://argocd.company.com/api/v1/applications/myapp-staging/sync" \
-H "Authorization: Bearer ${{ secrets.ARGOCD_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"revision":"HEAD"}'
四、GitLab CI 完整流水线
4.1 .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
REGISTRY: harbor.company.com
IMAGE_NAME: myproject/myapp
DOCKER_TLS_CERTDIR: "" # 关闭 TLS,内网用
# 缓存
.cache_template: &cache_template
key:
files:
- package-lock.json
paths:
- node_modules/
# ===== 测试 =====
test:
stage: test
image: node:20-alpine
cache:
- <<: *cache_template
script:
- npm ci --no-audit --no-fund
- npm run lint
- npm test
coverage: '/Statements\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
# ===== 构建 =====
build:
stage: build
image: docker:24
services:
- docker:24-dind
needs: [test]
before_script:
- echo "$HARBOR_PASSWORD" | docker login $REGISTRY -u "$HARBOR_USER" --password-stdin
script:
- |
TAGS=""
TAGS="$TAGS --tag $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA"
TAGS="$TAGS --tag $REGISTRY/$IMAGE_NAME:$CI_COMMIT_REF_NAME"
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
TAGS="$TAGS --tag $REGISTRY/$IMAGE_NAME:latest"
fi
if [ -n "$CI_COMMIT_TAG" ]; then
TAGS="$TAGS --tag $REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG"
fi
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache \
--cache-to type=registry,ref=$REGISTRY/$IMAGE_NAME:buildcache,mode=max \
$TAGS \
--push \
.
only:
- branches
- tags
- merge_requests
# ===== 部署测试 =====
deploy:staging:
stage: deploy
image: alpine:3.19
needs: [build]
before_script:
- apk add --no-cache curl
script:
- |
curl -X POST "https://argocd.company.com/api/v1/applications/myapp-staging/sync" \
-H "Authorization: Bearer $ARGOCD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"revision":"HEAD"}'
environment:
name: staging
url: https://staging.myapp.com
only:
- develop
# ===== 部署生产 =====
deploy:production:
stage: deploy
image: alpine:3.19
needs: [build]
before_script:
- apk add --no-cache curl
script:
- |
curl -X POST "https://argocd.company.com/api/v1/applications/myapp-production/sync" \
-H "Authorization: Bearer $ARGOCD_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"revision\":\"$CI_COMMIT_TAG\"}"
environment:
name: production
url: https://myapp.com
when: manual
only:
- tags
4.2 GitLab CI 的几个特殊点
缓存配置比 GitHub 灵活:
cache:
key: # 不同分支不共享缓存
files:
- package-lock.json
paths:
- node_modules/
policy: pull-push # 默认,pull 旧缓存,push 新缓存
services 启动 DinD:
image: docker:24
services:
- docker:24-dind # 启动一个 docker-in-docker 容器
GitLab Runner 默认开启了 DinD 支持,直接用即可。
when: manual 控制生产部署——必须人工点确认才能部署生产。
五、Jenkins 完整流水线
5.1 Jenkinsfile(Declarative Pipeline)
pipeline {
agent any
environment {
REGISTRY = 'harbor.company.com'
IMAGE_NAME = 'myproject/myapp'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Test') {
agent {
docker {
image 'node:20-alpine'
reuseNode true
}
}
steps {
sh 'npm ci --no-audit --no-fund'
sh 'npm run lint'
sh 'npm test'
}
post {
always {
junit '**/junit-*.xml'
}
}
}
stage('Build') {
steps {
script {
// 计算 tag
def gitCommit = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
def gitBranch = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim()
def tags = [
"${env.REGISTRY}/${env.IMAGE_NAME}:${gitCommit}",
"${env.REGISTRY}/${env.IMAGE_NAME}:${gitBranch}"
]
if (gitBranch == 'main') {
tags.add("${env.REGISTRY}/${env.IMAGE_NAME}:latest")
}
// 登录
sh "echo \${HARBOR_PASSWORD} | docker login ${env.REGISTRY} -u \${HARBOR_USER} --password-stdin"
// 构建并推送
sh """
docker buildx create --use --name mybuilder || docker buildx use mybuilder
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=${env.REGISTRY}/${env.IMAGE_NAME}:buildcache \
--cache-to type=registry,ref=${env.REGISTRY}/${env.IMAGE_NAME}:buildcache,mode=max \
${tags.collect { "--tag " + it }.join(' ')} \
--push \
.
"""
env.GIT_COMMIT_SHORT = gitCommit
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
sh """
curl -X POST "https://argocd.company.com/api/v1/applications/myapp-staging/sync" \
-H "Authorization: Bearer ${env.ARGOCD_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"revision":"HEAD"}'
"""
}
}
stage('Deploy to Production') {
when {
buildingTag()
}
steps {
input 'Deploy to production?'
sh """
curl -X POST "https://argocd.company.com/api/v1/applications/myapp-production/sync" \
-H "Authorization: Bearer ${env.ARGOCD_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"revision":"${env.GIT_COMMIT_SHORT}"}'
"""
}
}
}
post {
success {
// 通知 Slack
slackSend(
channel: '#deployments',
color: 'good',
message: "✅ ${env.JOB_NAME} #${env.BUILD_NUMBER} succeeded"
)
}
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: "❌ ${env.JOB_NAME} #${env.BUILD_NUMBER} failed"
)
}
}
}
5.2 Jenkins 的几个特殊点
agent { docker { ... } } 跑测试——每个 stage 用不同容器,隔离性强。
input 'Deploy to production?'——Jenkins 原生的"人工确认"步骤,生产部署前暂停等点确认。
buildingTag()——只在 git tag 触发时才跑生产部署。
六、缓存优化的几个进阶技巧
6.1 本地缓存
GitHub Actions 的 runner 每次都是新的 VM,没有本地磁盘持久化。但 BuildKit 自带的本地缓存能跨 step 共享(同一 job 内):
- name: Build
uses: docker/build-push-action@v5
with:
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
6.2 远程缓存(Registry)
已经在前面用过,生产强烈推荐——多个 CI job 之间共享缓存。
6.3 依赖预构建
问题:每次 CI 都 npm ci,即使 package.json 没变也要下载。
解决:预构建一个 base 镜像,把所有 dependencies 装好:
# Dockerfile.base
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
# 每周跑一次,更新 base 镜像
- name: Build base
run: |
docker buildx build \
-f Dockerfile.base \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base:latest \
--push .
应用 Dockerfile:
FROM harbor.company.com/myproject/myapp/base:latest AS deps
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
我们用这个方法,首次构建时间从 8 分钟降到 3 分钟(因为 deps 已经预装好)。
6.4 跳过某些文件的缓存影响
Dockerfile 改了不该影响缓存的层?用 --cache-from + BuildKit 的 inline cache:
- uses: docker/build-push-action@v5
with:
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
main 是上一次构建的完整镜像,作为 fallback cache。
七、安全:CI 镜像安全扫描
7.1 在 CI 里集成 Trivy
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # 有漏洞就失败
ignore-unfixed: true
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
把扫描结果上传到 GitHub Security tab,可以在 PR 里直接看到漏洞。
7.2 SBOM(软件物料清单)
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
format: spdx-json
artifact-name: myapp.spdx.json
SBOM 是合规要求——很多行业(金融、医疗)必须提供。
八、我们最终的完整 DevOps 闭环
Developer Push Code
│
▼
GitHub Actions (CI)
├─ Test (lint + unit + integration)
├─ Build & Push to Harbor
└─ Trivy Scan
│
▼
Harbor (镜像仓库)
│
▼
Argo CD (CD)
├─ 监听 Harbor 新镜像
├─ 自动同步到 K8s
└─ 失败自动回滚
│
▼
Production
这个闭环的好处:
- 完全 GitOps——所有变更通过 PR 走
- 自动回滚——健康检查失败 Argo CD 自动回滚到上一个稳定版本
- 可审计——每个环境里跑什么版本都能查到对应 commit
下一篇讲 容器监控与日志,用 Prometheus + Grafana + Loki 搭一套完整的可观测性体系。