backend scaffold

This commit is contained in:
Guangfei.Zhao
2026-08-17 15:31:27 +08:00
commit 84fc2c0677
159 changed files with 10542 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# Dev 的配置,由 scripts/deploy-dev.sh apply 到内网 k3s 集群的 retailapp-dev namespace。
# Dev 的数据库跑在同一台 Ubuntu 机器上(不是 Azure Flexible Server),所以连的是集群内的 Service。
apiVersion: v1
kind: ConfigMap
metadata:
name: conti-backend-env
namespace: retailapp-dev
data:
DB_HOST: "mysql.retailapp-dev.svc.cluster.local"
DB_USERNAME: "conti"
SECURITY_JWT_ACTIVE_KEY_ID: "v1"
# Dev 环境暂时没有可用的 F6 / O2O 联调地址,先指向不存在的地址,
# 走的就是降级路径——这本身也是对 fallback 的一次真实验证(见 05-integration-layer.md
F6_BASE_URL: "http://f6-not-available.retailapp-dev.svc.cluster.local"
MINI_BASE_URL: "http://mini-not-available.retailapp-dev.svc.cluster.local"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: conti-backend-config
namespace: retailapp-dev
data:
application-dev.yml: |
# Dev 上放开到 DEBUG,方便团队排查;改这一项是热更新的典型用例,
# 改完 15 秒生效,不用重新部署
logging:
level:
com.continental.retailapp: DEBUG
resilience4j:
circuitbreaker:
instances:
f6-api:
failure-rate-threshold: 50
slow-call-duration-threshold: 1500ms
+37
View File
@@ -0,0 +1,37 @@
# Prod 的配置。结构与 configmap-uat.yaml 完全一致,只有取值不同——
# 环境差异全部收敛在这里,Deployment 之间只差 namespace 和 profile。
apiVersion: v1
kind: ConfigMap
metadata:
name: conti-backend-env
namespace: retailapp-prod
data:
# UAT 和 Prod 是两个**独立的** Flexible Server 实例,不是同一实例上的两个 database——
# 共用实例意味着 UAT 的一次压测能直接影响生产(见 09-build-deploy.md 阶段五)
DB_HOST: "conti-mysql-prod.mysql.database.azure.com"
DB_USERNAME: "conti_app"
SECURITY_JWT_ACTIVE_KEY_ID: "v1"
F6_BASE_URL: "https://f6.internal.example.com"
MINI_BASE_URL: "https://mini.internal.example.com"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: conti-backend-config
namespace: retailapp-prod
data:
application-prod.yml: |
logging:
level:
root: INFO
com.continental.retailapp: INFO
# 生产绝不打开 SQL 日志:绑定参数里会带 PII(见 08-observability.md
org.hibernate.SQL: WARN
# 同 UAT:改了要走发布才生效,不是热更新
resilience4j:
circuitbreaker:
instances:
f6-api:
failure-rate-threshold: 50
slow-call-duration-threshold: 1500ms
+44
View File
@@ -0,0 +1,44 @@
# UAT 的配置。两个 ConfigMap,用途不同,别合并:
#
# - conti-backend-env:以**环境变量**注入(Deployment 的 envFrom),application.yml 里
# ${DB_HOST} 这类占位符从这里取值。改了必须重启 Pod 才生效。
# - conti-backend-config:由 spring-cloud-kubernetes 直接读 K8s API 拿到(不挂卷),
# 15 秒轮询一次,改了能热更新——但只对 @RefreshScope bean 生效,见下面的注释。
#
# 敏感值一个都不在这里,全在 Secret 里(见 secret-uat.yaml)。
apiVersion: v1
kind: ConfigMap
metadata:
name: conti-backend-env
namespace: retailapp-uat
data:
# Azure Database for MySQL Flexible Server,通过 Private Endpoint 接入 AKS 所在 VNet。
# 连接串里的 sslMode=REQUIRED 写在 application.yml 中,Flexible Server 默认要求 TLS。
DB_HOST: "conti-mysql-uat.mysql.database.azure.com"
# 运行时账号,只有 DML 权限;迁移用的 DDL 账号是另一个(见 09-build-deploy.md
DB_USERNAME: "conti_app"
SECURITY_JWT_ACTIVE_KEY_ID: "v1"
F6_BASE_URL: "https://f6-uat.internal.example.com"
MINI_BASE_URL: "https://mini-uat.internal.example.com"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: conti-backend-config
namespace: retailapp-uat
data:
application-uat.yml: |
# 这一段是**能**热更新的:改完 15 秒内通过 /actuator/loggers 同样的机制生效
logging:
level:
com.continental.retailapp: INFO
# 这一段是**不能**热更新的,放在这里只是为了让阈值和代码分离、改的时候不用重新构建镜像。
# resilience4j 的实例在启动时创建,普通 refresh 刷不到它们——改完要走一次发布
# (或者 strategy: restart_context,等于一次内部重启)。见 07-config-governance.md 的表格。
resilience4j:
circuitbreaker:
instances:
f6-api:
failure-rate-threshold: 50
slow-call-duration-threshold: 1500ms
+104
View File
@@ -0,0 +1,104 @@
# Dev:跑在公司内网一台 Ubuntu 服务器的 k3s 上,不接 CI/CD,由 scripts/deploy-dev.sh 手动 apply
# (见 09-build-deploy.md「环境层级」)。
#
# 跟 UAT/Prod 的两处有意差别:
# 1. replicas: 1 —— 单节点 k3s,起两个副本没有可用性收益,只是多占内存;
# 代价是滚动更新期间会有短暂不可用,Dev 环境可以接受,所以也不配 PDB。
# 2. requests 调小 —— 这台机器上还跑着别的东西。
# 其余(探针、优雅停机、securityContext)与另外两份保持一致,
# 这正是 Dev 用真 K8s 而不是 docker compose 的意义:验证的是真实部署行为。
apiVersion: apps/v1
kind: Deployment
metadata:
name: conti-backend
namespace: retailapp-dev
labels:
app: conti-backend
spec:
replicas: 1
selector:
matchLabels:
app: conti-backend
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: conti-backend
spec:
serviceAccountName: conti-backend
terminationGracePeriodSeconds: 45
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: conti-backend
image: registry.example.com/conti-backend:__TAG__
ports:
- name: http
containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: dev
envFrom:
- configMapRef:
name: conti-backend-env
# Dev 的 Secret 是写死的假值,由 scripts/deploy-dev.sh 创建,不接 Key Vault
- secretRef:
name: conti-backend-secret
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "250m"
memory: "768Mi"
limits:
memory: "1536Mi"
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: conti-backend
namespace: retailapp-dev
labels:
app: conti-backend
spec:
selector:
app: conti-backend
ports:
- name: http
port: 80
targetPort: 8080
+97
View File
@@ -0,0 +1,97 @@
# Prod。与 deployment-uat.yaml 逐字一致,只差 namespace 和 SPRING_PROFILES_ACTIVE
# ——两个环境跑的是同一个镜像,差异只在外部注入的配置上(见 09-build-deploy.md 附录)。
# 改动这份时请同步改另外两份,别让三份漂移。
apiVersion: apps/v1
kind: Deployment
metadata:
name: conti-backend
namespace: retailapp-prod
labels:
app: conti-backend
spec:
replicas: 2
selector:
matchLabels:
app: conti-backend
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: conti-backend
spec:
serviceAccountName: conti-backend
terminationGracePeriodSeconds: 45
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: conti-backend
image: registry.example.com/conti-backend:__TAG__
ports:
- name: http
containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
envFrom:
- configMapRef:
name: conti-backend-env
- secretRef:
name: conti-backend-secret
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
memory: "2Gi"
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: conti-backend
namespace: retailapp-prod
labels:
app: conti-backend
spec:
selector:
app: conti-backend
ports:
- name: http
port: 80
targetPort: 8080
+113
View File
@@ -0,0 +1,113 @@
# UAT 环境的部署清单。这是三个环境里的"基准版本"——
# deployment-dev.yaml / deployment-prod.yaml 与它的差别只有 namespace、副本数和镜像 tag
# 其余(探针、优雅停机、securityContext、资源)三份完全一致,
# 避免出现"UAT 好好的、生产漏配了一项"这类问题(见 07-config-governance.md)。
#
# 内容来源:09-build-deploy.md「Deployment 清单」+ 08-observability.md「K8s 探针配置」
# + 07-config-governance.md「ConfigMap / Secret 示例」,三处合并成同一份可 apply 的清单。
apiVersion: apps/v1
kind: Deployment
metadata:
name: conti-backend
namespace: retailapp-uat
labels:
app: conti-backend
spec:
replicas: 2
selector:
matchLabels:
app: conti-backend
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # 更新期间不允许可用副本数低于 replicas,先起新的再停旧的
maxSurge: 1 # 同时只有一个新 Pod 启动,配合 Flyway 表级锁,不会并发迁移
template:
metadata:
labels:
app: conti-backend
spec:
# Pod 要能读所在 namespace 的 ConfigMapspring-cloud-kubernetes 走 K8s API),
# 权限见同目录的 rbac.yaml
serviceAccountName: conti-backend
terminationGracePeriodSeconds: 45 # 必须 > preStop 等待(10) + 应用 graceful 超时(25)
securityContext:
runAsNonRoot: true # 镜像里 USER 写的是数字 10001,这条才校验得过
seccompProfile:
type: RuntimeDefault
containers:
- name: conti-backend
# 初始占位;实际版本由流水线的 `kubectl set image` 覆盖,
# 部署的永远是 cut-release 打出来的那个不可变 tag(见 09-build-deploy.md 阶段三)
image: registry.example.com/conti-backend:__TAG__
ports:
- name: http
containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: uat
envFrom:
# 非敏感的环境变量(DB_HOST / DB_USERNAME / F6_BASE_URL ...
- configMapRef:
name: conti-backend-env
# 敏感值(DB_PASSWORD / SECURITY_JWT_SECRET),由流水线从 Key Vault 现取现渲染
- secretRef:
name: conti-backend-secret
# 注:07 文档的示例里还把 conti-backend-config 挂成了 /app/config 卷。
# 这里没有挂——应用已经通过 spring.cloud.kubernetes.config.sources 直接读同一个
# ConfigMap,两条路都开着等于同一份配置有两个来源,出问题时说不清以谁为准;
# 而且热更新只有走 spring-cloud-kubernetes 这条路才生效。
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
memory: "2Gi" # 只限内存,不限 CPU:cfs 限流会让延迟毫无规律地抖动
startupProbe: # 启动阶段专用,跑通之前 liveness/readiness 都不生效
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 30 # 最多给 150 秒完成 JVM 启动 + Flyway 迁移
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
lifecycle:
preStop:
exec:
# 把 SIGTERM 推迟 10 秒,等 Endpoints 摘除在各节点生效,
# 否则滚动更新期间会有零星 502(见 09-build-deploy.md
command: ["sh", "-c", "sleep 10"]
volumeMounts:
- name: tmp
mountPath: /tmp # readOnlyRootFilesystem 的必需配套:内嵌 Tomcat 要可写的临时目录
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: conti-backend
namespace: retailapp-uat
labels:
app: conti-backend
spec:
selector:
app: conti-backend
ports:
- name: http
port: 80
targetPort: 8080
+28
View File
@@ -0,0 +1,28 @@
# PodDisruptionBudget:保证节点维护、AKS 节点池升级这类**自愿中断**时至少留一个副本在跑。
# 没有它,一次节点池升级可能把两个副本同时驱逐,造成一次没人预料到的短暂全站不可用。
#
# 注意 PDB 只管自愿中断(drain/evict),管不了节点宕机、OOMKilled 这类非自愿中断——
# 那是 replicas + 探针的事。
#
# Dev 没有 PDB:单节点 k3s 上 replicas=1minAvailable: 1 会让节点永远 drain 不掉。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: conti-backend-pdb
namespace: retailapp-uat
spec:
minAvailable: 1
selector:
matchLabels:
app: conti-backend
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: conti-backend-pdb
namespace: retailapp-prod
spec:
minAvailable: 1
selector:
matchLabels:
app: conti-backend
+100
View File
@@ -0,0 +1,100 @@
# spring-cloud-kubernetes 是在 Pod 里调 K8s API 去读 ConfigMap 的,
# 默认的 default ServiceAccount 没有这个权限——漏了这份清单,应用启动时读配置会 403,
# 而且 reload 的轮询会每 15 秒报一次错(见 07-config-governance.md「关键规则」)。
#
# 权限严格收敛到"读所在 namespace 的 ConfigMap"这一件事:
# - 不给 secrets 的 get/listSecret 是通过 envFrom 由 kubelet 注入的,应用自己不需要读;
# 给了反而等于让一次 RCE 直接拿到 JWT 签名密钥。
# - 用 Role 而不是 ClusterRole:跨 namespace 读不到,UAT 的 Pod 看不见 Prod 的配置。
# - reload 用的是 mode: polling 而不是 event,所以不需要 watch 权限。
#
# 三个 namespace 各一份,内容相同。
apiVersion: v1
kind: ServiceAccount
metadata:
name: conti-backend
namespace: retailapp-dev
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: conti-backend-config-reader
namespace: retailapp-dev
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: conti-backend-config-reader
namespace: retailapp-dev
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: conti-backend-config-reader
subjects:
- kind: ServiceAccount
name: conti-backend
namespace: retailapp-dev
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: conti-backend
namespace: retailapp-uat
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: conti-backend-config-reader
namespace: retailapp-uat
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: conti-backend-config-reader
namespace: retailapp-uat
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: conti-backend-config-reader
subjects:
- kind: ServiceAccount
name: conti-backend
namespace: retailapp-uat
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: conti-backend
namespace: retailapp-prod
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: conti-backend-config-reader
namespace: retailapp-prod
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: conti-backend-config-reader
namespace: retailapp-prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: conti-backend-config-reader
subjects:
- kind: ServiceAccount
name: conti-backend
namespace: retailapp-prod
+27
View File
@@ -0,0 +1,27 @@
# 这份文件**只是形状说明**,不是拿来 apply 的。
#
# 真实值由 deploy-uat job 在流水线里从 Azure Key Vault 现取现渲染:
#
# JWT_KEY=$(az keyvault secret show --vault-name conti-backend-kv --name security-jwt-key-v1 ...)
# kubectl create secret generic conti-backend-secret -n retailapp-uat \
# --from-literal=SECURITY_JWT_SECRET="$JWT_KEY" ... | kubectl apply -f -
#
# 密钥只在那个 job 的执行过程中短暂存在,不落盘、不进镜像、不进 git。
# 谁要是把真值填进这个文件并提交,即使之后再删掉,也已经永久留在 git 历史里了——
# 那时候唯一正确的处理是轮换密钥,而不是改一次提交。
apiVersion: v1
kind: Secret
metadata:
name: conti-backend-secret
namespace: retailapp-uat
type: Opaque
stringData:
# HS256 签名密钥,base64 编码;解码后必须 ≥ 32 字节,否则 JwtProperties 在启动时就拒绝
# (见 04-security-auth.md)。轮换时先加 SECURITY_JWT_KEYS_V2,再切 ACTIVE_KEY_ID
# 最后一步才移除 V1——三步之间要隔开,否则已签发的 token 会集体失效。
SECURITY_JWT_SECRET: "__injected_by_pipeline__"
# 运行时账号的密码(只有 DML 权限),迁移账号的密码是另一个
DB_PASSWORD: "__injected_by_pipeline__"
# F6 的调用凭证。骨架里 IntegrationClientProperties.ClientConfig 还没有 apiKey 字段,
# F6ApiClient 也还没往请求头上挂它——接入 F6 真实鉴权时补上这两处,这个 key 就有人用了。
F6_API_KEY: "__injected_by_pipeline__"