227 lines
10 KiB
YAML
227 lines
10 KiB
YAML
# 五阶段流水线,见 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 Registry(ACR)
|
||
|
||
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 Cluster(API 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「数据库迁移与回滚的协同」)。
|