2026-08-12 18:23:11 +08:00
|
|
|
|
# 09. 构建与多环境部署
|
|
|
|
|
|
|
|
|
|
|
|
## 决策
|
|
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
Gradle 多模块统一构建,`bootstrap` 产出单一 jar/Docker 镜像;三个环境里 UAT/Prod 部署到 Azure AKS(见 [Architecture-Diagram/gitlab-cicd-azure-deployment-diagram.drawio](../Architecture-Diagram/gitlab-cicd-azure-deployment-diagram.drawio)),Dev 部署到公司内网一台 Ubuntu 服务器上的 k3s 集群(团队通过公司 VPN 访问,手动执行部署脚本,不接入 CI/CD 自动触发),个人日常调试用的是更轻量的 `local` profile(不经过任何 K8s,见下文区分)。
|
2026-08-12 18:23:11 +08:00
|
|
|
|
|
|
|
|
|
|
## 结构约定
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
settings.gradle # include 所有 platform-* / domains/*(见 01-project-structure.md)
|
|
|
|
|
|
build.gradle # 根工程统一 Kotlin/Spring Boot 插件版本、依赖约束
|
|
|
|
|
|
bootstrap/build.gradle # bootJar,构建出可执行 jar
|
|
|
|
|
|
Dockerfile # 基于 bootstrap 的 jar 打镜像
|
|
|
|
|
|
.gitlab-ci.yml # build -> test -> docker build/push -> deploy
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Dockerfile 示例(多阶段构建)
|
|
|
|
|
|
|
|
|
|
|
|
```dockerfile
|
|
|
|
|
|
# Dockerfile
|
|
|
|
|
|
FROM eclipse-temurin:21-jdk AS build
|
|
|
|
|
|
WORKDIR /workspace
|
|
|
|
|
|
COPY . .
|
|
|
|
|
|
RUN ./gradlew :bootstrap:bootJar --no-daemon
|
|
|
|
|
|
|
|
|
|
|
|
FROM eclipse-temurin:21-jre
|
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
|
COPY --from=build /workspace/bootstrap/build/libs/*.jar app.jar
|
|
|
|
|
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
多阶段构建的好处:最终镜像只包含 JRE + 一个 jar,不带 Gradle 缓存、源码、编译工具链,镜像体积和攻击面都更小。
|
|
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
## 环境层级:local(个人本机)vs Dev(内网 Ubuntu k3s 集群)vs UAT/Prod(Azure AKS)
|
|
|
|
|
|
|
|
|
|
|
|
三层环境的定位不一样,容易混淆,先说清楚区别:
|
|
|
|
|
|
|
|
|
|
|
|
| 环境 | 跑在哪 | 是否过 K8s | 是否走 CI/CD | 访问方式 |
|
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
|
| **local** | 开发者自己电脑 | 否,`local` profile 直接关掉 Spring Cloud Kubernetes(见 [07-config-governance.md](./07-config-governance.md#本地开发怎么跑不需要真的连-k8s)) | 否 | 只有自己,localhost |
|
|
|
|
|
|
| **Dev** | 公司内网一台 Ubuntu 服务器,跑 [k3s](https://k3s.io/)(轻量级单节点 K8s 发行版) | 是,真实 K8s 集群 | 否,手动执行部署脚本(见下文) | 团队通过公司 **VPN** 访问(连上 VPN 后即可直接访问这台机器的内网地址) |
|
|
|
|
|
|
| **UAT / Prod** | Azure **Private AKS** | 是 | 是 | 见后面阶段四/五 |
|
|
|
|
|
|
|
|
|
|
|
|
`local` 是纯个人编码调试用的,跑得最快、依赖最少;`Dev` 是团队共享的、真实跑在 K8s 里的验证环境,行为上(ConfigMap 热更新、Secret 挂载方式、Deployment 滚动更新)跟 UAT/Prod 是一致的,只是物理上跑在公司内网的一台 Ubuntu 服务器而不是 Azure——这也是为什么选 k3s 这样一个真实的、哪怕是单节点的 K8s 发行版,而不是简单用 `docker compose` 起一堆容器:**能验证真实 K8s 行为,而不只是"能不能跑起来"**。这台机器本身就是 Ubuntu(跟 AKS 节点同为 Linux),k3s 直接跑在宿主机上,没有额外的虚拟化层。
|
|
|
|
|
|
|
|
|
|
|
|
### Dev 环境怎么部署(手动脚本,不接入 CI/CD 自动触发)
|
|
|
|
|
|
|
|
|
|
|
|
Dev 不需要跟 UAT/Prod 一样接自动化流水线,谁想更新 Dev 环境,连上公司 VPN,本机配置好指向这台机器的 `KUBECONFIG`,手动跑一下部署脚本就行:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# scripts/deploy-dev.sh
|
|
|
|
|
|
# 用法:./scripts/deploy-dev.sh <commit-sha 或 release tag>
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
IMAGE_TAG=${1:?"必须传一个镜像 tag,比如某次 main 分支的 commit-sha"}
|
|
|
|
|
|
|
|
|
|
|
|
kubectl create secret generic conti-backend-secret -n retailapp-dev \
|
|
|
|
|
|
--from-literal=SECURITY_JWT_SECRET="dev-only-fake-secret" \
|
|
|
|
|
|
--from-literal=DB_PASSWORD="dev-only-fake-password" \
|
|
|
|
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
|
|
kubectl apply -f k8s/configmap-dev.yaml -n retailapp-dev
|
|
|
|
|
|
kubectl set image deployment/conti-backend conti-backend=$CI_REGISTRY_IMAGE:$IMAGE_TAG -n retailapp-dev
|
|
|
|
|
|
kubectl rollout status deployment/conti-backend -n retailapp-dev --timeout=180s
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
几个和 UAT/Prod 不一样的地方:
|
|
|
|
|
|
|
|
|
|
|
|
- **不需要 Runner,也不需要接进 `.gitlab-ci.yml`**:镜像已经由阶段三的 `docker-build-push` job(合入 `main` 时自动触发)推到 ACR 了,Dev 这一步只是"把已经存在的镜像 apply 到这台机器",谁需要验证最新代码,自己连 VPN 跑一下脚本,不需要为此单独维护一条自动化流水线。
|
|
|
|
|
|
- **不用 Key Vault**:这台机器到不了 Azure Key Vault 的 Private Endpoint,Dev 环境的 Secret 就是写死的假值(跟 `local` profile 里的假密码同一个思路),不是真实密钥,本来 Dev 环境也不该碰生产密钥。
|
|
|
|
|
|
- **镜像可以是任意 commit-sha**:想验证哪次提交,脚本参数传哪个 tag,不需要等到打 release tag,因为 Dev 不参与"Build once, promote across environments"这条只针对 UAT/Prod 的发布晋升链路。
|
|
|
|
|
|
- 需要提前把 `KUBECONFIG`(k3s 默认生成在 `/etc/rancher/k3s/k3s.yaml`,把里面 `https://127.0.0.1:6443` 换成机器的内网 IP)分发给需要部署/排查 Dev 环境的团队成员,连上 VPN 后即可直接用。
|
|
|
|
|
|
|
|
|
|
|
|
## CI/CD 到 Kubernetes 的完整流程
|
|
|
|
|
|
|
|
|
|
|
|
整体沿用 [Architecture-Diagram/gitlab-cicd-azure-deployment-diagram.drawio](../Architecture-Diagram/gitlab-cicd-azure-deployment-diagram.drawio) 里已经确认的流水线阶段,核心原则是 **"Build once, promote across environments with versioned artifacts and gated approvals"**——这条原则针对的是 UAT/Prod 之间的晋升;Dev 不在这条流水线里(见上一节,手动脚本部署)。下面按阶段展开 UAT/Prod 这条主链路,并结合我们 [部署架构](../Architecture-Diagram/deployment-architecture-diagram.drawio) 是 **Private AKS**(只能通过 Private Endpoint 访问)这个关键约束说明每一步具体怎么落地。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段一:Source & Triggers(触发)
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
# .gitlab-ci.yml(节选)
|
|
|
|
|
|
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 触发"选择版本发布"流程
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 日常开发:Developer 提 Merge Request → 触发 **CI Validation**(下一阶段),只做质量门禁,不产出可发布制品。
|
|
|
|
|
|
- 发布:在 `main` 分支上打一个 **protected tag**(如 `v1.4.0`),触发"选择要部署的版本"这条链路——这是唯一能进入 Artifact & Release Controls 之后阶段的入口,普通分支/MR 流水线到 CI Validation 就结束,避免任何未评审代码意外流入生产。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段二:CI Validation(质量门禁)
|
2026-08-12 18:23:11 +08:00
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
stages:
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- validate
|
2026-08-12 18:23:11 +08:00
|
|
|
|
- package
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- release
|
|
|
|
|
|
- deploy-uat
|
|
|
|
|
|
- deploy-prod
|
2026-08-12 18:23:11 +08:00
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
lint:
|
|
|
|
|
|
stage: validate
|
2026-08-12 18:23:11 +08:00
|
|
|
|
script:
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- ./gradlew ktlintCheck detekt --no-daemon # Lint / Static Checks
|
2026-08-12 18:23:11 +08:00
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
unit-integration-test:
|
|
|
|
|
|
stage: validate
|
2026-08-12 18:23:11 +08:00
|
|
|
|
script:
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- ./gradlew test --no-daemon # Unit/Integration Tests,含 Testcontainers(见 10-testing.md)
|
2026-08-12 18:23:11 +08:00
|
|
|
|
artifacts:
|
|
|
|
|
|
reports:
|
|
|
|
|
|
junit: '**/build/test-results/test/TEST-*.xml'
|
|
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
build-package:
|
|
|
|
|
|
stage: validate
|
2026-08-12 18:23:11 +08:00
|
|
|
|
script:
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- ./gradlew :bootstrap:bootJar --no-daemon # Build/Package
|
2026-08-12 18:23:11 +08:00
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
security-scan:
|
|
|
|
|
|
stage: validate
|
2026-08-12 18:23:11 +08:00
|
|
|
|
script:
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- ./gradlew dependencyCheckAnalyze # Security/Quality Scan(依赖漏洞扫描)
|
2026-08-12 18:23:11 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
这四个 job 对应架构图里 CI Validation 阶段的四项检查,都跑在 GitLab Runner 上,任意一项失败流水线即中止——这一步只验证代码质量,**不产出会被部署的镜像**,MR 流水线到这里就结束。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段三:Artifact & Release Controls(制品与发布控制)
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
docker-build-push:
|
|
|
|
|
|
stage: package
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
cut-release:
|
|
|
|
|
|
stage: release
|
|
|
|
|
|
rules:
|
|
|
|
|
|
- if: '$CI_COMMIT_TAG'
|
|
|
|
|
|
script:
|
|
|
|
|
|
# 把已经验证过的 commit-sha 镜像"打标"成不可变的发布版本,而不是重新构建
|
|
|
|
|
|
- az acr import --name $ACR_NAME --source $ACR_NAME.azurecr.io/conti-backend:$CI_COMMIT_SHORT_SHA --image conti-backend:$CI_COMMIT_TAG
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- `docker-build-push` 把镜像推到 **Azure Container Registry**,Runner 需要有 ACR 的 `AcrPush` 权限(通过 Managed Identity 或 Service Principal 认证,不使用固定密码)。
|
|
|
|
|
|
- `cut-release` 是"Promotion Gate"的起点:只有打了 tag 才会生成一个**不可变的发布版本**(`az acr import` 把 commit-sha 镜像复制成一个语义化 tag,源镜像内容不变,只是多一个别名),后续 UAT/Prod 部署的都是这同一个镜像摘要(digest),保证"UAT 验证过的和 Prod 部署的字节级一致",呼应前面"一个镜像走所有环境"的原则。
|
|
|
|
|
|
- 回滚就是这一层的直接应用:出问题时不重新构建,而是把 Deployment 的镜像 tag 改回上一个已批准的 release 版本(见下面 `rollback` job)。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段四:CD to Azure(部署到 UAT/Prod)
|
|
|
|
|
|
|
|
|
|
|
|
**私有 AKS 对 Runner 的网络要求**:架构图确认 AKS 是 **Private Cluster**(Kubernetes API Server 只能通过 Private Endpoint 访问),这意味着 GitLab 默认的共享公网 Runner **连不上**这个 API Server。落地方式:在 AKS 所在 VNet(或对等互联的 VNet)内部署 **self-hosted GitLab Runner**(跑成 AKS 里的一个专门 namespace,或者 VNet 里的一台 VM/VMSS),只有这个 Runner 能执行 `deploy-*` 系列 job。
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
deploy-uat:
|
|
|
|
|
|
stage: deploy-uat
|
|
|
|
|
|
tags:
|
|
|
|
|
|
- azure-vnet-runner # 指定跑在能访问私有 AKS 的 self-hosted runner 上
|
|
|
|
|
|
environment:
|
|
|
|
|
|
name: uat
|
|
|
|
|
|
rules:
|
|
|
|
|
|
- if: '$CI_COMMIT_TAG'
|
|
|
|
|
|
when: manual # Promotion Gate:需要人工点击"Promote to UAT"
|
|
|
|
|
|
script:
|
|
|
|
|
|
- az login --identity # Runner 用 Managed Identity 登录 Azure
|
|
|
|
|
|
- az aks get-credentials --resource-group $RG --name $AKS_NAME --overwrite-existing
|
|
|
|
|
|
# Key Vault / Config Retrieval:从 Key Vault 读值渲染成 K8s Secret(见 07-config-governance.md 方式一)
|
|
|
|
|
|
- JWT_SECRET=$(az keyvault secret show --vault-name conti-backend-kv --name security-jwt-secret --query value -o tsv)
|
|
|
|
|
|
- DB_PASSWORD=$(az keyvault secret show --vault-name conti-backend-kv --name db-password --query value -o tsv)
|
|
|
|
|
|
- kubectl create secret generic conti-backend-secret -n retailapp-uat
|
|
|
|
|
|
--from-literal=SECURITY_JWT_SECRET="$JWT_SECRET"
|
|
|
|
|
|
--from-literal=DB_PASSWORD="$DB_PASSWORD"
|
|
|
|
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
|
|
- kubectl apply -f k8s/configmap-uat.yaml -n retailapp-uat
|
|
|
|
|
|
- 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
|
|
|
|
|
|
- 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
|
|
|
|
|
|
- kubectl set image deployment/conti-backend conti-backend=$CI_REGISTRY_IMAGE:$LAST_APPROVED_TAG -n retailapp-prod
|
|
|
|
|
|
- kubectl rollout status deployment/conti-backend -n retailapp-prod --timeout=180s
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
几个关键点:
|
|
|
|
|
|
|
|
|
|
|
|
- **`tags: [azure-vnet-runner]`**:强制这几个 job 只能被部署在 AKS 私有网络内、能直连 API Server 的 self-hosted Runner 执行,公网共享 Runner 没有这个 tag,天然不会被误调度去执行部署。
|
|
|
|
|
|
- **Runner 认证 Azure 用 Managed Identity**(`az login --identity`),不在 CI 变量里存长期有效的 Service Principal 密码,减少凭证泄漏面。
|
|
|
|
|
|
- **Key Vault/Config Retrieval**:Runner 用 `az keyvault secret show` 现取值,`kubectl create secret --dry-run=client -o yaml | kubectl apply -f -` 渲染成 K8s Secret(见 [07-config-governance.md](./07-config-governance.md#azure-上-secret-的真正来源key-vault不是手写-k8s-secret));密钥值只在这个 job 的执行过程中短暂存在(不会打印到日志、不落盘到镜像),换来的好处是不需要在 AKS 上额外装 CSI 插件、不需要给节点配 Managed Identity 绑定,配置都集中在 GitLab 侧。
|
|
|
|
|
|
- **`when: manual` = Promotion Gate**:UAT/Prod 部署都设成手动触发(GitLab Protected Environments 可以进一步限制"只有某些角色能点这个按钮")——打了 tag 之后不会自动上线,需要专人点一次"Promote to UAT",验证通过后再点一次"Promote to Prod"。
|
|
|
|
|
|
- **部署的是 tag 不是 commit-sha**:`deploy-uat`/`deploy-prod` 用的镜像引用都是 `$CI_COMMIT_TAG`(对应阶段三里 `az acr import` 生成的不可变发布版本),而不是重新拿 commit-sha 构建——这就是"同一个制品在环境间晋升"而不是"每个环境各自构建"。
|
|
|
|
|
|
- **回滚**不重新跑构建流水线,只是把 `LAST_APPROVED_TAG`(上一个已经在 Prod 跑过的 release tag,记录在部署记录/GitLab Environment 历史里)重新 `kubectl set image` 一次,几秒钟内完成,这也是为什么"发布版本必须不可变"很重要——回滚目标必须是确定性的、镜像内容不会变的一个 tag。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段五:Azure Environments(部署目标)
|
|
|
|
|
|
|
|
|
|
|
|
UAT/Production 对应同一个 Private AKS 集群里两个独立的 namespace(`retailapp-uat` / `retailapp-prod`),各自有独立的 Deployment/Pod/Service,各自的 ConfigMap/Secret(见 [07-config-governance.md](./07-config-governance.md))、各自的资源配额(`ResourceQuota`/`LimitRange`,防止某个环境的异常负载影响另一个)。两个环境共享同一个物理集群,靠 namespace + NetworkPolicy 隔离,而不是各自起一个集群——集群运维成本更低,也符合"环境差异只在配置层面"的原则。Dev 不在这个集群里,跑在公司内网一台 Ubuntu 服务器的 k3s 集群上,见前面"环境层级"一节。
|
2026-08-12 18:23:11 +08:00
|
|
|
|
|
|
|
|
|
|
## 关键规则
|
|
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- UAT/Prod 通过 K8s namespace + ConfigMap/Secret 区分(见 [07-config-governance.md](./07-config-governance.md)),**镜像本身不区分环境**,同一个镜像跨环境部署,只是挂载的 ConfigMap/Secret 和 `SPRING_PROFILES_ACTIVE` 不同——避免"UAT 验证过的镜像和 Prod 部署的镜像不是同一个产物"这种环境不一致风险。
|
|
|
|
|
|
- Dev 是独立的一层:跑在公司内网一台 Ubuntu 服务器的 k3s 集群上,不接入 CI/CD 自动触发,谁需要更新就手动跑 `scripts/deploy-dev.sh`(不接 Key Vault),跟 UAT/Prod 的"打 tag 才能晋升"这条链路是分开的,见前面"环境层级"一节。
|
|
|
|
|
|
- CI 流程顺序:Gradle build(含单元测试)→ 打 Docker 镜像 → 推送 ACR → 打 release tag → 按 UAT(人工晋升)/Prod(人工审批)顺序部署;Dev 不在这条流水线里,需要时手动执行部署脚本。
|
|
|
|
|
|
- 部署到私有 AKS 的 job 必须跑在能访问集群私有网络的 self-hosted Runner 上,公网共享 Runner 无法执行这些 job(网络层面直接不通,不是权限层面的限制);Dev 环境没有 Runner,直接由团队成员在自己电脑上连 VPN 手动执行部署脚本。
|
2026-08-12 18:23:11 +08:00
|
|
|
|
- 模块化单体阶段只有一个部署产物(一个 Deployment);如果后续拆分微服务,每个 domain 各自补一份 `Dockerfile` 和 CI job,工程结构上已经按模块划好边界(见 [01-project-structure.md](./01-project-structure.md)),拆分成本较低——本质上是把 `bootstrap` 依赖的某个 `domains/xxx` 模块摘出来,单独套一层 `@SpringBootApplication` 入口和自己的 `Dockerfile`。
|
|
|
|
|
|
|
|
|
|
|
|
## 附录:为什么坚持"一个镜像走所有环境"
|
|
|
|
|
|
|
|
|
|
|
|
一种常见但有风险的做法是:给每个环境单独打包(比如构建时注入 `application-uat.yml` 到镜像里),这样看起来"环境隔离更彻底",但实际引入了一个更严重的问题——**UAT 验证通过的镜像和 Prod 部署的镜像,字节级别就不是同一个东西**,即使代码版本号一样,构建过程中的依赖解析、基础镜像 layer 缓存状态都可能有细微差异,理论上会出现"UAT 测过没问题,Prod 部署后行为不一致"的情况,而且事后很难证明"两次构建到底有没有差异"。
|
|
|
|
|
|
|
|
|
|
|
|
"一个镜像走所有环境"(Build once, deploy many)反过来保证:镜像本身在所有环境完全一致,环境差异只体现在外部注入的配置(ConfigMap/Secret/环境变量)上。这也是 [The Twelve-Factor App](https://12factor.net/zh_cn/build-release-run) 里"严格分离构建和运行"这条原则的直接应用。
|
|
|
|
|
|
|
|
|
|
|
|
## 待补充
|
|
|
|
|
|
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- 灰度发布(金丝雀/蓝绿)方案——目前只有整体切流的滚动更新,还没有按流量比例灰度的方案。
|
2026-08-12 18:23:11 +08:00
|
|
|
|
- Gradle 构建缓存/并行构建的 CI 加速配置。
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- self-hosted Runner 本身的高可用和运维(比如 Runner 所在 VM/VMSS 的扩缩容、镜像更新)。
|
2026-08-12 18:23:11 +08:00
|
|
|
|
|
|
|
|
|
|
## 参考链接
|
|
|
|
|
|
|
|
|
|
|
|
- [The Twelve-Factor App](https://12factor.net/zh_cn/)
|
|
|
|
|
|
- [Gradle 官方 Docker 集成建议](https://docs.gradle.org/current/userguide/multi_project_builds.html)
|
|
|
|
|
|
- [Spring Boot 官方 Docker 打包指南](https://docs.spring.io/spring-boot/reference/packaging/container-images/dockerfiles.html)
|
2026-08-13 15:19:05 +08:00
|
|
|
|
- [k3s 官方文档](https://docs.k3s.io/)
|