学习目标
学完本章,你应该能够:
- 解释「物理隔离多集群」为何是金融/政企的合规底线,以及生产/预发布/测试/开发四环境如何划分。
- 描述统一 K8s API 网关(反向隧道 / 中心代理,参考 remotedialer 思路)如何收敛多集群 apiserver 访问。
- 完成一次集群注册:导入已有集群、托管其 kubeconfig(KMS 信封加密)、下发租户 Namespace 配额。
- 说清联邦 Prometheus Agent 如何随集群纳管自动接入,指标带
tenant/app/env/cluster标签进中心。 - 处理故障集群的自动摘除与流量隔离,并基于联邦监控定位纳管异常根因。
前置知识:
- 了解 Kubernetes 集群基本组成(apiserver / etcd / kubelet / CNI / CSI)。
- 知道 kubeconfig 文件的作用与
context/cluster/user字段含义。 - 看过本白皮书《〇、全局基础架构约束》与《模块 01 租户与组织权限中心》(Namespace 租户隔离、联邦监控)。
- 了解反向隧道 / WebSocket 长连接的基本思路(无需会写)。
本章你会动手做的事:
- 用
paas-cli cluster register把一套测试 K8s 集群纳管进平台,观察 kubeconfig 被加密托管。 - 在网关侧用
kubectl等价命令跨集群查询 Namespace,验证你无需直连各集群 apiserver。 - 在 Grafana 用
cluster=标签切换,确认不同物理集群指标已汇入同一联邦视图。
一、模块概述与企业商用价值
类比:多集群纳管就像连锁品牌的总部调度中心。每家门店(集群)分布在不同的城市(物理隔离机房),各有自己的门禁和账本;总部不每家都派常驻经理,而是拉一条加密专线与每家店的收银系统(apiserver)对账。平时总部统一下发促销策略(Namespace 配额/部署),某家店断网(故障集群)时总部自动把它从调度名单摘掉,订单转给邻近门店。
金融、政企客户因等保与容灾要求,绝不允许把所有业务塞进一个 K8s 集群。生产、预发布、测试、开发往往分属不同机房甚至不同云。本模块是企业自研 PaaS 的神经中枢——它把分散、异构、物理隔离的 K8s 集群,收敛成「平台上一个统一入口」,让上层模块(权限、应用、监控)无需关心集群在哪。
适用角色:集群运维(注册/巡检/节点池)、平台管理员(环境划分/网关调优)、SRE(故障摘除/流量隔离)。
企业商用价值:
- 统一入口:所有集群经统一 K8s API 网关纳管,上层模块零改造跨集群操作。
- 物理隔离合规:四环境物理隔离,满足等保「生产与非生产分离」要求。
- 连接安全:kubeconfig 永不落明文本地,KMS 信封加密托管,网关侧按需解密下发。
- 可观测一致:纳管即接入联邦 Prometheus Agent,监控口径跨集群统一。
下图展示纳管模块在 PaaS 中的中枢定位:上接各业务模块,下收多套物理隔离集群,横向接监控与权限。
graph TD
A[权限中心] --> G[统一 K8s API 网关]
B[应用生命周期] --> G
C[观测平台] --> G
G --> D[生产集群 物理隔离]
G --> E[预发布集群 物理隔离]
G --> F[测试/开发集群 物理隔离]
G --> H[联邦 Prometheus Agent 随纳管接入]
H --> I[(中心 Thanos 长期存储)]二、细分功能详解(商用生产级)
带「★禁止删减」项为金融/政企交付红线,缺失即不达标。
【基础能力】
① 集群注册(导入已有 / 平台纳管)
支持「导入已有集群」(客户提供 kubeconfig)与「平台纳管新建」(平台自动化初始化)。注册后平台与集群建立受控连接,apiserver 不再对外直暴露。集群在平台侧建模为自定义资源 Cluster(CRD clusters.paas.io),spec.env/spec.endpoint/spec.mode/spec.kubeconfigRef 与 Go 代码字段自洽。
| 注册方式 | 适用场景 | 连接模式 |
|---|---|---|
| 导入已有 | 客户既有集群接入 | 反向隧道(集群侧出连网关) |
| 平台纳管新建 | 平台统一供给 | 中心代理(网关转发) |
下面给出 Cluster CRD 定义(平台侧用 DynamicClient 操作,与 Go cluster.go 字段一致):
# file: deploy/crd-cluster.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: clusters.paas.io
spec:
group: paas.io
names:
kind: Cluster
plural: clusters
singular: cluster
scope: Cluster
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
required: ["spec"]
properties:
spec:
type: object
required: ["env", "endpoint"]
properties:
env:
type: string
description: "production / staging / test / dev"
endpoint:
type: string
description: "apiserver 地址,仅网关可达,不直接对外暴露"
mode:
type: string
enum: ["reverse-tunnel", "center-proxy"]
kubeconfigRef:
type: string
description: "KMS 信封加密后的 kubeconfig Secret 名"
status:
type: object
properties:
phase:
type: string
enum: ["Connected", "Degraded", "Evicted"]
subresources:
status: {}
② 环境划分(★禁止删减:跨集群网络策略)——生产/预发布/测试/开发物理隔离
每套集群标注 env 标签,平台按 env 做调度域隔离;跨环境网络默认不通,确需互通走审批后的专用通道。
③ 集群健康巡检与纳管状态
定时探活 apiserver 健康、etcd 健康、节点 Ready 比例、核心组件(CNI/CSI)状态,纳管状态分:已连接 / degraded / 已摘除。
④ 节点池管理
按机型/可用区划分节点池,给租户 Namespace 绑定节点池亲和性与 ResourceQuota(★禁止删减:租户 Namespace 配额)。
【高级企业增值能力】
⑤ kubeconfig 加密托管(★禁止删减:集群连接加密)
客户 kubeconfig 上传后,平台用 KMS 信封加密(数据密钥 DEK 由 KMS 加密为密文,DEK 明文仅在网关内存使用),明文永不落盘。网关向目标集群发请求时内存解密、用完即焚。
⑥ 集群版本与 CNI/CSI 抽象
屏蔽底层 K8s 小版本差异与不同 CNI(Calico/Cilium)/CSI(云盘/自建存储)差异,向上提供一致接口。
⑦ 故障集群自动摘除与流量隔离
探测到集群连续不可用,网关将其标记为「已摘除」,调度器停止向该集群投放新负载,存量流量按策略隔离/转移,并触发 Alertmanager 告警。
下面功能架构图区分基础与高级能力:
graph LR
A[集群注册
导入/纳管新建] --> B[环境划分
生产/预发布/测试/开发]
B --> C[健康巡检
apiserver/etcd/节点]
B --> D[节点池管理
亲和/配额]
A --> E[kubeconfig 加密托管
KMS 信封加密]
B --> F[版本与 CNI/CSI 抽象]
C --> G[故障自动摘除
流量隔离]
H[联邦 Prometheus Agent 自动接入] --> A三、底层架构联动设计
1. 与多 K8s 集群交互:统一 K8s API 网关收敛 apiserver
平台不把各集群 apiserver 直接暴露给上层模块,而是全部经统一 K8s API 网关。网关采用「反向隧道 / 中心代理」模式(参考 Rancher remotedialer 思路):集群侧 agent 经 WebSocket 长连接主动连回网关,网关作为唯一入口转发所有 K8s API 请求。这样客户防火墙只需放通网关方向,且所有请求在网关层做统一鉴权、限流、审计。
项目结构与文件清单(本章起逐个文件完整展示,全部端到端自洽):
paas-multicluster/ ├── deploy/ │ ├── crd-cluster.yaml # Cluster CRD(DynamicClient 操作对象) │ ├── gateway-deployment.yaml # 统一 K8s API 网关 Deployment │ ├── networkpolicy.yaml # 跨租户/跨环境默认 Deny 网络策略 │ └── tenant-quota.yaml # 注册时随之下发的租户 ResourceQuota ├── platform/ │ ├── main.go # gin 网关:注册 / 多集群代理 / metrics │ ├── go.mod # 模块与依赖 │ └── k8s/ │ ├── registry.go # rest.Config + 多集群路由(网关下发) │ ├── kms.go # KMS 信封加密 kubeconfig(明文不落盘) │ ├── cluster.go # 集群注册:加密托管+Cluster CR+部署Agent │ ├── health.go # 探活 + 故障自动摘除 │ ├── metrics.go # Prometheus 指标(与规则名一致) │ └── audit.go # 审计落库(≥90 天) ├── observability/ │ ├── prometheus-agent.yaml # 集群内 Prometheus Agent(relabel 注入标签) │ ├── prometheus-federation.yaml# 中心联邦拉取 │ ├── recording-rules.yaml │ ├── alert-rules.yaml │ └── alertmanager.yaml └── runbook/ └── cluster-ops.sh # kubectl/helm/shell 运维命令
registry.go 是「Go 调用 K8s」的核心:展示 rest.Config 的三种来源(in-cluster / kubeconfig / 网关下发的多集群配置)、kubernetes.NewForConfig 与 dynamic.NewForConfig,并演示网关按集群 ID 路由到对应 apiserver 连接池。
// file: platform/k8s/registry.go
package k8s
import (
"fmt"
"sync"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// ClusterEntry 网关侧保存的单集群连接(来自注册时 KMS 解密到内存的 kubeconfig)。
type ClusterEntry struct {
ClusterID string
Env string
Endpoint string
Config *rest.Config
Client kubernetes.Interface
Dyn dynamic.Interface
Phase string // Connected / Degraded / Evicted
}
type ClusterRegistry struct {
mu sync.RWMutex
clusters map[string]*ClusterEntry
}
func NewClusterRegistry() *ClusterRegistry {
return &ClusterRegistry{clusters: make(map[string]*ClusterEntry)}
}
// LoadGatewayConfig 加载网关自身(中心控制面)rest.Config。
func LoadGatewayConfig(path string) (*rest.Config, error) {
if path == "" {
return rest.InClusterConfig()
}
return clientcmd.BuildConfigFromFlags("", path)
}
// RegisterCluster 用「KMS 信封解密后的 kubeconfig 字节」构建 rest.Config 并接入网关连接池。
// 明文 kubeconfig 不落盘:调用方从 Secret/内存解密后传入,函数返回后即被 GC。
func (r *ClusterRegistry) RegisterCluster(clusterID, env, endpoint string, kubeconfigBytes []byte) error {
cfg, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigBytes)
if err != nil {
return fmt.Errorf("build rest.Config from decrypted kubeconfig: %w", err)
}
cli, err := kubernetes.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("new clientset: %w", err)
}
dyn, err := dynamic.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("new dynamic client: %w", err)
}
r.mu.Lock()
defer r.mu.Unlock()
r.clusters[clusterID] = &ClusterEntry{
ClusterID: clusterID, Env: env, Endpoint: endpoint,
Config: cfg, Client: cli, Dyn: dyn, Phase: "Connected",
}
return nil
}
// ClientForCluster 网关按集群 ID 路由(反向隧道模式下实际转发到内存解密的 apiserver 连接)。
func (r *ClusterRegistry) ClientForCluster(id string) (*ClusterEntry, error) {
r.mu.RLock()
defer r.mu.RUnlock()
c, ok := r.clusters[id]
if !ok {
return nil, fmt.Errorf("cluster %s not registered", id)
}
return c, nil
}
func (r *ClusterRegistry) SetPhase(id, phase string) {
r.mu.Lock()
defer r.mu.Unlock()
if c, ok := r.clusters[id]; ok {
c.Phase = phase
}
}
// IDs 返回当前已纳管的所有集群 ID(健康巡检循环遍历用)。
func (r *ClusterRegistry) IDs() []string {
r.mu.RLock()
defer r.mu.RUnlock()
ids := make([]string, 0, len(r.clusters))
for id := range r.clusters {
ids = append(ids, id)
}
return ids
}
kms.go 演示 KMS 信封加密:随机 DEK 加密明文,主密钥加密 DEK,密文落盘、DEK 明文仅内存使用(生产替换为云 KMS,接口一致)。
// file: platform/k8s/kms.go
package k8s
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
// MockKMS 模拟云 KMS:真实场景调用 AWS KMS / 腾讯云 KMS 的 Encrypt/Decrypt。
// 这里用固定主密钥演示「数据密钥 DEK 由 KMS 加密为密文,DEK 明文仅内存使用」。
type MockKMS struct {
masterKey []byte
}
func NewMockKMS(masterKey string) *MockKMS {
return &MockKMS{masterKey: []byte(masterKey)}
}
func splitDot(s string) []string {
for i := 0; i < len(s); i++ {
if s[i] == '.' {
return []string{s[:i], s[i+1:]}
}
}
return []string{s}
}
// EnvelopeEncrypt 信封加密:生成随机 DEK,用 DEK 加密明文,再用主密钥加密 DEK。
// 返回格式:base64(主IV+encDEK).base64(数据IV+数据密文)
func (k *MockKMS) EnvelopeEncrypt(plaintext []byte) (string, error) {
dek := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, dek); err != nil {
return "", err
}
block, err := aes.NewCipher(dek)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
iv := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
ct := gcm.Seal(nil, iv, plaintext, nil)
mblock, err := aes.NewCipher(k.masterKey)
if err != nil {
return "", err
}
mgcm, err := cipher.NewGCM(mblock)
if err != nil {
return "", err
}
miv := make([]byte, mgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, miv); err != nil {
return "", err
}
encDEK := mgcm.Seal(nil, miv, dek, nil)
outer := append(miv, encDEK...)
inner := append(iv, ct...)
return base64.StdEncoding.EncodeToString(outer) + "." + base64.StdEncoding.EncodeToString(inner), nil
}
// EnvelopeDecrypt 解密:主密钥解出 DEK,DEK 解出明文;DEK 明文仅函数内使用,用完即焚。
func (k *MockKMS) EnvelopeDecrypt(payload string) ([]byte, error) {
parts := splitDot(payload)
if len(parts) != 2 {
return nil, errors.New("bad envelope payload")
}
outer, err := base64.StdEncoding.DecodeString(parts[0])
if err != nil {
return nil, err
}
inner, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return nil, err
}
mblock, err := aes.NewCipher(k.masterKey)
if err != nil {
return nil, err
}
mgcm, err := cipher.NewGCM(mblock)
if err != nil {
return nil, err
}
ns := mgcm.NonceSize()
if len(outer) < ns {
return nil, errors.New("short outer")
}
miv, encDEK := outer[:ns], outer[ns:]
dek, err := mgcm.Open(nil, miv, encDEK, nil)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(dek)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(inner) < gcm.NonceSize() {
return nil, errors.New("short inner")
}
iv, ct := inner[:gcm.NonceSize()], inner[gcm.NonceSize():]
return gcm.Open(nil, iv, ct, nil)
}
cluster.go 演示完整纳管流程(KMS 加密托管 + Cluster CR + 部署 Prometheus Agent),字段与 crd-cluster.yaml 自洽:
// file: platform/k8s/cluster.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/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)
var clusterGVR = schema.GroupVersionResource{Group: "paas.io", Version: "v1", Resource: "clusters"}
// RegisterCluster 完整纳管:加密托管 kubeconfig -> 建 Cluster CR -> 部署 Agent -> 接入连接池。
func RegisterCluster(ctx context.Context, reg *ClusterRegistry, kms *MockKMS,
gwCli kubernetes.Interface, gwDyn dynamic.Interface,
clusterID, env, endpoint, mode string, kubeconfigPlain []byte) error {
// 1) KMS 信封加密 kubeconfig,仅密文落 Secret(明文不落盘)
cipherText, err := kms.EnvelopeEncrypt(kubeconfigPlain)
if err != nil {
return fmt.Errorf("envelope encrypt kubeconfig: %w", err)
}
sec := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "kubeconfig-" + clusterID, Namespace: "paas-system",
Labels: map[string]string{"paas.io/cluster": clusterID},
},
StringData: map[string]string{"kubeconfig": cipherText},
}
if _, err := gwCli.CoreV1().Secrets("paas-system").Create(ctx, sec, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("store encrypted kubeconfig: %w", err)
}
// 2) 在网关侧建 Cluster CR(DynamicClient)
c := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "paas.io/v1", "kind": "Cluster",
"metadata": map[string]interface{}{"name": clusterID, "labels": map[string]interface{}{"env": env}},
"spec": map[string]interface{}{
"env": env, "endpoint": endpoint, "mode": mode,
"kubeconfigRef": "kubeconfig-" + clusterID,
},
"status": map[string]interface{}{"phase": "Connected"},
}}
if _, err := gwDyn.Resource(clusterGVR).Create(ctx, c, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("create Cluster CR: %w", err)
}
// 3) 解密后的 kubeconfig 接入网关连接池(明文仅内存)
if err := reg.RegisterCluster(clusterID, env, endpoint, kubeconfigPlain); err != nil {
return err
}
// 4) 在该集群部署 Prometheus Agent(用目标集群 client)
entry, err := reg.ClientForCluster(clusterID)
if err != nil {
return err
}
if err := DeployPrometheusAgent(ctx, entry.Client, clusterID, env); err != nil {
return err
}
IncClusterRegister(env, mode)
return nil
}
// DeployPrometheusAgent 在目标集群部署 Agent(Agent 模式只 remote_write,本地不长周期存)。
func DeployPrometheusAgent(ctx context.Context, cli kubernetes.Interface, clusterID, env string) error {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "prom-agent-" + clusterID, Namespace: "paas-monitoring",
Labels: map[string]string{"paas.io/cluster": clusterID},
},
Data: map[string]string{
"prometheus.yml": fmt.Sprintf(`global:
scrape_interval: 30s
external_labels:
cluster: %s
env: %s
remote_write:
- url: http://thanos-receiver.paas-monitoring.svc:19291/api/v1/receive
scrape_configs:
- job_name: kube-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
target_label: tenant
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
`, clusterID, env),
},
}
if _, err := cli.CoreV1().ConfigMaps("paas-monitoring").Create(ctx, cm, metav1.CreateOptions{}); err != nil {
return err
}
replicas := int32(1)
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "prom-agent-" + clusterID, Namespace: "paas-monitoring",
Labels: map[string]string{"paas.io/cluster": clusterID},
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "prom-agent", "paas.io/cluster": clusterID}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "prom-agent", "paas.io/cluster": clusterID}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "agent",
Image: "prom/prometheus:v2.51.0",
Args: []string{"--config.file=/etc/prometheus/prometheus.yml", "--enable-feature=agent"},
VolumeMounts: []corev1.VolumeMount{
{Name: "cfg", MountPath: "/etc/prometheus"},
},
}},
Volumes: []corev1.Volume{{
Name: "cfg",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "prom-agent-" + clusterID},
},
},
}},
},
},
},
}
if _, err := cli.AppsV1().Deployments("paas-monitoring").Create(ctx, dep, metav1.CreateOptions{}); err != nil {
return err
}
return nil
}
health.go 探活与故障自动摘除:
// file: platform/k8s/health.go
package k8s
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// ProbeCluster 探活:apiserver 健康 + 节点 Ready 比例,返回 phase。
func ProbeCluster(ctx context.Context, cli kubernetes.Interface) (string, error) {
nodes, err := cli.CoreV1().Nodes().List(ctx, metav1.ListOptions{Limit: 100})
if err != nil {
return "Degraded", fmt.Errorf("apiserver probe failed: %w", err)
}
ready := 0
for _, n := range nodes.Items {
for _, c := range n.Status.Conditions {
if c.Type == "Ready" && c.Status == "True" {
ready++
}
}
}
if ready == 0 && len(nodes.Items) > 0 {
return "Degraded", nil
}
return "Connected", nil
}
// EvictIfUnhealthy 连续不可用达阈值则标记为已摘除并隔离流量;否则标记 Degraded。
func EvictIfUnhealthy(reg *ClusterRegistry, id string, failCount int) (evicted bool) {
if failCount >= 3 {
reg.SetPhase(id, "Evicted")
return true
}
reg.SetPhase(id, "Degraded")
return false
}
metrics.go 暴露指标(paas_cluster_health / paas_cluster_agent_up / paas_cluster_register_total),与第六章规则名一致:
// file: platform/k8s/metrics.go
package k8s
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
clusterHealthGauge = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "paas_cluster_health",
Help: "集群健康状态:1=Connected 0=Degraded -1=Evicted",
},
[]string{"cluster", "env"},
)
agentUpGauge = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "paas_cluster_agent_up",
Help: "Prometheus Agent 是否在线",
},
[]string{"cluster", "env"},
)
clusterRegisterTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "paas_cluster_register_total",
Help: "集群纳管总数",
},
[]string{"env", "mode"},
)
)
func SetClusterHealth(cluster, env, phase string) {
v := 1.0
if phase == "Degraded" {
v = 0
} else if phase == "Evicted" {
v = -1
}
clusterHealthGauge.WithLabelValues(cluster, env).Set(v)
}
func SetAgentUp(cluster, env string, up bool) {
if up {
agentUpGauge.WithLabelValues(cluster, env).Set(1)
} else {
agentUpGauge.WithLabelValues(cluster, env).Set(0)
}
}
func IncClusterRegister(env, mode string) {
clusterRegisterTotal.WithLabelValues(env, mode).Inc()
}
audit.go 审计落库(≥90 天):
// file: platform/k8s/audit.go
package k8s
import (
"context"
"encoding/json"
"os"
"sync"
"time"
)
// AuditRecord 审计记录(含操作人/对象/动作/集群/时间/来源IP/审批单号)。
type AuditRecord struct {
Timestamp time.Time `json:"ts"`
Operator string `json:"operator"`
Action string `json:"action"`
Cluster string `json:"cluster"`
Target string `json:"target"`
SrcIP string `json:"src_ip"`
Ticket string `json:"ticket"`
}
type AuditSink struct {
mu sync.Mutex
path string
}
func NewAuditSink(path string) *AuditSink { return &AuditSink{path: path} }
func (a *AuditSink) Write(ctx context.Context, rec AuditRecord) error {
rec.Timestamp = time.Now()
b, err := json.Marshal(rec)
if err != nil {
return err
}
a.mu.Lock()
defer a.mu.Unlock()
f, err := os.OpenFile(a.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(append(b, '\n'))
return err
}
// file: platform/go.mod
module paas.example.com
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/prometheus/client_golang v1.18.0
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
)
网关 HTTP 服务(gin):注册集群 + 多集群代理路由 + metrics:
// file: platform/main.go
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"paas.example.com/platform/k8s"
)
func main() {
reg := k8s.NewClusterRegistry()
kms := k8s.NewMockKMS("0123456789abcdef0123456789abcdef") // 真实为 KMS 主密钥,永不落盘
// 网关自身连接(中心控制面:存 Cluster CR / 加密 kubeconfig Secret / 部署 Agent)
gwCfg, err := k8s.LoadGatewayConfig("/etc/paas/gateway-kubeconfig")
if err != nil {
log.Fatalf("load gateway config: %v", err)
}
gwCli, err := kubernetes.NewForConfig(gwCfg)
if err != nil {
log.Fatalf("gateway clientset: %v", err)
}
gwDyn, err := dynamic.NewForConfig(gwCfg)
if err != nil {
log.Fatalf("gateway dynamic client: %v", err)
}
audit := k8s.NewAuditSink("/var/log/paas/cluster-audit.jsonl")
r := gin.Default()
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
// 集群注册(平台管理员 / 集群运维)
r.POST("/api/v1/clusters", func(c *gin.Context) {
var req struct {
ID string `json:"id"`
Env string `json:"env"`
Endpoint string `json:"endpoint"`
Mode string `json:"mode"`
Kubeconfig string `json:"kubeconfig"` // 明文仅请求内存中,立即 KMS 加密
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生产环境注册强制二级审批:header 携带审批单号
if req.Env == "production" && c.GetHeader("X-Approval-Ticket") == "" {
c.JSON(http.StatusForbidden, gin.H{"error": "production register requires approval ticket"})
return
}
if err := k8s.RegisterCluster(c.Request.Context(), reg, kms, gwCli, gwDyn,
req.ID, req.Env, req.Endpoint, req.Mode, []byte(req.Kubeconfig)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = audit.Write(c.Request.Context(), k8s.AuditRecord{
Operator: c.GetHeader("X-User"), Action: "register-cluster",
Cluster: req.ID, Target: "cluster/" + req.ID,
SrcIP: c.ClientIP(), Ticket: c.GetHeader("X-Approval-Ticket"),
})
c.JSON(http.StatusOK, gin.H{"status": "registered"})
})
// 网关多集群代理:按集群 ID 路由到对应 apiserver(反向隧道/中心代理)
r.Any("/proxy/:cluster/*path", func(c *gin.Context) {
id := c.Param("cluster")
entry, err := reg.ClientForCluster(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 真实实现:用 entry.Config 构造 transport 转发 K8s API;此处回显路由命中结果
c.JSON(http.StatusOK, gin.H{
"cluster": entry.ClusterID, "endpoint": entry.Endpoint, "phase": entry.Phase,
})
})
// 健康检查与故障摘除循环(SRE):每 30s 探活,连续失败达阈值则摘除
probeFails := map[string]int{}
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
for _, id := range reg.IDs() {
entry, err := reg.ClientForCluster(id)
if err != nil {
continue
}
phase, perr := k8s.ProbeCluster(context.Background(), entry.Client)
if perr != nil {
probeFails[id]++
if k8s.EvictIfUnhealthy(reg, id, probeFails[id]) {
phase = "Evicted"
} else {
phase = "Degraded"
}
} else {
probeFails[id] = 0
}
k8s.SetClusterHealth(id, entry.Env, phase)
if phase == "Degraded" || phase == "Evicted" {
_ = audit.Write(context.Background(), k8s.AuditRecord{
Operator: "health-probe", Action: "cluster-probe",
Cluster: id, Target: "cluster/" + id, SrcIP: "gateway", Ticket: "AUTO",
})
}
}
}
}()
log.Println("multi-cluster gateway listening on :8080")
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
网关自身部署清单:
# file: deploy/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-api-gateway
namespace: paas-system
labels:
app: k8s-api-gateway
spec:
replicas: 2
selector:
matchLabels:
app: k8s-api-gateway
template:
metadata:
labels:
app: k8s-api-gateway
spec:
containers:
- name: gateway
image: paas/k8s-api-gateway:v1.0.0
args: ["--kubeconfig=/etc/paas/gateway-kubeconfig", "--listen=:8080"]
ports:
- containerPort: 8080
volumeMounts:
- name: kc
mountPath: /etc/paas
readOnly: true
volumes:
- name: kc
secret:
secretName: gateway-kubeconfig # 由 KMS 信封加密托管,挂载即内存解密
网关收敛多集群的架构:
graph TD
A[应用模块] --> G[统一 K8s API 网关]
B[权限中心] --> G
G -->|反向隧道 WebSocket| C[生产集群 agent]
G -->|反向隧道 WebSocket| D[测试集群 agent]
C --> E[生产 apiserver]
D --> F[测试 apiserver]
G -->|内存解密 kubeconfig| E
G -->|内存解密 kubeconfig| F2. 与联邦 Prometheus 监控联动:纳管即接入
集群注册成功的同时,平台在该集群部署 Prometheus Agent(Agent 模式只远程写、本地不存长周期),指标统一带 tenant/app/env/cluster 标签,经中心 Prometheus federation 拉取 + Thanos 长期存储。Grafana 用 cluster= 标签跨集群统一视图,Alertmanager 按 env/cluster 分级路由。Go DeployPrometheusAgent 注入的 cluster/env external_labels 与下列 Agent 清单一致。
graph TD
A[集群A Prometheus Agent] -->|remote_write / federation| C[中心 Prometheus]
B[集群B Prometheus Agent] -->|remote_write / federation| C
C --> D[Thanos Query + Store]
D --> E[(对象存储 长期)]
D --> F[Grafana 按 cluster/env 过滤]
D --> G[Alertmanager 分级告警]
H[纳管时自动注入 cluster/env 标签] --> A
H --> B3. 与 RBAC / 审批 / 审计的联动
- 网关层鉴权复用权限中心的租户/角色模型:请求带
tenant标签,网关校验「该租户是否有权操作目标集群的该 Namespace」。 - 集群摘除、kubeconfig 轮换等高危操作触发审批流,执行后写审计(含审批单号、操作人、集群 ID)。
四、端到端标准操作流程
分角色给出分步操作,并显式区分【测试环境】与【生产环境】。
角色一:集群运维 —— 注册一套测试集群
注册由平台 REST API 触发(main.go 的 POST /api/v1/clusters),后端经 RegisterCluster 完成 KMS 加密托管 + Cluster CR + Agent 部署:
# 步骤 1:准备最小权限 kubeconfig(禁止 cluster-admin)
# 步骤 2:注册,平台自动 KMS 信封加密托管,并部署 Prometheus Agent
curl -X POST https://paas-gateway/api/v1/clusters \
-H "Content-Type: application/json" \
-d '{
"id": "test-cluster-01",
"env": "test",
"endpoint": "https://10.10.10.10:6443",
"mode": "reverse-tunnel",
"kubeconfig": "<base64-or-plaintext-only-in-memory>"
}'
# 步骤 3:注册成功后,平台在集群内生成租户 Namespace 与配额
kubectl --context test-cluster-01 get ns subcompany-a --show-labels
kubectl --context test-cluster-01 get resourcequota -n subcompany-a
对应静态清单(供 helm/排障对照):
# file: deploy/tenant-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-subcompany-a
namespace: subcompany-a
labels:
tenant: subcompany-a
spec:
hard:
requests.cpu: "50"
requests.memory: 200Gi
pods: "100"
跨租户/跨环境默认拒绝网络策略:
# file: deploy/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-cross-tenant
namespace: subcompany-a
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
# 仅允许同租户 namespace 内通信,跨租户/跨环境默认拒绝
ingress:
- from:
- namespaceSelector:
matchLabels:
tenant: subcompany-a
egress:
- to:
- namespaceSelector:
matchLabels:
tenant: subcompany-a
⚠️ 【测试环境】可用自签证书、宽松网络;【生产环境】kubeconfig 必须最小权限、走 TLS 双向认证,且注册动作需二级审批 + 合规基线检查通过后方可纳管(见
main.go的X-Approval-Ticket校验)。
角色二:平台管理员 —— 环境划分与节点池绑定
# 步骤 1:给集群打 env 标签(物理隔离域)
paas-cli cluster label --name prod-cluster-01 --set env=production
# 步骤 2:创建节点池并绑定租户配额
paas-cli nodepool create --cluster prod-cluster-01 --name np-gold --zone az1
paas-cli tenant bind-nodepool --tenant subcompany-a --nodepool np-gold
角色三:SRE —— 故障集群摘除与恢复
【测试环境】可手动 paas-cli cluster drain 演练;【生产环境】摘除由健康巡检自动触发,且摘除动作即时 Alertmanager 告警 + 企业微信通知,恢复需 SRE 确认健康后手动 uncordon。
下面端到端时序图覆盖「注册 → Agent 接入 → 请求经网关 → 故障摘除」:
sequenceDiagram
participant O as 集群运维
participant P as 纳管模块
participant G as K8s API 网关
participant K as 目标集群
participant M as 联邦监控
O->>P: 提交注册申请
P->>G: KMS 加密托管 kubeconfig
G->>K: 建立反向隧道
K-->>G: 隧道就绪
G->>K: 部署 Prometheus Agent
K-->>M: 指标带 cluster/env 标签
P-->>O: 纳管成功
Note over G,K: 巡检发现集群不可用
G->>G: 标记为已摘除 + 流量隔离
G->>M: 触发 Alertmanager 告警五、生产环境管控与安全约束
核心原则:连接加密、环境物理隔离、故障可隔离、纳管可审计。
测试环境 vs 生产环境 差异化管控对照表
| 管控项 | 测试环境 | 生产环境 |
|---|---|---|
| kubeconfig 权限 | 可放宽便于调试 | 强制最小权限 clusterrole |
| 集群注册审批 | 免审批,自助纳管 | 二级审批 + 合规基线检查 |
| 网络通道 | 可自签证书、宽松 | 双向 TLS、专线/加密隧道 |
| 故障摘除 | 可手动演练 drain | 自动摘除 + 即时告警 + 人工确认恢复 |
| 节点池变更 | 随时调整 | 变更需审批,灰度生效 |
| 纳管审计 | ≥ 90 天 | ≥ 90 天,可追溯导出 |
| 跨环境互通 | 测试可临时放通 | 默认不通,需安全审批专用通道 |
| 镜像运行 | 可跳过扫描 | 强制 Trivy 扫描阻断高危(联动安全模块) |
- 集群连接加密:kubeconfig 全程 KMS 信封加密(见
kms.go),网关内存解密、用完即焚,明文不落盘、不进日志。 - 租户 Namespace 配额:注册即按租户 ResourceQuota 下发,杜绝单租户吃满集群。
- 跨集群网络策略:默认 Deny-All,跨环境/跨租户互通走 NetworkPolicy + 审批(见
deploy/networkpolicy.yaml)。 - 敏感操作审批:kubeconfig 轮换、集群摘除、env 变更触发审批流(见
main.go的X-Approval-Ticket校验)。 - 故障熔断:集群 degraded 时网关限流降级,避免雪崩波及健康集群。
六、常见生产故障与解决方案
监控端配置自洽:prometheus-agent.yaml 的 relabel 注入 tenant/app/env/cluster 标签,中心 prometheus-federation.yaml 联邦拉取,与 Go metrics.go 暴露的 paas_cluster_health / paas_cluster_agent_up 组合形成告警。
# file: observability/prometheus-agent.yaml
# 集群内 Prometheus Agent:采集本集群指标,强制注入 tenant/app/env/cluster 标签
apiVersion: v1
kind: ConfigMap
metadata:
name: prom-agent-prod-cluster-01
namespace: paas-monitoring
data:
prometheus.yml: |
global:
scrape_interval: 30s
external_labels:
cluster: prod-cluster-01
env: production
remote_write:
- url: http://thanos-receiver.paas-monitoring.svc:19291/api/v1/receive
scrape_configs:
- job_name: kube-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
target_label: tenant
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- target_label: env
replacement: production
- target_label: cluster
replacement: prod-cluster-01
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: prom-agent-prod-cluster-01
namespace: paas-monitoring
labels:
app: prom-agent
paas.io/cluster: prod-cluster-01
spec:
replicas: 1
selector:
matchLabels:
app: prom-agent
paas.io/cluster: prod-cluster-01
template:
metadata:
labels:
app: prom-agent
paas.io/cluster: prod-cluster-01
spec:
containers:
- name: agent
image: prom/prometheus:v2.51.0
args: ["--config.file=/etc/prometheus/prometheus.yml", "--enable-feature=agent"]
volumeMounts:
- name: cfg
mountPath: /etc/prometheus
volumes:
- name: cfg
configMap:
name: prom-agent-prod-cluster-01
# file: observability/prometheus-federation.yaml
# 中心 Prometheus 联邦拉取各集群 Agent,honor_labels 保留 tenant/env/cluster 源标签
global:
scrape_interval: 30s
scrape_configs:
- job_name: federate-prod
honor_labels: true
metrics_path: /federate
params:
match[]:
- '{__name__=~"paas_.*|kube_.*"}'
static_configs:
- targets: ['prom-agent-prod.paas-monitoring.svc:9090']
- job_name: federate-test
honor_labels: true
metrics_path: /federate
params:
match[]:
- '{__name__=~"paas_.*|kube_.*"}'
static_configs:
- targets: ['prom-agent-test.paas-monitoring.svc:9090']
# file: observability/recording-rules.yaml
groups:
- name: paas_cluster_recording
rules:
- record: paas:cluster:health_ratio
expr: sum(paas_cluster_health) / count(paas_cluster_health)
- record: paas:cluster:agent_up_count
expr: sum by (env) (paas_cluster_agent_up)
# file: observability/alert-rules.yaml
groups:
- name: paas_cluster_alerts
rules:
- alert: ClusterDegraded
expr: paas_cluster_health == 0
for: 5m
labels:
severity: warning
team: paas-sre
annotations:
summary: "集群 {{ $labels.cluster }} 降级,请检查反向隧道/防火墙"
- alert: ClusterEvicted
expr: paas_cluster_health == -1
for: 1m
labels:
severity: critical
team: paas-sre
annotations:
summary: "集群 {{ $labels.cluster }} 已摘除,触发流量隔离"
- alert: PrometheusAgentDown
expr: paas_cluster_agent_up == 0
for: 5m
labels:
severity: critical
annotations:
summary: "集群 {{ $labels.cluster }} Prometheus Agent 离线,联邦监控缺口"
# file: observability/alertmanager.yaml
global:
resolve_timeout: 5m
route:
receiver: default
group_by: ['cluster', 'env']
routes:
- matchers: ['env="production"']
receiver: prod-oncall
continue: true
- matchers: ['severity="critical"']
receiver: crit-page
receivers:
- name: default
webhook_configs:
- url: http://paas-notify.svc/api/v1/alert
- name: prod-oncall
webhook_configs:
- url: http://paas-notify.svc/api/v1/wechat
- name: crit-page
pagerduty_configs:
- routing_key: ${PAGERDUTY_KEY}
故障 1:纳管状态显示 degraded,apiserver 探活失败
- 排查:先查网关到集群的反向隧道是否中断(agent 进程/网络);再查客户侧防火墙是否放通网关出连。对应
paas_cluster_health == 0。 - 优化:agent 心跳超时阈值调优 + 多活网关,避免单点。
故障 2:联邦监控看不到某集群指标
- 排查:查该集群 Prometheus Agent 是否部署成功、remote_write 地址是否指向中心、标签
cluster是否注入;查 Thanos Store 是否挂载对应对象存储。对应paas_cluster_agent_up == 0。 - 优化:纳管流程把「Agent 就绪 + 首条指标到达」作为注册完成的验收项。
故障 3:某租户误占满集群资源,影响其他租户
- 排查:查 ResourceQuota 是否真的下发;查是否有 Pod 未设 requests 导致配额失效。
- 优化:准入控制(OPA/Gatekeeper)强制所有 Pod 设 requests/limits,超配额直接拒绝。
完整运维 runbook(runbook/cluster-ops.sh)覆盖章节四 SOP 与本章排查:
#!/usr/bin/env bash
# file: runbook/cluster-ops.sh
set -euo pipefail
# ---- 章节四 SOP:注册测试集群 ----
curl -X POST https://paas-gateway/api/v1/clusters \
-H "Content-Type: application/json" \
-d '{"id":"test-cluster-01","env":"test","endpoint":"https://10.10.10.10:6443","mode":"reverse-tunnel","kubeconfig":"<in-memory-only>"}'
# 校验 Namespace 与配额
kubectl --context test-cluster-01 get ns subcompany-a --show-labels
kubectl --context test-cluster-01 get resourcequota -n subcompany-a
# ---- 环境划分与节点池 ----
paas-cli cluster label --name prod-cluster-01 --set env=production
paas-cli nodepool create --cluster prod-cluster-01 --name np-gold --zone az1
# ---- 跨集群查询(经网关,无需直连 apiserver)----
curl -s https://paas-gateway/proxy/prod-cluster-01/api/v1/namespaces
# ---- 故障1:查反向隧道/防火墙/agent ----
kubectl --context prod-cluster-01 -n paas-system get pods -l app=cluster-agent -o wide
curl -s 'http://prom-center:9090/api/v1/query?query=paas_cluster_health' | jq '.data.result[] | {cluster:.metric.cluster, health:.value[1]}'
# ---- 故障2:查 Agent 在线情况 ----
curl -s 'http://prom-center:9090/api/v1/query?query=paas_cluster_agent_up' | jq '.data.result[] | {cluster:.metric.cluster, up:.value[1]}'
kubectl --context prod-cluster-01 -n paas-monitoring get deploy -l app=prom-agent
# ---- 故障3:查租户配额与 Pod requests ----
kubectl --context prod-cluster-01 -n subcompany-a get resourcequota -o yaml
kubectl --context prod-cluster-01 -n subcompany-a get pods -o json \
| jq '.items[] | select(.spec.containers[].resources.requests==null) | .metadata.name'
# ---- 查询审计(≥90 天)----
grep '"action":"register-cluster"' /var/log/paas/cluster-audit.jsonl | tail -5
故障排查统一流程:
flowchart TD
A[纳管异常告警] --> B{网关到集群连通?}
B -->|否| C[查反向隧道/防火墙/agent]
B -->|是| D{Agent 指标到达?}
D -->|否| E[查 Agent 部署与 remote_write]
D -->|是| F[查 RBAC/配额/标签]
C --> G[恢复隧道并重试]
E --> G
F --> G
G --> H[回归验证 + 写故障单]自测题与动手练习
5 道自测题
- 统一 K8s API 网关采用什么模式收敛多集群 apiserver?为何客户防火墙只需放通网关方向?
- kubeconfig 加密托管用了哪种加密方式?明文是否会落盘?
- 集群注册时,生产环境与测试环境在审批与证书上有何差异?
- 联邦 Prometheus 下,如何保证不同物理集群的指标汇入同一视图并按
cluster过滤? - 故障集群自动摘除后,存量流量如何处理?恢复需要什么动作?
3 个动手练习
- 用
paas-cli cluster register纳管一套测试集群,验证 kubeconfig 上传后本地文件被标记为「已加密托管」(对照kms.go信封加密)。 - 通过网关(而非直连 apiserver)跨集群
get namespace,确认你无需知道各集群地址(/proxy/:cluster/*path)。 - 在 Grafana 用
cluster=test-cluster-01过滤,确认该集群指标已进联邦视图;手动停掉 Agent,观察告警是否触发(PrometheusAgentDown)。
本章小结
- 多集群纳管是 PaaS 的神经中枢:把物理隔离的 K8s 集群收敛为统一入口。
- 统一 K8s API 网关用反向隧道/中心代理(remotedialer 思路)收敛 apiserver,统一鉴权/限流/审计;Go 用
rest.Config+Clientset/DynamicClient按集群 ID 路由。 - 四环境(生产/预发布/测试/开发)物理隔离,是等保合规底线。
- kubeconfig KMS 信封加密托管(明文仅内存、用完即焚),租户 Namespace 配额随纳管下发,跨集群网络默认隔离。
- 纳管即接入联邦 Prometheus Agent,指标带
tenant/app/env/cluster标签进中心;Go 暴露paas_cluster_health/paas_cluster_agent_up与告警规则名一致;故障集群自动摘除隔离。
下一章《应用全生命周期管理模块》将基于本模块的集群与网关,讲解应用如何被建模、安全发布与弹性伸缩——而这一切都运行在你刚刚纳管的物理隔离集群之上。