Files
2026-08-17 15:31:27 +08:00

227 lines
10 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 五阶段流水线,见 09-build-deploy.md。核心原则:
# **Build once, promote across environments with versioned artifacts and gated approvals**
# ——同一个镜像走 UAT 和 Prod,环境差异只在 ConfigMap/Secret/profile 上。
#
# Dev 环境**不在**这条流水线里:内网 k3s,手动跑 scripts/deploy-dev.sh(见 09 文档「环境层级」)。
stages:
- validate
- package
- release
- deploy-uat
- deploy-prod
variables:
# Gradle 缓存进 CI cache,避免每个 job 重新下载全部依赖
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' # MR 触发 CI Validation(不部署)
- if: '$CI_COMMIT_BRANCH == "main"' # main 分支合入触发 CI Validation
- if: '$CI_COMMIT_TAG' # 打 protected tag 触发"选择版本发布"流程
.gradle-cache: &gradle-cache
key: gradle-deps
paths:
- .gradle/caches
- .gradle/wrapper
# ---------------------------------------------------------------------------
# 阶段二:CI Validation(质量门禁)
# ---------------------------------------------------------------------------
# 待接入 ktlint/detekt/jacoco 后启用。
# 本次骨架没有引入这三个插件,job 放开就是必红——所以先注释保留占位,
# 而不是留一个 allow_failure: true 的假绿灯(那种门禁一旦长期黄着,就等于没有)。
# 启用步骤:
# 1. 根 build.gradle 的 subprojects 里加
# org.jlleitschuh.gradle.ktlint、io.gitlab.arturbosch.detekt、jacoco 三个插件;
# 2. jacoco 配 jacocoTestReport + jacocoTestCoverageVerification(阈值先定低再逐步抬);
# 3. 取消下面这个 job 和 unit-integration-test 里 coverage_report 那几行的注释。
#
# lint:
# stage: validate
# script:
# - ./gradlew ktlintCheck detekt --no-daemon
# cache: *gradle-cache
unit-integration-test:
stage: validate
# Testcontainers 需要能起 Docker(见 10-testing.md)。
# Runner 上没有 Docker 的话,这里退化成 ./gradlew test -PexcludeIntegration
# 但那样 @DataJpaTest / WireMock 这批测试就不跑了,等于放掉了最能发现问题的一层。
services:
- docker:dind
script:
- ./gradlew test --no-daemon
artifacts:
when: always # 失败时更需要看到测试报告
reports:
junit: '**/build/test-results/test/TEST-*.xml'
# 待接入 jacoco 后启用:
# coverage_report:
# coverage_format: jacoco
# path: build/reports/jacoco/aggregate/jacocoTestReport.xml
cache: *gradle-cache
build-package:
stage: validate
script:
- ./gradlew :bootstrap:bootJar --no-daemon
artifacts:
paths:
- bootstrap/build/libs/*.jar # 传给 package 阶段的 Dockerfile 直接消费
expire_in: 1 week
cache: *gradle-cache
# 待接入依赖漏洞扫描后启用。
# 需要先在根 build.gradle 加 org.owasp.dependencycheck 插件,
# 并去 https://nvd.nist.gov/developers/request-an-api-key 免费申请 NVD_API_KEY 存成 masked variable
# ——不配 API Key 会卡在 "Updating the NVD CVE data" 几十分钟甚至直接超时失败。
#
# security-scan:
# stage: validate
# script:
# - ./gradlew dependencyCheckAnalyze -Dnvd.api.key=$NVD_API_KEY --no-daemon
# cache:
# key: nvd-db # 缓存漏洞库,避免每次流水线重新拉全量数据
# paths:
# - build/dependency-check-data
# ---------------------------------------------------------------------------
# 阶段三:Artifact & Release Controls(制品与发布控制)
# ---------------------------------------------------------------------------
docker-build-push:
stage: package
needs: [build-package] # 直接消费 validate 阶段的 jar artifact,镜像里不再重新编译
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_TAG'
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA # 推送到 Azure Container RegistryACR
image-scan:
stage: package
needs: [docker-build-push]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_TAG'
script:
# dependencyCheck 只覆盖我们自己声明的依赖,管不到基础镜像里的 OS 包(glibc、openssl 这类),
# 而那恰恰是镜像 CVE 的大头,所以镜像层面要单独扫。
# --ignore-unfixed 是刻意的:上游还没发补丁的 CVE 报出来也无法处理,
# 让它阻断流水线只会训练团队去无脑加白名单,最后所有告警一起失效。
- >-
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# SBOM:记录这个镜像里到底装了什么。将来爆出新 CVE 时,能直接查"我们哪些线上版本受影响",
# 而不是挨个把历史镜像拉下来重新扫一遍。
- syft $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -o cyclonedx-json > sbom.json
artifacts:
paths: [sbom.json]
cut-release:
stage: release
rules:
- if: '$CI_COMMIT_TAG'
script:
# 把已经验证过的 commit-sha 镜像"打标"成不可变的发布版本,而不是重新构建。
# 后续 UAT/Prod 部署的都是这同一个镜像摘要,保证"UAT 验证过的和 Prod 部署的字节级一致"。
- >-
az acr import --name $ACR_NAME
--source $ACR_NAME.azurecr.io/conti-backend:$CI_COMMIT_SHORT_SHA
--image conti-backend:$CI_COMMIT_TAG
# ---------------------------------------------------------------------------
# 阶段四:CD to Azure
#
# AKS 是 Private ClusterAPI Server 只能通过 Private Endpoint 访问),
# GitLab 的公网共享 Runner 连不上——deploy-* 系列 job 必须跑在 VNet 内的 self-hosted Runner 上。
# tags: [azure-vnet-runner] 就是这个约束的落地:公网 Runner 没有这个 tag,天然不会被误调度。
# ---------------------------------------------------------------------------
deploy-uat:
stage: deploy-uat
tags:
- azure-vnet-runner
environment:
name: uat
rules:
- if: '$CI_COMMIT_TAG'
when: manual # Promotion Gate:需要人工点击"Promote to UAT"
script:
- az login --identity # Runner 用 Managed Identity 登录,不存长期有效的 SP 密码
- az aks get-credentials --resource-group $RG --name $AKS_NAME --overwrite-existing
# Key Vault / Config Retrieval:现取值渲染成 K8s Secret
# 密钥只在这个 job 执行期间短暂存在,不打印到日志、不落盘到镜像
- JWT_KEY=$(az keyvault secret show --vault-name conti-backend-kv --name security-jwt-key-v1 --query value -o tsv)
- DB_PASSWORD=$(az keyvault secret show --vault-name conti-backend-kv --name db-password --query value -o tsv)
- F6_API_KEY=$(az keyvault secret show --vault-name conti-backend-kv --name f6-api-key --query value -o tsv)
- >-
kubectl create secret generic conti-backend-secret -n retailapp-uat
--from-literal=SECURITY_JWT_SECRET="$JWT_KEY"
--from-literal=DB_PASSWORD="$DB_PASSWORD"
--from-literal=F6_API_KEY="$F6_API_KEY"
--dry-run=client -o yaml | kubectl apply -f -
- kubectl apply -f k8s/rbac.yaml
- kubectl apply -f k8s/configmap-uat.yaml -n retailapp-uat
- kubectl apply -f k8s/pdb.yaml
# 部署的是 tag 不是 commit-sha:用的是 cut-release 生成的那个不可变发布版本,
# 这就是"同一个制品在环境间晋升"而不是"每个环境各自构建"
- kubectl set image deployment/conti-backend conti-backend=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG -n retailapp-uat
- kubectl rollout status deployment/conti-backend -n retailapp-uat --timeout=180s
- curl -sf https://uat.internal.example.com/actuator/health || exit 1
deploy-prod:
stage: deploy-prod
tags:
- azure-vnet-runner
environment:
name: production
rules:
- if: '$CI_COMMIT_TAG'
when: manual # Promotion Gate:需要更高权限的人工审批
script:
- az login --identity
- az aks get-credentials --resource-group $RG --name $AKS_NAME --overwrite-existing
- JWT_KEY=$(az keyvault secret show --vault-name conti-backend-kv --name security-jwt-key-v1 --query value -o tsv)
- DB_PASSWORD=$(az keyvault secret show --vault-name conti-backend-kv --name db-password-prod --query value -o tsv)
- F6_API_KEY=$(az keyvault secret show --vault-name conti-backend-kv --name f6-api-key-prod --query value -o tsv)
- >-
kubectl create secret generic conti-backend-secret -n retailapp-prod
--from-literal=SECURITY_JWT_SECRET="$JWT_KEY"
--from-literal=DB_PASSWORD="$DB_PASSWORD"
--from-literal=F6_API_KEY="$F6_API_KEY"
--dry-run=client -o yaml | kubectl apply -f -
- kubectl apply -f k8s/rbac.yaml
- kubectl apply -f k8s/configmap-prod.yaml -n retailapp-prod
- kubectl apply -f k8s/pdb.yaml
- kubectl set image deployment/conti-backend conti-backend=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG -n retailapp-prod
- kubectl rollout status deployment/conti-backend -n retailapp-prod --timeout=180s
- curl -sf https://api.example.com/actuator/health || exit 1
rollback-prod:
stage: deploy-prod
tags:
- azure-vnet-runner
environment:
name: production
when: manual # 手动触发,回滚到"上一个已批准的镜像 tag"
script:
- az login --identity
- az aks get-credentials --resource-group $RG --name $AKS_NAME --overwrite-existing
# ROLLBACK_TAG 不是自动推导出来的,而是触发这个 job 时由操作人手工填入的变量。
# 取值来源:GitLab Environment "production" 的部署历史里,当前版本之前的那个 tag。
# 刻意不做成自动取"上一个"——回滚目标必须是人明确确认过的版本,
# 不能出现"上一个版本本身就是有问题的、结果自动回滚到它"这种情况。
- '[ -n "$ROLLBACK_TAG" ] || { echo "必须指定 ROLLBACK_TAG"; exit 1; }'
- kubectl set image deployment/conti-backend conti-backend=$CI_REGISTRY_IMAGE:$ROLLBACK_TAG -n retailapp-prod
- kubectl rollout status deployment/conti-backend -n retailapp-prod --timeout=180s
# 提醒:镜像能秒回滚,**数据库不能**。Flyway 社区版没有 undo
# 破坏性变更必须走 expand-contract 两次发布,否则回滚镜像救不了已经改过的库
# (见 09-build-deploy.md「数据库迁移与回滚的协同」)。