backend scaffold
This commit is contained in:
+226
@@ -0,0 +1,226 @@
|
||||
# 五阶段流水线,见 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「数据库迁移与回滚的协同」)。
|
||||
Reference in New Issue
Block a user