学习目标
学完本模块,你应该能够:
- 在平台中完成一次符合商用标准的应用建模(租户 / 环境 / 集群 / 资源规格四要素齐备),并理解命名空间隔离的落地边界。
- 区分滚动、蓝绿、金丝雀三种发布策略的适用场景,并能为不同业务选择最优灰度方案。
- 解释镜像安全扫描如何作为发布前置闸门阻断高危漏洞,并会解读 Trivy 扫描结果与放行策略。
- 配置 HPA 与定时伸缩,结合联邦监控指标说明弹性伸缩的触发与回缩闭环。
- 走完一次带多级审批的发布流程,并在异常时通过版本治理完成回滚。
前置知识
- 理解 Kubernetes 核心对象:Deployment / StatefulSet / Service / Ingress / Namespace。
- 了解 CI/CD 基本概念与镜像仓库(Harbor)工作方式。
- 已阅读本白皮书「模块 01 租户与组织权限中心」「模块 02 多 K8s 集群纳管模块」,理解统一 API 网关与 Namespace 强隔离。
本章你会动手做的事
- 在测试租户 Namespace 下创建一份应用建模 YAML,并触发一次镜像扫描。
- 用金丝雀策略把流量从 5% 逐步放大到 100%,观察 Grafana 租户大盘的延迟变化。
- 故意制造一次发布失败,验证审批被拒后镜像无法落地生产集群。
项目结构与文件清单
本模块提供「平台侧 Go 控制面 + 目标集群 K8s IaC + 联邦观测配置」三件套,目录如下:
paas-alm/ # 应用全生命周期管理模块平台侧源码(Go + gin + client-go)
├── go.mod
├── main.go # gin 入口 + /metrics(暴露 paas_* 业务指标)
├── pkg/
│ ├── gateway/
│ │ └── client.go # 统一 K8s API 网关多集群客户端
│ ├── k8s/
│ │ ├── deploy.go # 创建/更新 Deployment、HPA(client-go 调用 K8s)
│ │ ├── rollout.go # 滚动重启、蓝绿切换、金丝雀 setWeight、读取 Pod 状态
│ │ └── backup.go # 触发备份 Job
│ ├── metrics/
│ │ └── metrics.go # prometheus 业务指标(与观测配置一一对应)
│ └── handler/
│ └── release.go # gin 发布 / 状态 / 备份 Handler
├── manifests/ # K8s IaC(kubectl apply 即用)
│ ├── tenant-namespace.yaml # 租户 Namespace + ResourceQuota + LimitRange
│ ├── app-deployment.yaml # Deployment + Service + HPA(滚动)
│ ├── app-bluegreen.yaml # 蓝绿两套 Deployment + Service
│ ├── app-canary-rollouts.yaml # Argo Rollouts 金丝雀
│ ├── app-config-secret.yaml # ConfigMap + SealedSecret(KMS 信封加密)
│ └── app-pipeline-approval.yaml # Tekton 流水线 + 平台审批 CRD
└── observability/ # Prometheus / 联邦观测配置
├── app-servicemonitor.yaml # 应用 ServiceMonitor
├── prometheus-relabel.yaml # 联邦抓取 + tenant/app/env/cluster 注入
├── app-recording-rules.yaml # SLO / 资源 recording rules
└── app-alert-rules.yaml # 5xx / OOM / 就绪 / 熔断告警
下面各章逐文件完整展示,且所有文件相互自洽:Go 创建的资源名 = YAML 中的 metadata.name,Go 暴露的 paas_* 指标 = 观测配置中的 recording/alert 输入。
一、模块概述与企业商用价值
应用全生命周期管理模块(Application Lifecycle Management,简称 ALM)是企业 PaaS 平台面向业务研发的第一入口。它把"写代码 → 出镜像 → 过安全 → 走审批 → 发到生产 → 弹性运行 → 出问题回滚 → 不再需要就下线"这一整条链路,收敛成一个平台可控、审计可追、安全可阻断的标准化流程。
类比:这套模块像一栋写字楼的"企业级搬家公司 + 物业"。研发团队(租户)只要把打包好的集装箱(镜像)交给平台,平台负责先验货(镜像扫描)、办手续(发布审批)、安排楼层(目标集群 Namespace)、装电表(指标标签),并承诺随时能原样搬回上一版(回滚)。研发不再需要自己开着叉车(直接 kubectl 操作集群),避免了"谁都能动生产"的混乱。
适用角色与业务痛点
| 角色 | 痛点 | 本模块提供的价值 |
|---|---|---|
| 业务研发 | 直接操作集群风险高、发布姿势不统一 | 标准化发布入口,屏蔽集群差异 |
| 集群运维 | 多集群发布靠脚本,回滚无据可查 | 统一编排 + 版本治理 + 审计留痕 |
| 平台管理员 | 安全与合规无法前置卡点 | 镜像扫描闸门 + 多级审批 + 配置加密 |
合规与不可替代性
在金融 / 政企场景下,应用发布必须满足:镜像不含高危漏洞(安全合规)、发布经过多级审批(内控制度)、敏感配置不落明文(数据合规)、全程操作可审计 90 天以上(监管留存)。本模块把以上要求固化为平台能力,而非依赖个人自觉。
下面这张图给出本模块在整体 PaaS 架构中的定位。
graph TB
subgraph 研发侧
Dev[业务研发团队]
CI[CI 流水线 GitLab/Jenkins]
end
subgraph PaaS平台
ALM[应用全生命周期管理模块]
Scan[(镜像扫描 Trivy)]
Appr[发布审批引擎]
Enc[配置加密 KMS]
end
subgraph 基础设施
GW[统一 K8s API 网关]
NS1[(生产租户 Namespace)]
NS2[(测试租户 Namespace)]
end
Dev --> CI --> ALM
ALM --> Scan
ALM --> Appr
ALM --> Enc
ALM --> GW
GW --> NS1
GW --> NS2这张图说明:研发不直接触达集群,所有动作经由 ALM 模块统一收敛,再经 API 网关落到对应租户 Namespace。
建模落地的 IaC:租户 Namespace 强隔离
应用建模的第一个动作,是在目标集群创建「租户 Namespace + 配额 + LimitRange」。以下 manifests/tenant-namespace.yaml 由平台在创建应用时自动 kubectl apply,标签 tenant/env 即强隔离边界(与模块 02 一致)。
# file: manifests/tenant-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: tenant-a-prod
labels:
tenant: tenant-a # 租户维度
env: prod # 环境维度(prod/pre/ test/dev)
paas.example.com/managed: "true"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-a-prod-quota
namespace: tenant-a-prod
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "50"
persistentvolumeclaims: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: tenant-a-prod-limitrange
namespace: tenant-a-prod
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: 512Mi
defaultRequest:
cpu: "100m"
memory: 128Mi
max:
cpu: "4"
memory: 8Gi
二、细分功能详解(商用生产级)
下面用「基础能力」与「高级企业增值能力」两级区分。带 ★ 标记项为禁止删减的商用底线能力。
功能分级总览
| 级别 | 功能项 | 商用说明 |
|---|---|---|
| 【基础能力】 | ① 应用建模 | 租户 / 环境 / 集群 / 资源规格四要素建模 |
| 【基础能力】 | ② 镜像仓库对接 | 对接 Harbor,拉取与推送受 RBAC 约束 |
| 【基础能力】 | ④ 部署策略 | 滚动 / 蓝绿 / 金丝雀三种原生支持 |
| 【基础能力】 | ⑤ 弹性伸缩 | HPA + 定时伸缩,基于联邦指标 |
| 【基础能力】 | ⑧ 回滚与版本治理 | 保留历史版本,一键回退 |
| 【基础能力】 | ⑨ 应用下线回收 | 资源释放 + 配额回收 + 审计归档 |
| 【高级企业增值能力】 | ③ CI/CD 集成 | Webhook 触发流水线,多分支策略 |
| 【高级企业增值能力】 | ⑥ 配置与密钥管理 | 加密注入,Secret 不落明文 |
| 【高级企业增值能力】 | ⑦ 应用监控大盘 | 按租户过滤的 Grafana 大盘 |
| ★禁止删减 | ★ 镜像扫描 | Trivy 阻断高危漏洞,未过扫不出库 |
| ★禁止删减 | ★ 发布审批 | 生产发布必须多级审批通过 |
| ★禁止删减 | ★ 配置加密 | 敏感配置经 KMS 信封加密注入 |
① 应用建模(基础能力)
应用是平台的顶级资源对象,建模时必须绑定四个维度:
- 租户:决定落在哪个 Namespace,强隔离边界。
- 环境:生产 / 预发布 / 测试 / 开发,对应不同物理隔离集群。
- 目标集群:经 API 网关纳管的多套 K8s 之一。
- 资源规格:CPU / 内存 Request-Limit、副本数、存储类。
manifests/tenant-namespace.yaml(见第一章)即为建模在 K8s 侧的落地产物。
② 镜像仓库与镜像安全扫描(★禁止删减)
镜像推送到 Harbor 后,平台自动触发 Trivy 扫描,按漏洞严重度(Critical / High / Medium / Low)给出结论。生产发布闸门规则:存在未修复的 Critical/High 漏洞即阻断发布,研发须修复或提交豁免审批。
⚠️ 注意:扫描闸门只在"发布"环节生效,测试环境默认仅告警不阻断;但生产环境的阻断不可配置关闭,这是合规底线。
以下流水线 + 平台审批 CRD(manifests/app-pipeline-approval.yaml)把扫描闸门与多级审批固化为资源:
# file: manifests/app-pipeline-approval.yaml
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: order-service-build
namespace: tenant-a-ci
labels:
app: order-service
tenant: tenant-a
env: prod
spec:
pipelineRef:
name: build-and-scan
params:
- name: git-repo
value: git@example.com/tenant-a/order-service.git
- name: image
value: harbor.example.com/order-service:1.4.0
workspaces:
- name: shared
persistentVolumeClaim:
claimName: build-pvc
---
# 平台内审批闸门(平台自定义 CRD):生产发布必须多级审批通过
apiVersion: paas.example.com/v1
kind: ReleaseApproval
metadata:
name: order-service-1-4-0
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
spec:
app: order-service
image: harbor.example.com/order-service:1.4.0
strategy: canary
approvers:
- role: dev-lead
status: approved
- role: ops-lead
status: pending # ★ 多级审批,未通过前禁止下发
gates:
imageScan: passed # Trivy 扫描闸门结论
③ CI/CD 集成(高级企业增值能力)
通过 Webhook 把 GitLab / Jenkins 流水线结果回写平台,平台监听 push 与 pipeline success 事件自动生成待发布版本。多分支策略:主干分支进预发布,Tag 分支可进生产(仍需审批)。上面的 PipelineRun 即为平台触发的构建-扫描流水线。
④ 部署策略(基础能力)
- 滚动更新(Rolling):默认策略,逐批替换 Pod,适合无状态服务。
- 蓝绿(Blue-Green):两套完全环境,切流瞬时完成,回滚只需切回。
- 金丝雀(Canary):按权重逐步放量(5% → 20% → 50% → 100%),结合监控指标自动决策是否继续。
滚动 + HPA(manifests/app-deployment.yaml):
# file: manifests/app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
replicas: 3
selector:
matchLabels:
app: order-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
template:
metadata:
labels:
app: order-service
tenant: tenant-a
env: prod
cluster: cluster-prod
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: order-service
image: harbor.example.com/order-service:1.4.0
ports:
- containerPort: 8080
name: http
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /livez
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
envFrom:
- configMapRef:
name: order-service-config
- secretRef:
name: order-service-secret
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
selector:
app: order-service
ports:
- name: http
port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
蓝绿(两套 Deployment + 一个 Service,manifests/app-bluegreen.yaml):
# file: manifests/app-bluegreen.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-blue
namespace: tenant-a-prod
labels:
app: order-service
release: blue
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
replicas: 3
selector:
matchLabels:
app: order-service
release: blue
template:
metadata:
labels:
app: order-service
release: blue
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
containers:
- name: order-service
image: harbor.example.com/order-service:1.3.0
ports:
- containerPort: 8080
name: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-green
namespace: tenant-a-prod
labels:
app: order-service
release: green
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
replicas: 3
selector:
matchLabels:
app: order-service
release: green
template:
metadata:
labels:
app: order-service
release: green
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
containers:
- name: order-service
image: harbor.example.com/order-service:1.4.0
ports:
- containerPort: 8080
name: http
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
# 初始指向 blue;蓝绿切换由平台 Go 改 selector 到 green(秒级回滚只需切回)
selector:
app: order-service
release: blue
ports:
- name: http
port: 80
targetPort: 8080
金丝雀(Argo Rollouts,manifests/app-canary-rollouts.yaml):
# file: manifests/app-canary-rollouts.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: order-service
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
replicas: 10
strategy:
canary:
analysis:
templates:
- templateName: success-rate
successCondition: result >= 0.95
startingStep: 1
steps:
- setWeight: 5
- pause: { duration: 5m }
- setWeight: 20
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
containers:
- name: order-service
image: harbor.example.com/order-service:1.4.0
ports:
- containerPort: 8080
name: http
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: tenant-a-prod
spec:
metrics:
- name: success-rate
interval: 1m
successCondition: result >= 0.95
provider:
prometheus:
address: http://thanos-query.monitoring.svc:9090
query: |
sum(rate(http_requests_total{app="order-service",code!~"5..",tenant="tenant-a",env="prod"}[5m]))
/
sum(rate(http_requests_total{app="order-service",tenant="tenant-a",env="prod"}[5m]))
⑤ 弹性伸缩(基础能力)
- HPA:基于 CPU / 内存 / 自定义指标(来自联邦 Prometheus)自动扩缩,见
manifests/app-deployment.yaml中 HPA。 - 定时伸缩(CronHPA):应对可预测的流量高峰(如交易日开盘),平台通过 client-go 更新 HPA 的
minReplicas实现(见第三章pkg/k8s/deploy.go)。
⑥ 配置与密钥管理(★禁止删减)
敏感配置(数据库密码、AK/SK)通过 KMS 信封加密后以密文存入平台密钥库,注入 Pod 时由 SealedSecret 控制器在集群内解密为 order-service-secret,绝不落盘明文。
# file: manifests/app-config-secret.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
data:
LOG_LEVEL: "info"
APP_NAME: "order-service"
---
# 敏感配置经 KMS 信封加密后,以 SealedSecret 分发;解密发生在集群内。
# 解密产物即 Deployment 引用的 order-service-secret(与 app-deployment.yaml 中 secretRef 一致)。
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: order-service-sealed
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
spec:
encryptedData:
DB_PASSWORD: AgB.sSEAED8nR9QK0examplebase64sealed==
AK_SK: AgB.sSEAED8nR9QK0examplebase64sealed==
template:
metadata:
name: order-service-secret # 解密后生成的 Secret 名
namespace: tenant-a-prod
labels:
app: order-service
tenant: tenant-a
env: prod
type: Opaque
⑦ 应用监控大盘(高级企业增值能力)
Grafana 按 tenant/app/env/cluster 标签过滤,租户只能看到自己应用的大盘,平台管理员可见全局。
⑧ 回滚与版本治理(基础能力)
平台保留每个应用的版本快照(镜像、配置、编排),回滚即把目标集群 Deployment 重置到历史 Revision。运维命令见第六章 runbook。
⑨ 应用下线回收(基础能力)
下线时释放 Pod、Service、PVC,回收 Namespace 配额,并归档操作到审计系统 90 天以上。
下面这张图展示模块内部功能组件关系。
graph LR
A[应用建模] --> B[镜像仓库对接]
B --> C[镜像安全扫描 Trivy]
C --> D{漏洞达标?}
D -->|否| X[阻断并告警]
D -->|是| E[发布审批引擎]
E --> F[部署策略
滚动/蓝绿/金丝雀]
F --> G[配置加密注入]
G --> H[弹性伸缩 HPA]
H --> I[监控大盘]
I --> J[回滚与版本治理]
J --> K[下线回收]这张图按"建模 → 扫描 → 审批 → 部署 → 加密 → 伸缩 → 监控 → 回滚 → 下线"串联起全生命周期各环节。
三、底层架构联动设计
本模块不是孤立的 UI,它向上承接研发操作,向下通过统一 API 网关驱动多套物理隔离 K8s 集群,并向侧边把指标与事件汇入联邦监控。
1. 与多 K8s 集群的交互链路
应用控制器(平台侧)不直接持有各集群 kubeconfig,而是把期望状态(Desired State)下发给统一 K8s API 网关,由网关解密并转发到对应集群的 apiserver;目标集群返回的实际状态再经网关回传,控制器做调谐(Reconcile)。这样实现:研发永远不知道集群地址,租户只能落到被授权的 Namespace。
下面是平台侧多集群客户端的核心实现 pkg/gateway/client.go,它经由 API 网关的聚合 apiserver 访问各集群(client-go 是平台调用 K8s 的总入口):
// file: pkg/gateway/client.go
package gateway
import (
"context"
"fmt"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// ClusterGateway 统一 K8s API 网关客户端管理器。
// 平台不直接持有各集群 kubeconfig,而是通过 API 网关的聚合 apiserver 访问。
type ClusterGateway struct {
// clients 以 clusterID 为键缓存各集群客户端
clients map[string]kubernetes.Interface
// gatewayREST 为 API 网关(聚合层)的 REST config
gatewayREST *rest.Config
}
// NewClusterGateway 从网关 kubeconfig 初始化。
func NewClusterGateway(kubeconfigPath string) (*ClusterGateway, error) {
cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
return nil, fmt.Errorf("build gateway config: %w", err)
}
// 走 API 网关的 impersonation 头,按租户隔离(研发永不直接拿到集群凭证)
cfg.Impersonate = rest.ImpersonationConfig{
UserName: "paas-platform",
Groups: []string{"paas:platform"},
}
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("new gateway clientset: %w", err)
}
return &ClusterGateway{
clients: map[string]kubernetes.Interface{"gateway": clientset},
gatewayREST: cfg,
}, nil
}
// RESTConfig 暴露底层 *rest.Config,供 dynamic client 复用(金丝雀 patch 需要)。
func (g *ClusterGateway) RESTConfig() *rest.Config { return g.gatewayREST }
// ClientFor 返回指定集群(经网关纳管)的客户端。
func (g *ClusterGateway) ClientFor(ctx context.Context, clusterID string) (kubernetes.Interface, error) {
if c, ok := g.clients[clusterID]; ok {
return c, nil
}
// 生产中:网关按 clusterID 路由到对应后端集群 apiserver
cs, err := kubernetes.NewForConfig(g.gatewayREST)
if err != nil {
return nil, fmt.Errorf("client for cluster %s: %w", clusterID, err)
}
g.clients[clusterID] = cs
return cs, nil
}
2. 与联邦 Prometheus 监控的联动(必绑定)
应用在目标集群产生的部署事件、Pod 就绪数、QPS、错误率、资源使用率等,由集群内的 Prometheus Agent 采集,并强制注入 tenant/app/env/cluster 四个标签,再经联邦汇总进中心 Prometheus + Thanos 长期存储。Grafana 据此按租户过滤出独立大盘;当金丝雀版本错误率超阈值,触发 Alertmanager 按租户 / 环境分级路由告警,必要时自动暂停放量。
应用侧的 ServiceMonitor(observability/app-servicemonitor.yaml)与中心抓取 + relabel 配置(observability/prometheus-relabel.yaml)如下:
# file: observability/app-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: order-service
namespace: monitoring
labels:
tenant: tenant-a
env: prod
release: paas
spec:
selector:
matchLabels:
app: order-service
namespaceSelector:
matchNames:
- tenant-a-prod
endpoints:
- port: http
path: /metrics
interval: 15s
# file: observability/prometheus-relabel.yaml
# 中心 Prometheus 抓取配置片段:把 Namespace/Pod 标签注入为 tenant/app/env/cluster 维度
scrape_configs:
- job_name: paas-apps
kubernetes_sd_configs:
- role: endpoints
namespaces:
names:
- tenant-a-prod
- tenant-a-test
relabel_configs:
# 从 Pod 标签提取业务维度(四维标签强注入)
- source_labels: [__meta_kubernetes_pod_label_tenant]
target_label: tenant
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- source_labels: [__meta_kubernetes_pod_label_env]
target_label: env
- source_labels: [__meta_kubernetes_pod_label_cluster]
target_label: cluster
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__address__]
target_label: instance
# 平台控制面自身暴露的 paas_* 业务指标(已自带 tenant/app/env/cluster 标签)
- job_name: paas-platform
static_configs:
- targets: ["paas-alm.platform.svc:8080"]
relabel_configs:
- source_labels: [__address__]
target_label: instance
下面这张图是联邦监控联动拓扑,务必绑定理解。
graph TB
subgraph 集群A[生产集群 租户NS]
PA1[Prometheus Agent]
APP1[应用 Pod]
end
subgraph 集群B[测试集群 租户NS]
PA2[Prometheus Agent]
APP2[应用 Pod]
end
APP1 -->|指标带 tenant/app/env/cluster| PA1
APP2 -->|指标带 tenant/app/env/cluster| PA2
PA1 -->|联邦拉取| CENTER[中心 Prometheus]
PA2 -->|联邦拉取| CENTER
CENTER --> THANOS[Thanos 长期存储]
CENTER --> GRAF[Grafana 多租户大盘]
CENTER --> AM[Alertmanager 分级告警]
AM -->|异常放量| ALM[应用模块暂停金丝雀]这张图说明:应用指标带四维标签进联邦,Grafana/Alertmanager 据此做隔离与分级,发布异常可反哺应用模块。
3. 与 RBAC / 审批 / 审计的联动
- RBAC:谁能创建应用、谁能发起生产发布,由模块 01 的细粒度角色控制,Namespace 级 + API 级双重约束。
- 审批:生产发布事件写入审批引擎,多级审批(如 研发 Leader → 运维负责人)通过后才放行进网关。
- 审计:建模、扫描结论、审批动作、实际发布、回滚全部留痕,留存 ≥ 90 天,不可绕过、不可篡改。
四、端到端标准操作流程
下面以"研发发布一个新版本到生产"为主线,分三个角色给出可培训用的分步操作。
角色与职责
- 业务研发:提交代码、触发流水线、发起发布申请。
- 集群运维:配置目标集群资源规格、复核 HPA。
- 平台管理员:审批生产发布、处置异常告警。
操作流程时序
sequenceDiagram participant Dev as 业务研发 participant CI as CI流水线 participant ALM as 应用模块 participant Scan as Trivy扫描 participant Appr as 审批引擎 participant Admin as 平台管理员 participant GW as K8s API网关 participant Cluster as 生产集群 Dev->>CI: 推送代码并触发构建 CI->>ALM: 流水线成功,生成待发布版本 ALM->>Scan: 拉取镜像并扫描 Scan->>ALM: 返回漏洞结论(无Critical/High) ALM->>Appr: 发起生产发布审批 Appr->>Admin: 通知待审批 Admin->>Appr: 多级审批通过 Appr->>ALM: 放行 ALM->>GW: 下发期望状态(金丝雀5%) GW->>Cluster: 创建/更新 Deployment/Rollout Cluster->>GW: 回传就绪状态 GW->>ALM: 调谐完成 ALM->>Dev: 发布成功,进入放量观察
这段时序展示了从代码到生产落地的完整链路,以及扫描闸门与审批闸门的串联。
平台侧 Go 实现(gin + client-go 调用 K8s)
下面 pkg/k8s/deploy.go 用 client-go 创建 / 更新 Deployment 与 HPA,是平台下发期望状态的核心:
// file: pkg/k8s/deploy.go
package k8s
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// DeployClient 封装对 Deployment/HPA 的 client-go 操作。
type DeployClient struct {
cs kubernetes.Interface
}
func NewDeployClient(cs kubernetes.Interface) *DeployClient {
return &DeployClient{cs: cs}
}
// EnsureDeployment 创建或更新 Deployment(client-go 调用 K8s 的核心示例)。
func (d *DeployClient) EnsureDeployment(ctx context.Context, ns string, dep *appsv1.Deployment) (*appsv1.Deployment, error) {
client := d.cs.AppsV1().Deployments(ns)
existing, err := client.Get(ctx, dep.Name, metav1.GetOptions{})
if err != nil {
// 不存在则创建
created, creErr := client.Create(ctx, dep, metav1.CreateOptions{})
if creErr != nil {
return nil, fmt.Errorf("create deployment: %w", creErr)
}
return created, nil
}
// 存在则更新(保留 ResourceVersion)
dep.ResourceVersion = existing.ResourceVersion
updated, updErr := client.Update(ctx, dep, metav1.UpdateOptions{})
if updErr != nil {
return nil, fmt.Errorf("update deployment: %w", updErr)
}
return updated, nil
}
// EnsureHPA 创建或更新 HPA(定时伸缩即更新其 minReplicas)。
func (d *DeployClient) EnsureHPA(ctx context.Context, ns string, hpa *autoscalingv2.HorizontalPodAutoscaler) (*autoscalingv2.HorizontalPodAutoscaler, error) {
client := d.cs.AutoscalingV2().HorizontalPodAutoscalers(ns)
existing, err := client.Get(ctx, hpa.Name, metav1.GetOptions{})
if err != nil {
created, creErr := client.Create(ctx, hpa, metav1.CreateOptions{})
if creErr != nil {
return nil, fmt.Errorf("create hpa: %w", creErr)
}
return created, nil
}
hpa.ResourceVersion = existing.ResourceVersion
updated, updErr := client.Update(ctx, hpa, metav1.UpdateOptions{})
if updErr != nil {
return nil, fmt.Errorf("update hpa: %w", updErr)
}
return updated, nil
}
pkg/k8s/rollout.go 用 client-go 执行滚动重启、蓝绿切换、金丝雀放量、读取 Pod 状态:
// file: pkg/k8s/rollout.go
package k8s
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)
var rolloutGVR = schema.GroupVersionResource{
Group: "argoproj.io", Version: "v1alpha1", Resource: "rollouts",
}
// RolloutClient 封装发布相关操作。
type RolloutClient struct {
cs kubernetes.Interface
dyn dynamic.Interface
}
func NewRolloutClient(cs kubernetes.Interface, dyn dynamic.Interface) *RolloutClient {
return &RolloutClient{cs: cs, dyn: dyn}
}
// RollingRestart 触发一次滚动重启(等效 kubectl rollout restart)。
func (r *RolloutClient) RollingRestart(ctx context.Context, ns, name string) error {
client := r.cs.AppsV1().Deployments(ns)
dep, err := client.Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("get deployment: %w", err)
}
if dep.Spec.Template.Annotations == nil {
dep.Spec.Template.Annotations = map[string]string{}
}
dep.Spec.Template.Annotations["paas.example.com/restartedAt"] = metav1.Now().Format("2006-01-02T15:04:05Z07:00")
_, err = client.Update(ctx, dep, metav1.UpdateOptions{})
return err
}
// ShiftTrafficToGreen 蓝绿切换:把 Service 的 selector 从 blue 切到 green(秒级回滚只需切回)。
func (r *RolloutClient) ShiftTrafficToGreen(ctx context.Context, ns, svc, greenLabel string) error {
client := r.cs.CoreV1().Services(ns)
svcObj, err := client.Get(ctx, svc, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("get service: %w", err)
}
svcObj.Spec.Selector = map[string]string{"app": svc, "release": greenLabel}
_, err = client.Update(ctx, svcObj, metav1.UpdateOptions{})
return err
}
// PromoteCanary 通过 JSONPatch 修改 Argo Rollouts 的 setWeight 执行金丝雀放量(Go 调用 K8s CRD)。
func (r *RolloutClient) PromoteCanary(ctx context.Context, ns, name string, weight int32) error {
if weight < 0 || weight > 100 {
return fmt.Errorf("weight out of range: %d", weight)
}
patch := []byte(fmt.Sprintf(
`[{"op":"replace","path":"/spec/strategy/canary/steps/0/setWeight","value":%d}]`, weight))
_, err := r.dyn.Resource(rolloutGVR).Namespace(ns).Patch(
ctx, name, types.JSONPatchType, patch, metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("patch rollout weight: %w", err)
}
return nil
}
// PodStatus 返回某 Deployment 关联 Pod 的就绪情况(Go 调用 K8s 读取状态)。
func (r *RolloutClient) PodStatus(ctx context.Context, ns, app string) (ready, total int, err error) {
pods, err := r.cs.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{
LabelSelector: fmt.Sprintf("app=%s", app),
})
if err != nil {
return 0, 0, fmt.Errorf("list pods: %w", err)
}
total = len(pods.Items)
for _, p := range pods.Items {
for _, c := range p.Status.ContainerStatuses {
if c.Ready {
ready++
break
}
}
}
return ready, total, nil
}
var _ = unstructured.Unstructured{}
var _ = appsv1.Deployment{}
var _ = corev1.Service{}
pkg/k8s/backup.go 用 client-go 触发一次性备份 Job:
// file: pkg/k8s/backup.go
package k8s
import (
"context"
"fmt"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// BackupClient 触发一次性备份 Job。
type BackupClient struct {
cs kubernetes.Interface
}
func NewBackupClient(cs kubernetes.Interface) *BackupClient {
return &BackupClient{cs: cs}
}
// TriggerBackupJob 立即创建一个备份 Job(等效 cron 之外的手动触发)。
func (b *BackupClient) TriggerBackupJob(ctx context.Context, ns, name, image string, args []string) error {
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{"paas.example.com/type": "backup"},
},
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"paas.example.com/type": "backup"},
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{{
Name: "backup",
Image: image,
Args: args,
}},
},
},
},
}
if _, err := b.cs.BatchV1().Jobs(ns).Create(ctx, job, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("trigger backup job: %w", err)
}
return nil
}
pkg/metrics/metrics.go 定义平台暴露的 paas_* 业务指标(与观测配置 recording/alert 一一对应):
// file: pkg/metrics/metrics.go
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
ReleaseTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "paas_release_total",
Help: "Total number of application releases",
},
[]string{"tenant", "app", "env", "cluster", "strategy", "result"},
)
ReleaseDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "paas_release_duration_seconds",
Help: "Duration of application releases in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"tenant", "app", "env", "cluster", "strategy"},
)
ApprovalPending = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "paas_approval_pending",
Help: "Number of pending approvals",
},
)
PodReadyRatio = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "paas_app_pod_ready_ratio",
Help: "Ready pod ratio per application",
},
[]string{"tenant", "app", "env", "cluster"},
)
BackupJobLastSuccess = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "paas_backup_job_last_success_timestamp",
Help: "Timestamp of last successful backup job",
},
[]string{"tenant", "app", "env", "cluster"},
)
)
func init() {
prometheus.MustRegister(ReleaseTotal, ReleaseDuration, ApprovalPending, PodReadyRatio, BackupJobLastSuccess)
}
pkg/handler/release.go 是 gin Handler,把上面所有 client-go 调用收敛成 HTTP 接口:
// file: pkg/handler/release.go
package handler
import (
"context"
"fmt"
"net/http"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/dynamic"
"github.com/gin-gonic/gin"
"github.com/example/paas-alm/pkg/gateway"
"github.com/example/paas-alm/pkg/k8s"
"github.com/example/paas-alm/pkg/metrics"
)
// ReleaseRequest 发布请求体。
type ReleaseRequest struct {
Tenant string `json:"tenant" binding:"required"`
App string `json:"app" binding:"required"`
Env string `json:"env" binding:"required"`
Cluster string `json:"cluster" binding:"required"`
Image string `json:"image" binding:"required"`
Strategy string `json:"strategy"` // rolling | bluegreen | canary
Weight int32 `json:"weight"`
}
// buildDeployment 构造与 manifests/app-deployment.yaml 一致的 Deployment(资源名/标签严格对齐)。
func buildDeployment(req ReleaseRequest, ns string) *appsv1.Deployment {
replicas := int32(3)
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: req.App,
Namespace: ns,
Labels: map[string]string{
"app": req.App, "tenant": req.Tenant, "env": req.Env, "cluster": req.Cluster,
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": req.App}},
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
RollingUpdate: &appsv1.RollingUpdateDeployment{
MaxSurge: intstr.FromString("25%"),
MaxUnavailable: intstr.FromString("0"),
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": req.App, "tenant": req.Tenant, "env": req.Env, "cluster": req.Cluster,
},
Annotations: map[string]string{
"prometheus.io/scrape": "true",
"prometheus.io/port": "8080",
"prometheus.io/path": "/metrics",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: req.App,
Image: req.Image,
Ports: []corev1.ContainerPort{{ContainerPort: 8080, Name: "http"}},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("512Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
EnvFrom: []corev1.EnvFromSource{
{ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: req.App + "-config"}}},
{SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: req.App + "-secret"}}},
},
}},
},
},
},
}
}
// ReleaseHandler 处理发布请求:经 API 网关拿集群客户端 -> 创建/更新资源 -> 记录指标。
func ReleaseHandler(gw *gateway.ClusterGateway, dyn dynamic.Interface) gin.HandlerFunc {
return func(c *gin.Context) {
var req ReleaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
cs, err := gw.ClientFor(ctx, req.Cluster)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ns := req.Tenant + "-" + req.Env
start := time.Now()
dc := k8s.NewDeployClient(cs)
rc := k8s.NewRolloutClient(cs, dyn)
switch req.Strategy {
case "bluegreen":
// 蓝绿:切流到 green(blue/green Deployment 已由 app-bluegreen.yaml 预置)
if err := rc.ShiftTrafficToGreen(ctx, ns, req.App, "green"); err != nil {
metrics.ReleaseTotal.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, req.Strategy, "failed").Inc()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
case "canary":
// 金丝雀:patch Argo Rollouts setWeight(Rollout 已由 app-canary-rollouts.yaml 预置)
if err := rc.PromoteCanary(ctx, ns, req.App, req.Weight); err != nil {
metrics.ReleaseTotal.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, req.Strategy, "failed").Inc()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
default: // rolling
if _, err := dc.EnsureDeployment(ctx, ns, buildDeployment(req, ns)); err != nil {
metrics.ReleaseTotal.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, req.Strategy, "failed").Inc()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
metrics.ReleaseTotal.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, req.Strategy, "success").Inc()
metrics.ReleaseDuration.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, req.Strategy).Observe(time.Since(start).Seconds())
c.JSON(http.StatusOK, gin.H{"status": "released", "namespace": ns})
}
}
// PodStatusHandler 读取 Pod 就绪状态(Go 调用 K8s 读取状态)。
func PodStatusHandler(gw *gateway.ClusterGateway) gin.HandlerFunc {
return func(c *gin.Context) {
tenant := c.Param("tenant")
env := c.Param("env")
app := c.Param("app")
cluster := c.Query("cluster")
if cluster == "" {
cluster = "cluster-prod"
}
ctx := c.Request.Context()
cs, err := gw.ClientFor(ctx, cluster)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rc := k8s.NewRolloutClient(cs, nil)
ready, total, err := rc.PodStatus(ctx, tenant+"-"+env, app)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ratio := 0.0
if total > 0 {
ratio = float64(ready) / float64(total)
}
metrics.PodReadyRatio.WithLabelValues(tenant, app, env, cluster).Set(ratio)
c.JSON(http.StatusOK, gin.H{"ready": ready, "total": total, "ratio": ratio})
}
}
// BackupHandler 触发备份 Job(Go 调用 K8s 创建 Job)。
func BackupHandler(gw *gateway.ClusterGateway) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
Tenant string `json:"tenant" binding:"required"`
App string `json:"app" binding:"required"`
Env string `json:"env" binding:"required"`
Cluster string `json:"cluster" binding:"required"`
Image string `json:"image"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
cs, err := gw.ClientFor(ctx, req.Cluster)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ns := req.Tenant + "-" + req.Env
img := req.Image
if img == "" {
img = "harbor.example.com/paas-backup:latest"
}
bc := k8s.NewBackupClient(cs)
jobName := fmt.Sprintf("%s-backup-%d", req.App, time.Now().Unix())
if err := bc.TriggerBackupJob(ctx, ns, jobName, img,
[]string{"--app", req.App, "--tenant", req.Tenant}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
metrics.BackupJobLastSuccess.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster).SetToCurrentTime()
c.JSON(http.StatusOK, gin.H{"job": jobName, "status": "triggered"})
}
}
go.mod 与 main.go 把以上组装成可运行服务:
// file: go.mod
module github.com/example/paas-alm
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/prometheus/client_golang v1.19.0
k8s.io/api v0.30.0
k8s.io/apimachinery v0.30.0
k8s.io/client-go v0.30.0
)
// file: main.go
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/client-go/dynamic"
"github.com/example/paas-alm/pkg/gateway"
"github.com/example/paas-alm/pkg/handler"
)
func main() {
gw, err := gateway.NewClusterGateway("/etc/paas/gateway.kubeconfig")
if err != nil {
log.Fatalf("init gateway: %v", err)
}
dyn, err := dynamic.NewForConfig(gw.RESTConfig())
if err != nil {
log.Fatalf("init dynamic client: %v", err)
}
r := gin.Default()
r.POST("/api/v1/releases", handler.ReleaseHandler(gw, dyn))
r.GET("/api/v1/releases/:tenant/:env/:app/pods", handler.PodStatusHandler(gw))
r.POST("/api/v1/backups", handler.BackupHandler(gw))
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
if err := r.Run(":8080"); err != nil {
log.Fatalf("server: %v", err)
}
_ = http.StatusOK
}
分步操作(节选关键命令与 YAML)
# 步骤 1:应用建模(测试环境先行)—— 等价 manifests/tenant-namespace.yaml 的命名空间
apiVersion: paas.example.com/v1
kind: Application
metadata:
name: order-service
namespace: tenant-a-test # 租户 Namespace,强隔离
spec:
env: test # 环境维度
targetCluster: cluster-test # 目标集群
image: harbor.example.com/order-service:1.4.0
replicas: 2
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "1", memory: "1Gi" }
---
# 步骤 2:生产发布需显式开启金丝雀并绑定审批策略
spec:
strategy:
canary:
steps: [ { weight: 5 }, { weight: 20 }, { weight: 50 }, { weight: 100 } ]
approval: required # ★ 生产必须审批
运维 runbook(端到端真实命令)
# 1) 镜像安全扫描(Trivy)作为发布前置闸门:发现 Critical/High 直接非零退出,阻断发布
trivy image --severity CRITICAL,HIGH --exit-code 1 \
--format table harbor.example.com/order-service:1.4.0
# 2) 经平台 API 发起一次金丝雀发布(Go 侧 ReleaseHandler -> PromoteCanary)
curl -XPOST http://paas-gateway/api/v1/releases -H 'Content-Type: application/json' -d '{
"tenant":"tenant-a","app":"order-service","env":"prod",
"cluster":"cluster-prod","image":"harbor.example.com/order-service:1.4.0",
"strategy":"canary","weight":20}'
# 3) 滚动发布状态观察
kubectl -n tenant-a-prod rollout status deployment/order-service
kubectl -n tenant-a-prod rollout history deployment/order-service
# 4) 金丝雀放量 / 暂停(Argo Rollouts)
kubectl -n tenant-a-prod argo rollouts set weight order-service 20
kubectl -n tenant-a-prod argo rollouts abort order-service # 异常暂停放量
# 5) 蓝绿切换(平台 Go 触发;手动等价于改 Service selector)
kubectl -n tenant-a-prod patch service order-service \
-p '{"spec":{"selector":{"app":"order-service","release":"green"}}}'
# 6) 扩缩容 / 定时伸缩(HPA)
kubectl -n tenant-a-prod scale deployment/order-service --replicas=10
kubectl -n tenant-a-prod patch hpa order-service --type merge \
-p '{"spec":{"minReplicas":5}}'
# 7) 手动触发备份 Job(Go BackupHandler 同源)
kubectl -n tenant-a-prod create job order-service-backup-manual \
--from=cronjob/order-service-backup
【测试环境】与【生产环境】差异化步骤
| 环节 | 测试环境 | 生产环境 |
|---|---|---|
| 镜像扫描 | 仅告警,不阻断 | ★ 阻断 Critical/High |
| 发布审批 | 免审批或单级 | ★ 多级审批,不可绕过 |
| 部署策略 | 默认滚动 | 金丝雀 + 自动暂停阈值 |
| 配置注入 | 可用明文占位 | ★ 必须 KMS 加密注入 |
| HPA 上限 | 低副本上限 | 依容量规划放开 |
⚠️ 测试环境虽免审批,但扫描与配置加密的"能力"仍需开启,保证测试数据尽量贴近生产真实形态,避免"测试能跑、生产翻车"。
五、生产环境管控与安全约束
生产环境相比测试环境,在资源、权限、审批、审计上有更严格的约束。下面是「测试 vs 生产 差异化管控」对照表。
| 管控维度 | 测试环境 | 生产环境 |
|---|---|---|
| 资源配额 | 共享小配额,超用即限流 | 独立配额 + LimitRange 硬限制 |
| 命名空间隔离 | 租户间逻辑隔离 | 租户间网络策略 + 配额双重隔离 |
| 镜像来源 | 任意 Harbor 项目 | 仅白名单签名镜像仓库 |
| 镜像扫描 | 告警 | ★ 阻断高危,未过扫禁止发布 |
| 发布审批 | 单级/免审 | ★ 多级审批,审计留痕 |
| 配置加密 | 可明文(占位) | ★ KMS 信封加密,明文不可落盘 |
| 回滚权限 | 研发自助 | 需运维 + 审批双确认 |
| 审计留存 | 90 天 | 90 天以上,不可篡改 |
| 故障熔断 | 手动 | 自动:错误率超阈值暂停放量 |
关键约束说明
- 资源配额:生产 Namespace 设 ResourceQuota 与 LimitRange(见
manifests/tenant-namespace.yaml),防止单应用挤占整集群。 - 权限隔离:研发仅有测试 Namespace 的写权限,生产写权限收敛给集群运维角色。以下 RBAC 即「仅测试写、生产只读」的底线:
# file: manifests/rbac-prod.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tenant-a-prod-dev
namespace: tenant-a-prod
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"] # 研发在生产仅可观测,不可写
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenant-a-prod-dev
namespace: tenant-a-prod
subjects:
- kind: User
name: dev-team@tenant-a
roleRef:
kind: Role
name: tenant-a-prod-dev
apiGroup: rbac.authorization.k8s.io
- 敏感操作审批:任何生产发布、配置变更、回滚都须走审批引擎(
manifests/app-pipeline-approval.yaml中ReleaseApproval),且审批记录进审计。 - 审计规则:所有 ALM 操作写入审计日志,字段含 操作人 / 租户 / 环境 / 集群 / 对象 / 时间,留存 ≥ 90 天。
- 故障熔断:金丝雀放量期间若 Alertmanager 命中错误率 / 延迟阈值,应用模块自动暂停后续步骤并升级告警。
配置加密运行验证 runbook
# 用 kubectl 校验 SealedSecret 已解密为明文 Secret(仅在集群内可见,不落业务 Pod 明文)
kubectl -n tenant-a-prod get secret order-service-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# 期望:仅集群内返回密文解密结果;平台侧存储的始终是 SealedSecret.encryptedData
# 镜像扫描闸门在 CI 中阻断高危漏洞(生产不可关闭)
trivy image --severity CRITICAL --exit-code 1 harbor.example.com/order-service:1.4.0
六、常见生产故障与解决方案
结合多集群与联邦监控场景,列举高频问题。下方 observability/app-recording-rules.yaml 与 observability/app-alert-rules.yaml 把"可观测"固化为规则。
SLO / 资源 recording rules
# file: observability/app-recording-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: app-recording-rules
namespace: monitoring
labels:
role: recording-rules
spec:
groups:
- name: app_slo
interval: 30s
rules:
- record: app_request_success_ratio
expr: |
sum(rate(http_requests_total{code!~"5.."}[5m])) by (tenant,app,env,cluster)
/
sum(rate(http_requests_total[5m])) by (tenant,app,env,cluster)
- record: app_request_latency_p99
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (tenant,app,env,cluster,le))
- record: app_cpu_usage_ratio
expr: |
sum(rate(container_cpu_usage_seconds_total{namespace=~"tenant-.*"}[5m])) by (tenant,app,env,cluster)
/
sum(kube_pod_container_resource_limits{resource="cpu"}) by (tenant,app,env,cluster)
# 平台控制面指标(与 pkg/metrics/metrics.go 上报一致)
- record: paas_release_success_ratio
expr: |
sum(rate(paas_release_total{result="success"}[5m])) by (tenant,app,env,cluster)
/
sum(rate(paas_release_total[5m])) by (tenant,app,env,cluster)
告警规则(5xx / OOM / 就绪 / 熔断)
# file: observability/app-alert-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: app-alert-rules
namespace: monitoring
labels:
role: alert-rules
spec:
groups:
- name: app_errors
rules:
- alert: AppHigh5xxErrorRate
expr: |
(sum(rate(http_requests_total{code=~"5.."}[5m])) by (tenant,app,env,cluster)
/ sum(rate(http_requests_total[5m])) by (tenant,app,env,cluster)) > 0.05
for: 5m
labels:
severity: critical
tenant: "{{ $labels.tenant }}"
env: "{{ $labels.env }}"
annotations:
summary: "应用 {{ $labels.app }} 5xx 错误率超 5%"
- alert: AppOOMKilled
expr: |
increase(kube_pod_container_status_restarts_total{reason="OOMKilled"}[10m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} 发生 OOMKilled"
- alert: AppPodNotReady
expr: |
kube_deployment_status_replicas_available{deployment="order-service"}
< kube_deployment_spec_replicas{deployment="order-service"}
for: 10m
labels:
severity: warning
annotations:
summary: "Deployment {{ $labels.deployment }} 就绪副本不足"
- alert: CanaryErrorBudgetBurn
expr: |
app_request_success_ratio{app="order-service"} < 0.95
for: 3m
labels:
severity: warning
annotations:
summary: "金丝雀版本成功率低于 95%,建议暂停放量"
故障 1:金丝雀放量后错误率飙升
现象:放量到 20% 时 Grafana 租户大盘 error_rate 陡增,Alertmanager 触发 P2 告警。
排查:① 看联邦大盘确认是否仅新版本升高;② 查 Trivy 结论是否为旧镜像已带漏洞;③ 查配置注入是否因加密失败退化为默认值。
优化:确认熔断阈值合理,版本治理一键回滚到上一稳定 Revision(runbook 见下)。
故障 2:镜像扫描卡住导致发不出去
现象:流水线成功但平台长期显示"扫描中"。 排查:① Harbor 与 Trivy 服务连接;② 镜像体积过大超出扫描超时;③ 网关到扫描服务的网络策略。 优化:开启增量扫描,并为大镜像设独立扫描队列。
故障 3:多集群下状态不一致
现象:平台显示已发布,但某集群 Pod 未就绪。 排查:① API 网关到该集群链路;② 目标集群节点资源耗尽;③ 配额 LimitRange 拒绝。 优化:控制器加强调谐重试与状态对齐,联邦指标补充"期望 vs 实际副本"差值告警。
故障处置 runbook
# 回滚到上一版本(版本治理一键回退)
kubectl -n tenant-a-prod rollout undo deployment/order-service
kubectl -n tenant-a-prod rollout undo deployment/order-service --to-revision=3
# 蓝绿秒级回滚:把流量切回 blue
kubectl -n tenant-a-prod patch service order-service \
-p '{"spec":{"selector":{"app":"order-service","release":"blue"}}}'
# 金丝雀异常主动暂停
kubectl -n tenant-a-prod argo rollouts abort order-service
# 查看 Pod 就绪与 OOM(配合 AppPodNotReady / AppOOMKilled 告警)
kubectl -n tenant-a-prod get pods -l app=order-service -o wide
kubectl -n tenant-a-prod get events --sort-by=.lastTimestamp | tail -20
下面这张图给出故障排查的通用流程。
flowchart TD
S[收到生产告警] --> A{是否新版本引入?}
A -->|否| B[查基础设施/集群链路]
A -->|是| C{镜像扫描是否漏过?}
C -->|是| D[紧急回滚+补扫]
C -->|否| E{配置加密是否失败?}
E -->|是| F[重注入密钥并重启]
E -->|否| G[查 HPA/资源配额]
B --> H[恢复后闭环归档审计]
D --> H
F --> H
G --> H这张流程图把"告警 → 定位新版本 → 扫描/加密/资源三路归因 → 回滚或重注入 → 审计归档"串成可复用的处置路径。
自测题与动手练习
自测题
- 应用建模必须绑定的四个维度是什么?它们分别如何影响落地点?
- Trivy 镜像扫描在生产环境的阻断规则是什么?测试环境为何不直接阻断?
- 滚动、蓝绿、金丝雀三种策略各自最适合什么业务场景?
- 应用指标进入联邦时必须携带哪四个标签?缺少会怎样影响 Grafana 与告警?
- 为什么说生产发布的审批"不可绕过"?它与审计留存如何共同满足合规?
动手练习
- 在
tenant-a-testNamespace 下用上面建模 YAML 创建应用,观察联邦大盘是否出现带tenant/app/env/cluster标签的指标。 - 故意给一个含 High 漏洞的镜像打 Tag,验证生产发布被 Trivy 闸门阻断,并记录阻断日志。
- 配置一条金丝雀
steps: [5,20,50,100],在放量到 20% 时手动触发一次回滚,确认版本治理可一键退回。
本章小结
- 应用全生命周期管理模块把"编码到下线"收敛为平台可控、审计可追、安全可阻断的标准流程。
- ★ 镜像扫描、发布审批、配置加密是生产环境的不可删减底线,分别卡住"带病上线"“越权上线"“明文泄露”。
- 所有发布经统一 API 网关落到租户 Namespace,指标带
tenant/app/env/cluster进联邦,实现隔离与分级告警。 - 平台侧 Go 控制面(
pkg/gateway+pkg/k8s+pkg/handler)用 client-go 完成 Deployment/HPA 创建更新、滚动重启、蓝绿切换、金丝雀放量(Argo Rollouts patch)、Pod 状态读取、备份 Job 触发,并通过paas_*指标与联邦观测配置一一对齐。 - 测试环境偏敏捷、生产环境偏管控,差异体现在扫描阻断、审批级别、配置加密与熔断策略。
下一模块我们将进入「中间件托管平台」,看 MySQL / Redis / Kafka 等实例如何以同样的多集群、联邦、加密、审批范式被平台统一托管。