一、把参考流程变成可审计的交付系统
本文把 Git 提交、克隆、构建、单元测试、静态扫描、镜像构建、开发/测试部署、集成测试、制品晋级和生产发布串成一条可执行流水线。参考图中的构建集群、开发测试集群、生产集群、开发测试 Harbor 和生产 Harbor都会保留,但做两项生产化修正:不用特权 Docker-in-Docker,而用 rootless BuildKit;生产阶段不重新构建镜像,而是按 digest 晋级同一个不可变制品。
Git commit -> checkout -> compile/unit/static/security tests
-> rootless BuildKit -> dev Harbor (image@sha256)
-> deploy dev -> smoke -> deploy test -> integration
-> sign + SBOM + approval -> copy SAME digest to prod Harbor
-> pre-release canary -> production rollout -> verify or rollback核心不变量:源码提交、SBOM、签名、测试结果和最终部署的镜像 digest 必须一一对应。任何环境重新 build 都会破坏这个证据链。
二、仓库、集群和权限边界
- build 集群:运行 Jenkins Controller 与短生命周期 Agent,只能推送 dev Harbor。
- dev/test 集群:分别使用独立 Namespace 或独立集群,部署凭据不具备生产权限。
- prod 集群:Jenkins 默认无写权限,发布阶段通过人工审批后使用短时凭据。
- dev Harbor 与 prod Harbor 使用独立 Robot Account;prod 项目启用不可变标签、扫描和签名策略。
kubectl config get-contexts
kubectl --context build-cluster create namespace cicd
kubectl --context devtest-cluster create namespace demo-dev
kubectl --context devtest-cluster create namespace demo-test
kubectl --context prod-cluster create namespace demo-prerelease
kubectl --context prod-cluster create namespace demo-prod
# Jenkins 中保存的凭据只引用 Secret ID,禁止把口令写进 Jenkinsfile
# dev-harbor-robot, prod-harbor-robot, kubeconfig-devtest, kubeconfig-prod, cosign-key三、应用仓库必须提供的交付契约
流水线不应猜测如何构建。仓库至少提供 Dockerfile、测试命令、Kubernetes 基础清单与环境 overlay。探针、资源限制、非 root 用户和只读文件系统是上线契约的一部分。
# syntax=docker/dockerfile:1.7
FROM golang:1.24-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN CGO_ENABLED=0 go test ./... && CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/app ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]apiVersion: apps/v1
kind: Deployment
metadata: {name: demo-api}
spec:
replicas: 3
strategy:
rollingUpdate: {maxSurge: 1, maxUnavailable: 0}
selector: {matchLabels: {app: demo-api}}
template:
metadata: {labels: {app: demo-api}}
spec:
securityContext: {runAsNonRoot: true, seccompProfile: {type: RuntimeDefault}}
containers:
- name: app
image: IMAGE_PLACEHOLDER
ports: [{containerPort: 8080}]
readinessProbe: {httpGet: {path: /ready, port: 8080}, periodSeconds: 5}
livenessProbe: {httpGet: {path: /health, port: 8080}, periodSeconds: 10}
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 500m, memory: 512Mi}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}四、Jenkins Kubernetes Agent:每次构建都是临时 Pod
Jenkins Kubernetes 插件按任务创建 Agent Pod。容器分别承担 Git/测试、BuildKit、kubectl、扫描和签名,不共享宿主 Docker Socket。缓存放到受控 PVC 或远端缓存,任务结束后销毁 Pod。
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins-build
securityContext: {runAsNonRoot: true, fsGroup: 1000}
containers:
- name: test
image: golang:1.24-alpine
command: [sleep]
args: [99d]
- name: buildkit
image: moby/buildkit:v0.20.2-rootless
args: [--oci-worker-no-process-sandbox]
securityContext: {runAsUser: 1000, runAsGroup: 1000}
- name: tools
image: registry.example.com/ci/delivery-tools:2026.09@sha256:REPLACE_WITH_APPROVED_DIGEST
command: [sleep]
args: [99d]
- name: scanner
image: aquasec/trivy:0.61.0
command: [sleep]
args: [99d]五、完整 Jenkinsfile:CI、环境验证与制品晋级
下面的流水线用提交 SHA 作为标签,但所有部署都转换为 digest。示例命令中的域名、凭据 ID、Git 地址和策略阈值需要替换为你的环境值。
pipeline {
agent { kubernetes { yamlFile 'ci/jenkins-agent.yaml'; defaultContainer 'test' } }
options { timestamps(); disableConcurrentBuilds(); timeout(time: 45, unit: 'MINUTES') }
environment {
DEV_IMAGE = 'dev-harbor.example.com/demo/api'
PROD_IMAGE = 'prod-harbor.example.com/demo/api'
}
stages {
stage('Checkout') {
steps { checkout scm; sh 'git diff --exit-code --check'; script { env.SHORT_SHA = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim() } }
}
stage('Unit and Static Tests') {
parallel {
stage('Unit') { steps { sh 'go test -race -coverprofile=coverage.out ./...' } }
stage('Static') { steps { sh 'go vet ./...' } }
stage('Secret Scan') { steps { container('scanner') { sh 'trivy fs --scanners secret --exit-code 1 .' } } }
}
}
stage('Build Immutable Image') {
steps {
container('buildkit') {
withCredentials([usernamePassword(credentialsId: 'dev-harbor-robot', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS')]) {
sh '''mkdir -p ~/.docker
printf '{"auths":{"dev-harbor.example.com":{"username":"%s","password":"%s"}}}' "$REG_USER" "$REG_PASS" > ~/.docker/config.json
buildctl-daemonless.sh build --frontend dockerfile.v0 --local context=. --local dockerfile=. \
--output type=image,name=$DEV_IMAGE:$SHORT_SHA,push=true \
--metadata-file image-metadata.json
jq -r '."containerimage.digest"' image-metadata.json > image.digest'''
}
}
}
}
stage('SBOM and Image Scan') {
steps { container('scanner') { withCredentials([usernamePassword(credentialsId: 'dev-harbor-robot', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS')]) { sh 'trivy registry login --username "$REG_USER" --password "$REG_PASS" dev-harbor.example.com'; sh 'trivy image --format cyclonedx --output sbom.cdx.json $DEV_IMAGE@$(cat image.digest)'; sh 'trivy image --severity CRITICAL,HIGH --exit-code 1 $DEV_IMAGE@$(cat image.digest)' } } }
}
stage('Deploy Dev') {
steps { container('tools') { withCredentials([file(credentialsId: 'kubeconfig-devtest', variable: 'KUBECONFIG')]) { sh 'ci/deploy.sh demo-dev $DEV_IMAGE@$(cat image.digest)'; sh 'ci/smoke.sh https://api-dev.example.com' } } }
}
stage('Deploy Test and Integration') {
steps { container('tools') { withCredentials([file(credentialsId: 'kubeconfig-devtest', variable: 'KUBECONFIG')]) { sh 'ci/deploy.sh demo-test $DEV_IMAGE@$(cat image.digest)'; sh 'ci/integration.sh https://api-test.example.com' } } }
}
stage('Sign Dev Artifact') {
steps { container('tools') { withCredentials([file(credentialsId: 'cosign-key', variable: 'COSIGN_KEY'), string(credentialsId: 'cosign-password', variable: 'COSIGN_PASSWORD')]) { sh 'cosign sign --yes --key $COSIGN_KEY $DEV_IMAGE@$(cat image.digest)'; sh 'cosign attest --yes --key $COSIGN_KEY --predicate sbom.cdx.json --type cyclonedx $DEV_IMAGE@$(cat image.digest)' } } }
}
stage('Production Approval') {
input { message 'Promote the tested digest to production?'; ok 'Promote' }
steps { echo "Approved ${DEV_IMAGE}@${readFile('image.digest').trim()}" }
}
stage('Promote Same Digest') {
steps { container('tools') { withCredentials([usernamePassword(credentialsId: 'prod-harbor-robot', usernameVariable: 'PROD_USER', passwordVariable: 'PROD_PASS')]) { sh 'skopeo copy --all --dest-creds "$PROD_USER:$PROD_PASS" docker://$DEV_IMAGE@$(cat image.digest) docker://$PROD_IMAGE:$SHORT_SHA' } } }
}
stage('Pre-release and Production') {
steps { container('tools') { withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) { sh 'ci/deploy.sh demo-prerelease $PROD_IMAGE@$(cat image.digest)'; sh 'ci/smoke.sh https://api-pre.example.com'; sh 'ci/deploy.sh demo-prod $PROD_IMAGE@$(cat image.digest)'; sh 'kubectl -n demo-prod rollout status deploy/demo-api --timeout=5m' } } }
}
}
post {
always { archiveArtifacts artifacts: 'coverage.out,image.digest,image-metadata.json,sbom.cdx.json', fingerprint: true; junit 'reports/*.xml' }
failure { sh 'ci/notify.sh failed "$BUILD_URL" || true' }
}
}delivery-tools 是团队维护并按 digest 固定的内部 CI 镜像,至少包含 kubectl、kustomize、cosign、skopeo、crane、jq 和 curl;先在隔离仓库构建、扫描和签名,再替换示例地址与 digest。
六、部署脚本必须按 digest 工作
#!/usr/bin/env sh
set -eu
namespace="$1"
image="$2"
case "$image" in *@sha256:*) ;; *) echo 'image must be pinned by digest' >&2; exit 2;; esac
kubectl -n "$namespace" set image deployment/demo-api app="$image"
kubectl -n "$namespace" annotate deployment/demo-api \
delivery.fleyun.example/image="$image" \
delivery.fleyun.example/build-url="${BUILD_URL:-manual}" --overwrite
kubectl -n "$namespace" rollout status deployment/demo-api --timeout=300s
kubectl -n "$namespace" get pods -l app=demo-api -o wide在生产晋级后,用 skopeo inspect --raw 或 crane digest 校验 prod Harbor 中的 manifest digest 与测试通过的 digest 一致。多架构镜像必须复制整个 manifest list,不能只复制当前架构。
七、集成测试、预发布与生产门禁
- Smoke:/health、/ready、版本端点与关键依赖连通性。
- Integration:真实协议、数据库迁移兼容、消息幂等、权限边界与超时重试。
- Pre-release:使用生产配置但隔离流量,验证镜像拉取、Secret、策略与探针。
- Production:先 5% canary,观察错误率、P95/P99、饱和度和业务指标,再逐步放量。
- 人工审批看到的必须是 commit、digest、扫描结果、SBOM、测试报告和变更窗口。
八、失败回滚与数据库迁移
# 查看历史并回滚工作负载
kubectl -n demo-prod rollout history deployment/demo-api
kubectl -n demo-prod rollout undo deployment/demo-api --to-revision=7
kubectl -n demo-prod rollout status deployment/demo-api --timeout=5m
# 确认回滚后实际运行的 digest
kubectl -n demo-prod get pods -l app=demo-api \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].imageID}{"\n"}{end}'数据库使用 expand/contract:先增加向后兼容字段并双写,应用全部升级后再迁移数据,最后单独删除旧字段。不要在应用启动时执行不可逆 DDL;否则镜像回滚成功也无法恢复数据契约。
九、Harbor 复制、签名与供应链加固
Harbor 可配置从开发实例向生产实例的 push replication,并连同 Cosign 签名复制。无论使用复制规则还是 skopeo,都要以 digest 为晋级依据;标签只是便于人阅读。生产项目开启不可变标签和漏洞阈值,Admission Policy 在集群侧再次校验签名与允许的 registry。
DIGEST="$(cat image.digest)"
cosign verify --key cosign.pub "dev-harbor.example.com/demo/api@$DIGEST"
crane digest "prod-harbor.example.com/demo/api:$SHORT_SHA"
# 两个输出必须等于同一个 sha256 digest十、验收清单与总结
- 故意让单元测试失败,确认不会构建或推送镜像。
- 故意引入高危镜像漏洞,确认门禁阻止部署。
- 记录 dev/test 通过的 digest,确认生产仓库和运行中 Pod 都是同一 digest。
- 拒绝审批,确认生产凭据不会被使用。
- 制造 canary 错误率升高,确认自动或人工回滚能恢复上一 digest。
- 从制品反查 commit、SBOM、签名、测试报告和审批人,证据链完整。
一条高质量 CI/CD 流水线不是“脚本越长越好”,而是每个阶段都有清晰输入、输出和失败语义。以不可变 digest 为主线,把构建、测试、扫描、签名、晋级、发布和回滚串成可验证闭环,才能把参考流程真正落地到多集群生产环境。