租户与组织权限中心

2025-10-15T10:30:00+08:00 | 19分钟阅读 | 更新于 2025-10-15T10:30:00+08:00

@

学习目标

学完本章,你应该能够:

  1. 说清「集团 → 子公司 → 团队 → 项目」四级组织架构在 PaaS 平台里如何映射为租户与 Namespace。
  2. 独立设计一套「Namespace 级 + API 级 + 数据级」三层 RBAC 模型,并落到 K8s Role/RoleBinding。
  3. 把企业既有 LDAP / 企业微信 / OAuth2 身份源接入平台 SSO,并实现账号生命周期同步。
  4. 配置权限继承、委派与定期复审,确保「最小权限」原则长期不被腐化。
  5. 解释租户权限标签如何注入监控指标,实现联邦 Prometheus 下的多租户数据隔离与审计留痕。

前置知识

  • 理解 Kubernetes Namespace、Role、RoleBinding、ServiceAccount 基本概念。
  • 了解 RBAC(Role-Based Access Control)与「最小权限原则」。
  • 知道 SSO、LDAP、OAuth2、SAML 的大致含义(不需要会部署)。
  • 看过本白皮书《〇、全局基础架构约束》:多集群经统一 K8s API 网关纳管、租户 Namespace 强隔离、联邦 Prometheus + Thanos。

本章你会动手做的事

  • 用平台控制台新建一个子公司租户,并自动生成其专属 Namespace 与配额。
  • 给一名开发同学分配「只读 + 仅自己项目」的细粒度角色,验证其越权访问被拒。
  • 在 Grafana 里用 tenant= 标签过滤,确认你只能看到自己租户的指标曲线。

一、模块概述与企业商用价值

类比:租户隔离就像一栋写字楼按公司隔出独立办公室。每间办公室有独立门锁(Namespace 配额与网络策略),独立电表(指标带 tenant 标签),物业(平台)看不到租户内部文件,但能按办公室统计水电。组织权限中心,就是这栋楼的「门禁 + 人事系统」:谁属于哪家公司、能开哪扇门、做了什么都要登记在册。

在金融、政企客户的多租户场景下,一套 PaaS 平台往往同时承载十几个子公司、上百个团队。如果没有统一的权限中心,就会上演「A 公司误删 B 公司数据库」「审计时发现操作人无法溯源」的事故。本模块是企业自研 PaaS 的信任基石——它回答三个问题:你是谁(身份)、你能干什么(授权)、你干了什么(审计)。

适用角色:平台管理员(建租户/配角色)、安全合规员(审权限/查审计)、子公司管理员(管自己团队)、普通开发者(用被分配的权限)。

企业商用价值:

  • 强隔离合规:租户间资源、网络、数据、监控四重隔离,满足等保/银保监对租户隔离的硬性要求。
  • 统一身份:对接客户既有 4A/AD/LDAP,避免重复建账号、账号流失难回收。
  • 可审计:所有授权与操作留痕 90 天以上,监管检查可一键导出。
  • 不可绕过的审批:高危操作必须走审批流,权限中心与审批引擎联动,授权即放行。

下面这张图说明权限中心在整套 PaaS 中的位置:它上接身份源,下接 K8s API 网关与统一监控,横向联动审批与审计。

graph TD
    A[身份源
LDAP / 企业微信 / OAuth2] --> B[租户与组织权限中心] B --> C[统一 K8s API 网关] C --> D[生产集群 Namespace] C --> E[测试集群 Namespace] B --> F[审批工作流引擎] B --> G[审计日志中心 90天+] B --> H[联邦 Prometheus 指标打标] H --> I[(Thanos 长期存储)]

二、细分功能详解(商用生产级)

本模块功能分【基础能力】与【高级企业增值能力】两层。带「★禁止删减」的项为金融/政企交付红线,缺失即不达标。

【基础能力】

① 多租户组织架构(集团 → 子公司 → 团队 → 项目)

平台以「组织树」建模企业结构。集团是根节点,子公司是租户(Tenant),团队是租户下的子分组,项目最终映射为一个或多个 K8s Namespace。租户在平台上建模为自定义资源 Tenant(CRD tenants.paas.io),其 spec.env 决定命名域与物理集群路由。

层级平台概念K8s 映射说明
L0集团(Platform)平台全局元数据跨租户管理与计量
L1子公司(Tenant)★禁止删减一组 Namespace + 配额租户强隔离边界
L2团队(Team)Namespace 内 RBAC 分组团队内协作
L3项目(Project)单个 Namespace部署单元

⚠️ 租户(Tenant)边界一旦创建就禁止用配置把两个租户的 Namespace 放进同一网络平面或共享 Secret。这是★禁止删减项,违反等于击穿隔离。

下面给出 Tenant 的 CRD 定义(平台侧用 DynamicClient 操作,与 Go 代码字段自洽):

# file: deploy/crd-tenant.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: tenants.paas.io
spec:
  group: paas.io
  names:
    kind: Tenant
    listKind: TenantList
    plural: tenants
    singular: tenant
  scope: Cluster
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: ["env"]
              properties:
                env:
                  type: string
                  description: "production / staging / test / dev"
                quota:
                  type: object
                  properties:
                    cpu:
                      type: string
                    memory:
                      type: string
                    namespaceCount:
                      type: integer
                      minimum: 1
            status:
              type: object
              properties:
                phase:
                  type: string
      subresources:
        status: {}

② RBAC 三层权限模型(★禁止删减:最小权限 / 审计留痕)

  • Namespace 级:控制对哪些 Namespace 的哪些资源(Pod/Deployment/Secret)有何种动词(get/list/watch/create/delete)。
  • API 级:控制对平台 OpenAPI(如「扩缩容」「发布」)的调用权,独立于 K8s 动词。
  • 数据级:通过标签选择器约束可见数据范围,例如 project=order 的开发者只能看自己项目的指标与日志。

下列清单给出「项目级只读 Role + 租户管理员 ClusterRole + RoleBinding」完整落地(可 kubectl apply):

# file: deploy/tenant-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: order-readonly
  namespace: subcompany-a-order
  labels:
    tenant: subcompany-a
    project: order
rules:
- apiGroups: [""]
  resources: ["pods", "configmaps", "services"]
  verbs: ["get", "list", "watch"]
---
# 集群级只读 ClusterRole(供租户管理员查看节点/PV 等共享资源)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: tenant-cluster-readonly
  labels:
    paas.io/managed: "true"
rules:
- apiGroups: [""]
  resources: ["nodes", "persistentvolumes"]
  verbs: ["get", "list", "watch"]
---
# 把项目只读 Role 绑定到开发者用户(数据级:仅 project=order 的 namespace)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: order-ro-dev-lisi
  namespace: subcompany-a-order
  labels:
    tenant: subcompany-a
subjects:
- kind: User
  name: dev-lisi
  apiGroup: rbac.authorization.k8s.io
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: order-readonly

③ SSO / LDAP / 企业微信 / OAuth2 集成

支持对接客户既有身份源,账号开通/停用与 HR 系统同步,离职即冻结。平台签发 HMAC 令牌(生产替换为 OIDC ID Token),所有 API 必经鉴权中间件。

【高级企业增值能力】

④ 权限继承与委派、权限定期复审

  • 子公司管理员可将权限委派给团队管理员(Delegation),但委派范围不得超过自身授权(不能「授权超出自己拥有的权限」)。
  • 平台强制每季度权限复审:系统列出长期未使用、权限过大的账号,推送给租户管理员确认回收。

⑤ 操作审计留痕(★禁止删减)

所有「授权变更 / 角色分配 / 委派」动作写入审计日志,字段含:操作人、被操作对象、动作、租户、时间、来源 IP、审批单号。留存 ≥ 90 天,支持按租户/时间/操作人检索与导出。

下面用功能架构图展示模块内部能力组成与分类。

graph LR
    A[身份集成层
LDAP/OAuth2/企业微信] --> B[组织树管理
集团-子公司-团队-项目] B --> C[RBAC 引擎
Namespace/API/数据三级] C --> D[委派与继承] C --> E[权限复审调度] C --> F[审计留痕 90天+] G[审批流接入] --> C H[指标标签注入] --> C

三、底层架构联动设计

1. 与多 K8s 集群交互:权限如何落到 Namespace

用户在平台侧的「角色分配」不会直接改 K8s,而是经统一 K8s API 网关收敛后,由平台控制面在目标集群生成对应的 Role/RoleBinding。这样多套物理隔离集群无需每个都手动配 RBAC,且网关层可做统一鉴权与限流。

项目结构与文件清单(本章起逐个文件完整展示,全部端到端自洽):

paas-tenant-rbac/
├── deploy/
│   ├── crd-tenant.yaml          # Tenant CRD(平台侧 DynamicClient 操作对象)
│   ├── tenant-namespace.yaml    # Namespace + tenant/env 标签
│   ├── tenant-rbac.yaml         # Role/RoleBinding/ClusterRole
│   ├── tenant-networkpolicy.yaml# 默认 Deny-All 网络策略
│   └── tenant-quota.yaml        # ResourceQuota + LimitRange
├── platform/
│   ├── main.go                  # gin 服务:租户/角色 REST API
│   ├── go.mod                   # 模块与依赖
│   ├── auth/sso.go              # SSO 令牌签发与鉴权中间件
│   └── k8s/
│       ├── registry.go          # rest.Config + 多集群路由(网关下发)
│       ├── tenant.go            # Tenant→Namespace/Quota/NetPol 编排
│       ├── rbac.go              # Role/RoleBinding/ClusterRole 增删改查
│       ├── metrics.go           # Prometheus 指标(与规则名一致)
│       └── audit.go             # 审计落库(≥90 天)
├── observability/
│   ├── prometheus-federation.yaml  # 中心联邦拉取
│   ├── prometheus-agent-scrape.yaml# 集群 Agent relabel 注入标签
│   ├── recording-rules.yaml
│   ├── alert-rules.yaml
│   └── alertmanager.yaml
└── runbook/
    └── tenant-ops.sh            # kubectl/helm/shell 运维命令

registry.go 是「Go 调用 K8s」的核心:展示 rest.Config 的三种来源(in-cluster / kubeconfig / 网关下发的多集群配置)、kubernetes.NewForConfigdynamic.NewForConfig,以及按租户 env 路由到对应物理集群的网关逻辑。

// file: platform/k8s/registry.go
package k8s

import (
	"fmt"
	"strings"
	"sync"

	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

// ClusterEntry 保存网关下发的单集群连接(来自统一 K8s API 网关)。
// 实际生产中 Config 由网关从 KMS 信封加密托管的 kubeconfig 在内存解密注入,
// 客户端不读取明文 kubeconfig。
type ClusterEntry struct {
	ClusterID string
	Env       string
	Config    *rest.Config
	Client    kubernetes.Interface
	Dyn       dynamic.Interface
}

type ClusterRegistry struct {
	mu       sync.RWMutex
	clusters map[string]*ClusterEntry
}

// NewClusterRegistry 从网关下发的多集群 kubeconfig 初始化所有集群连接。
// kubeconfigPath 为空时使用 in-cluster 配置(中心控制面运行于网关 Pod 内)。
func NewClusterRegistry(kubeconfigPath string) (*ClusterRegistry, error) {
	reg := &ClusterRegistry{clusters: make(map[string]*ClusterEntry)}

	var baseCfg *rest.Config
	var err error
	if kubeconfigPath == "" {
		baseCfg, err = rest.InClusterConfig()
	} else {
		loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
		loadingRules.ExplicitPath = kubeconfigPath
		baseCfg, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
			loadingRules, &clientcmd.ConfigOverrides{}).ClientConfig()
	}
	if err != nil {
		return nil, fmt.Errorf("build base rest.Config: %w", err)
	}

	rawCfg, err := clientcmd.LoadFromFile(kubeconfigPath)
	if err != nil {
		return nil, fmt.Errorf("load kubeconfig: %w", err)
	}
	for name, ctx := range rawCfg.Contexts {
		cfg := rest.CopyConfig(baseCfg)
		cfg.Host = rawCfg.Clusters[ctx.Cluster].Server
		// 真实场景注入 CA + 客户端证书;此处复用 baseCfg 的 TLS。
		cli, err := kubernetes.NewForConfig(cfg)
		if err != nil {
			return nil, fmt.Errorf("new clientset for %s: %w", name, err)
		}
		dyn, err := dynamic.NewForConfig(cfg)
		if err != nil {
			return nil, fmt.Errorf("new dynamic client for %s: %w", name, err)
		}
		reg.clusters[name] = &ClusterEntry{
			ClusterID: name,
			Env:       envFromContextName(name),
			Config:    cfg,
			Client:    cli,
			Dyn:       dyn,
		}
	}
	return reg, nil
}

// envFromContextName 由网关下发的 context 名推导环境(生产实践中来自平台 CMDB 元数据)。
func envFromContextName(name string) string {
	switch {
	case strings.Contains(name, "prod"):
		return "production"
	case strings.Contains(name, "staging"):
		return "staging"
	case strings.Contains(name, "test"):
		return "test"
	default:
		return "dev"
	}
}

// ClientAndDynForTenant 按租户环境路由到对应物理集群(生产租户只能命中 production 集群,跨环境默认拒绝)。
func (r *ClusterRegistry) ClientAndDynForTenant(tenant, env string) (kubernetes.Interface, dynamic.Interface, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	for _, c := range r.clusters {
		if c.Env == env {
			return c.Client, c.Dyn, nil
		}
	}
	return nil, nil, fmt.Errorf("no cluster routed for tenant=%s env=%s", tenant, env)
}

// ClientForCluster 按集群 ID 取连接(排障/跨集群查询用)。
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
}

tenant.go 演示 DynamicClient 操作 Tenant CR + Clientset 落地 Namespace/ResourceQuota/LimitRange/NetworkPolicy,字段与上面 CRD 完全一致:

// file: platform/k8s/tenant.go
package k8s

import (
	"context"
	"fmt"

	corev1 "k8s.io/api/core/v1"
	netv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"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 tenantGVR = schema.GroupVersionResource{Group: "paas.io", Version: "v1", Resource: "tenants"}

func mustQ(s string) resource.Quantity {
	q, _ := resource.ParseQuantity(s)
	return q
}

// ReconcileTenant 在目标集群创建 Tenant CR + Namespace + 配额 + 默认 Deny-All 网络策略。
func ReconcileTenant(ctx context.Context, cli kubernetes.Interface, dyn dynamic.Interface,
	tenant, env, cpu, mem string, nsCount int) error {

	// 1) 平台侧 CRD 落一条 Tenant 记录(DynamicClient 演示)
	t := &unstructured.Unstructured{Object: map[string]interface{}{
		"apiVersion": "paas.io/v1",
		"kind":       "Tenant",
		"metadata": map[string]interface{}{
			"name":  tenant,
			"labels": map[string]interface{}{"tenant": tenant, "env": env},
		},
		"spec": map[string]interface{}{
			"env": env,
			"quota": map[string]interface{}{
				"cpu":            cpu,
				"memory":         mem,
				"namespaceCount": nsCount,
			},
		},
		"status": map[string]interface{}{"phase": "Provisioning"},
	}}
	if _, err := dyn.Resource(tenantGVR).Create(ctx, t, metav1.CreateOptions{}); err != nil {
		return fmt.Errorf("create Tenant CR: %w", err)
	}

	// 2) 创建租户 Namespace(带 tenant/env 标签,用于指标隔离)
	ns := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name:   tenant,
			Labels: map[string]string{"tenant": tenant, "env": env},
		},
	}
	if _, err := cli.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); err != nil {
		return fmt.Errorf("create namespace: %w", err)
	}

	// 3) 配额 + 限额范围 + 默认拒绝网络策略
	if err := applyResourceQuota(ctx, cli, tenant, cpu, mem); err != nil {
		return err
	}
	if err := applyLimitRange(ctx, cli, tenant); err != nil {
		return err
	}
	if err := applyDefaultDenyNetworkPolicy(ctx, cli, tenant); err != nil {
		return err
	}
	return nil
}

func applyResourceQuota(ctx context.Context, cli kubernetes.Interface, ns, cpu, mem string) error {
	q := &corev1.ResourceQuota{
		ObjectMeta: metav1.ObjectMeta{
			Name: "tenant-quota", Namespace: ns,
			Labels: map[string]string{"tenant": ns},
		},
		Spec: corev1.ResourceQuotaSpec{
			Hard: corev1.ResourceList{
				corev1.ResourceRequestsCPU:    mustQ(cpu),
				corev1.ResourceRequestsMemory: mustQ(mem),
			},
		},
	}
	_, err := cli.CoreV1().ResourceQuotas(ns).Create(ctx, q, metav1.CreateOptions{})
	return err
}

func applyLimitRange(ctx context.Context, cli kubernetes.Interface, ns string) error {
	lr := &corev1.LimitRange{
		ObjectMeta: metav1.ObjectMeta{Name: "tenant-limitrange", Namespace: ns},
		Spec: corev1.LimitRangeSpec{
			Limits: []corev1.LimitRangeItem{
				{
					Type: corev1.LimitTypeContainer,
					Default: corev1.ResourceList{
						corev1.ResourceCPU:    mustQ("500m"),
						corev1.ResourceMemory: mustQ("512Mi"),
					},
					DefaultRequest: corev1.ResourceList{
						corev1.ResourceCPU:    mustQ("100m"),
						corev1.ResourceMemory: mustQ("128Mi"),
					},
					Max: corev1.ResourceList{
						corev1.ResourceCPU:    mustQ("4"),
						corev1.ResourceMemory: mustQ("4Gi"),
					},
				},
			},
		},
	}
	_, err := cli.CoreV1().LimitRanges(ns).Create(ctx, lr, metav1.CreateOptions{})
	return err
}

func applyDefaultDenyNetworkPolicy(ctx context.Context, cli kubernetes.Interface, ns string) error {
	ingress := netv1.PolicyTypeIngress
	egress := netv1.PolicyTypeEgress
	np := &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name: "default-deny-all", Namespace: ns,
			Labels: map[string]string{"tenant": ns},
		},
		Spec: netv1.NetworkPolicySpec{
			PodSelector: metav1.LabelSelector{},
			PolicyTypes: []netv1.PolicyType{ingress, egress},
		},
	}
	_, err := cli.NetworkingV1().NetworkPolicies(ns).Create(ctx, np, metav1.CreateOptions{})
	return err
}

rbac.go 演示 Role/RoleBinding/ClusterRole 的增删改查(Clientset):

// file: platform/k8s/rbac.go
package k8s

import (
	"context"
	"fmt"

	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

// CreateProjectReadonlyRole 创建「仅 project=<project> 只读」的 Role(数据级用标签选择器约束)。
func CreateProjectReadonlyRole(ctx context.Context, cli kubernetes.Interface,
	namespace, project, roleName string) error {
	role := &rbacv1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name:      roleName,
			Namespace: namespace,
			Labels:    map[string]string{"tenant": namespace, "project": project},
		},
		Rules: []rbacv1.PolicyRule{
			{
				APIGroups: []string{""},
				Resources: []string{"pods", "configmaps", "services"},
				Verbs:     []string{"get", "list", "watch"},
			},
		},
	}
	_, err := cli.RbacV1().Roles(namespace).Create(ctx, role, metav1.CreateOptions{})
	if err != nil {
		return fmt.Errorf("create role: %w", err)
	}
	return nil
}

// BindRoleToUser 把 Role 绑定到用户(或 ServiceAccount)。
func BindRoleToUser(ctx context.Context, cli kubernetes.Interface,
	namespace, roleName, user, bindingName string) error {
	rb := &rbacv1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{Name: bindingName, Namespace: namespace},
		Subjects: []rbacv1.Subject{
			{Kind: "User", Name: user, APIGroup: "rbac.authorization.k8s.io"},
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: "rbac.authorization.k8s.io",
			Kind:     "Role",
			Name:     roleName,
		},
	}
	_, err := cli.RbacV1().RoleBindings(namespace).Create(ctx, rb, metav1.CreateOptions{})
	if err != nil {
		return fmt.Errorf("create rolebinding: %w", err)
	}
	return nil
}

// CreateClusterReadOnlyRole 创建集群级 ClusterRole(租户管理员查看节点/PV)。
func CreateClusterReadOnlyRole(ctx context.Context, cli kubernetes.Interface, name string) error {
	cr := &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{Name: name, Labels: map[string]string{"paas.io/managed": "true"}},
		Rules: []rbacv1.PolicyRule{
			{APIGroups: []string{""}, Resources: []string{"nodes", "persistentvolumes"}, Verbs: []string{"get", "list", "watch"}},
		},
	}
	_, err := cli.RbacV1().ClusterRoles().Create(ctx, cr, metav1.CreateOptions{})
	if err != nil {
		return fmt.Errorf("create clusterrole: %w", err)
	}
	return nil
}

metrics.go 暴露的指标名必须与 Prometheus 规则完全一致(详见第六章);audit.go 落库审计(≥90 天留存):

// file: platform/k8s/metrics.go
package k8s

import (
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
	rbacRoleBindingTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "paas_rbac_rolebinding_total",
			Help: "租户 RoleBinding 创建总数(标签需与 Prometheus 规则一致)",
		},
		[]string{"tenant", "env", "app"},
	)
	tenantProvisionSeconds = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "paas_tenant_provision_duration_seconds",
			Help:    "租户 Namespace/配额下发耗时",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"tenant", "env"},
	)
	rbacSyncErrorsTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "paas_rbac_sync_errors_total",
			Help: "RBAC 经网关同步失败次数",
		},
		[]string{"tenant", "cluster"},
	)
)

func IncRoleBinding(tenant, env, app string) {
	rbacRoleBindingTotal.WithLabelValues(tenant, env, app).Inc()
}
func ObserveProvision(tenant, env string, secs float64) {
	tenantProvisionSeconds.WithLabelValues(tenant, env).Observe(secs)
}
func IncSyncError(tenant, cluster string) {
	rbacSyncErrorsTotal.WithLabelValues(tenant, cluster).Inc()
}
// 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"`
	Tenant    string    `json:"tenant"`
	Target    string    `json:"target"`
	SrcIP     string    `json:"src_ip"`
	Ticket    string    `json:"ticket"`
}

// AuditSink 审计落库(演示写 JSONL 文件,生产写入 Elasticsearch/对象存储,留存 ≥90 天)。
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
}

auth/sso.go 签发与校验令牌(生产替换为 OIDC):

// file: platform/auth/sso.go
package auth

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
)

// IssueToken 为通过 SSO/LDAP 校验的用户签发平台令牌(演示:HMAC 签名 tenant|user|ts)。
// 生产应换成 OIDC ID Token / 企业微信 code2session,并带短过期与吊销。
func IssueToken(tenant, user, secret string) string {
	ts := time.Now().Unix()
	body := tenant + "|" + user + "|" + strconv.FormatInt(ts, 10)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(body))
	return body + "|" + hex.EncodeToString(mac.Sum(nil))
}

// AuthMiddleware 校验 Authorization: Bearer <token>,并把 tenant/user 注入上下文。
func AuthMiddleware(secret string) gin.HandlerFunc {
	return func(c *gin.Context) {
		h := c.GetHeader("Authorization")
		parts := strings.SplitN(h, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"})
			return
		}
		fields := strings.Split(parts[1], "|")
		if len(fields) != 4 {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "bad token"})
			return
		}
		body := fields[0] + "|" + fields[1] + "|" + fields[2]
		mac := hmac.New(sha256.New, []byte(secret))
		mac.Write([]byte(body))
		if hex.EncodeToString(mac.Sum(nil)) != fields[3] {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
			return
		}
		c.Set("tenant", fields[0])
		c.Set("user", fields[1])
		c.Next()
	}
}
// 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
)

权限中心经网关下发 RBAC 的时序:

sequenceDiagram
    participant U as 用户/SSO
    participant P as 权限中心
    participant G as 统一 K8s API 网关
    participant K as 目标集群 apiserver
    U->>P: 登录并请求分配角色
    P->>P: 校验最小权限与委派范围
    P->>G: 下发 RoleBinding 创建指令
    G->>G: 鉴权 + 限流 + 审计埋点
    G->>K: 在租户 Namespace 创建 RoleBinding
    K-->>G: 确认
    G-->>P: 成功
    P->>P: 写审计日志

2. 与联邦 Prometheus 监控联动:权限标签注入指标

租户、角色、项目等元数据在指标采集时被注入为标签(含 tenant/app/env/cluster)。联邦架构下,各集群 Prometheus Agent 采集本地指标,中心 Prometheus 通过 federation 拉取,Thanos 做长期存储。Grafana 按登录用户的 tenant 标签过滤大盘,实现「看得到自己的、看不到别人的」。Go 上报的 paas_rbac_rolebinding_total{tenant,env,app} 经 Agent relabel 补 cluster 后汇入联邦视图。

graph TD
    A[集群A Prometheus Agent] -->|federation 拉取| C[中心 Prometheus]
    B[集群B Prometheus Agent] -->|federation 拉取| C
    C --> D[Thanos Query]
    D --> E[(对象存储 长期)]
    D --> F[Grafana 多租户大盘]
    F -->|按 tenant 标签过滤| G[租户A 视图]
    F -->|按 tenant 标签过滤| H[租户B 视图]
    I[权限中心注入 tenant/app/env/cluster 标签] --> A
    I --> B

3. 与 RBAC / 审批 / 审计的联动

  • 审批联动:删除 Namespace、提权等高敏操作,权限中心先拦截,触发审批流;审批通过才在网关侧真正下发。
  • 审计联动:每次授权变更都带审批单号写审计,形成「申请→审批→执行→留痕」闭环,不可绕过。

四、端到端标准操作流程

下面分别给三类角色的分步操作,并显式区分【测试环境】与【生产环境】差异。所有 K8s 操作均经统一 K8s API 网关,平台代码即第三章的 ReconcileTenant / rbac.go

角色一:平台管理员 —— 新建子公司租户并分配租户管理员

租户创建由平台 REST API 触发(main.goPOST /api/v1/tenants),后端按 envClientAndDynForTenant 路由到目标物理集群:

// file: platform/main.go
package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"paas.example.com/platform/auth"
	"paas.example.com/platform/k8s"
)

func main() {
	reg, err := k8s.NewClusterRegistry("/etc/paas/gateway-kubeconfig")
	if err != nil {
		log.Fatalf("init cluster registry: %v", err)
	}
	audit := k8s.NewAuditSink("/var/log/paas/audit.jsonl")

	r := gin.Default()
	r.GET("/metrics", gin.WrapH(promhttp.Handler()))
	r.Use(auth.AuthMiddleware("change-me-in-prod"))

	// 新建租户(平台管理员)
	r.POST("/api/v1/tenants", func(c *gin.Context) {
		var req struct {
			Name string `json:"name"`
			Env  string `json:"env"`
			CPU  string `json:"cpu"`
			Mem  string `json:"memory"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		// 按租户 env 路由到对应物理集群(网关多集群路由)
		cli, dyn, err := reg.ClientAndDynForTenant(req.Name, req.Env)
		if err != nil {
			c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
			return
		}
		start := time.Now()
		if err := k8s.ReconcileTenant(c.Request.Context(), cli, dyn, req.Name, req.Env, req.CPU, req.Mem, 20); err != nil {
			k8s.IncSyncError(req.Name, req.Env)
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		k8s.ObserveProvision(req.Name, req.Env, time.Since(start).Seconds())
		k8s.IncRoleBinding(req.Name, req.Env, "tenant-provisioner")
		_ = audit.Write(c.Request.Context(), k8s.AuditRecord{
			Operator: c.GetString("user"), Action: "create-tenant",
			Tenant: req.Name, Target: "namespace/" + req.Name,
			SrcIP: c.ClientIP(), Ticket: "AUTO-" + req.Name,
		})
		c.JSON(http.StatusOK, gin.H{"status": "provisioned"})
	})

	// 给项目分配只读角色(子公司管理员)
	r.POST("/api/v1/projects/:ns/readonly", func(c *gin.Context) {
		ns := c.Param("ns")
		var req struct {
			User    string `json:"user"`
			Project string `json:"project"`
			Env     string `json:"env"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		cli, _, err := reg.ClientAndDynForTenant(c.GetString("tenant"), req.Env)
		if err != nil {
			c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
			return
		}
		role := "order-readonly"
		if err := k8s.CreateProjectReadonlyRole(c.Request.Context(), cli, ns, req.Project, role); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if err := k8s.BindRoleToUser(c.Request.Context(), cli, ns, role, req.User, "ro-"+req.User); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		k8s.IncRoleBinding(ns, c.GetString("tenant"), req.Project)
		_ = audit.Write(c.Request.Context(), k8s.AuditRecord{
			Operator: c.GetString("user"), Action: "grant-readonly",
			Tenant: ns, Target: "rolebinding/ro-" + req.User,
			SrcIP: c.ClientIP(), Ticket: "AUTO",
		})
		c.JSON(http.StatusOK, gin.H{"status": "bound"})
	})

	log.Println("tenant & org RBAC platform listening on :8080")
	if err := r.Run(":8080"); err != nil {
		log.Fatal(err)
	}
}

对应的静态清单(供 helm/kubectl 幂等落地与排障对照):

# file: deploy/tenant-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: subcompany-a
  labels:
    tenant: subcompany-a
    env: production
    istio-injection: enabled
# file: deploy/tenant-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-quota
  namespace: subcompany-a
  labels:
    tenant: subcompany-a
spec:
  hard:
    requests.cpu: "200"
    requests.memory: 800Gi
    limits.cpu: "400"
    limits.memory: 1600Gi
    pods: "200"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-limitrange
  namespace: subcompany-a
spec:
  limits:
  - type: Container
    default:
      cpu: 500m
      memory: 512Mi
    defaultRequest:
      cpu: 100m
      memory: 128Mi
    max:
      cpu: 4
      memory: 4Gi
# 步骤 2:平台自动在纳管的生产集群创建专属 Namespace(管理员无需手敲 kubectl)
# 等价 REST 调用(token 由 SSO 签发)
curl -X POST https://paas-gateway/api/v1/tenants \
  -H "Authorization: Bearer $(paas-cli token issue --user zhangsan --tenant platform)" \
  -d '{"name":"subcompany-a","env":"production","cpu":"200","memory":"800Gi"}'

# 步骤 3:分配租户管理员角色(Namespace 级 + API 级)
paas-cli role assign --user zhangsan --role tenant-admin --tenant subcompany-a

⚠️ 【生产环境】创建生产租户必须关联预算配额与合规基线检查,且创建动作本身需二级审批;【测试环境】可由子公司管理员自助创建、免审批、配额宽松(见第五章对照表)。

角色二:子公司管理员 —— 给开发配「只读 + 仅自己项目」角色

# 步骤 1:用平台 API 分配(等效于 kubectl apply -f deploy/tenant-rbac.yaml)
curl -X POST https://paas-gateway/api/v1/projects/subcompany-a-order/readonly \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"user":"dev-lisi","project":"order","env":"production"}'

# 步骤 2(排障):直接核对 RoleBinding 是否落到正确 namespace
kubectl --context prod-cluster-01 -n subcompany-a-order get rolebinding order-ro-dev-lisi -o yaml

角色三:安全合规员 —— 季度权限复审

【测试环境】复审可一键自动回收闲置账号;【生产环境】自动回收前需租户管理员确认,且回收动作写入审计并通知企业微信(见 runbook/tenant-ops.sh)。

下面是端到端流程的时序图,涵盖从申请到审计的闭环。

sequenceDiagram
    participant D as 开发者
    participant A as 子公司管理员
    participant P as 权限中心
    participant W as 审批流
    participant G as K8s API 网关
    participant L as 审计中心
    D->>A: 申请 project=order 只读权限
    A->>P: 提交授权申请
    P->>W: 触发审批(生产需二级)
    W-->>P: 审批通过
    P->>G: 下发 RoleBinding
    G-->>P: 成功
    P->>L: 写审计(含审批单号)

五、生产环境管控与安全约束

核心原则:默认最小权限、敏感操作必审批、授权必留痕、跨租户零信任

测试环境 vs 生产环境 差异化管控对照表

管控项测试环境生产环境
租户创建子公司管理员自助,免审批平台管理员操作,二级审批
配额宽松,可超额预警不阻断硬上限,超预算需审批扩容
提权/删 Namespace可直接执行强制多级审批 + 二次认证
权限复审自动回收闲置账号自动列出,回收需租户管理员确认
审计留存≥ 90 天≥ 90 天(监管可追溯导出)
跨租户访问默认拒绝,测试可临时放通默认拒绝,放通需安全合规审批
SSO 强制建议开启强制开启,禁止本地弱口令
指标可见性可按需跨租户排障严格按 tenant 标签隔离
  • 资源配额:租户级配额在权限中心定义,网关侧转为 ResourceQuota + LimitRange 下发到 Namespace,超出即调度失败。
  • 权限隔离:跨租户网络策略默认 Deny-All;数据级通过指标 tenant 标签与 Grafana 文件夹权限双重隔离。

默认 Deny-All 网络策略清单(可独立 apply 校验,与 tenant.goapplyDefaultDenyNetworkPolicy 一致):

# file: deploy/tenant-networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: subcompany-a
  labels:
    tenant: subcompany-a
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
---
# 仅放通 DNS 出向(否则 Pod 无法解析服务发现)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: subcompany-a
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  • 敏感配置加密:权限中心托管的客户端密钥、OAuth2 client secret 经 KMS 信封加密落库,明文不落盘。
  • 故障熔断:当某租户 RBAC 同步异常导致网关鉴权雪崩时,网关进入「拒绝未知、放行已授权缓存」的降级模式,并告警(对应 paas_rbac_sync_errors_total 告警)。

六、常见生产故障与解决方案

监控端配置自洽:下列 prometheus-agent-scrape.yaml 的 relabel 注入 tenant/app/env/cluster 标签,与 Go metrics.go 暴露的 paas_rbac_rolebinding_total{tenant,env,app}cluster(联邦 external_labels)组合,Grafana 据此过滤。

# file: observability/prometheus-agent-scrape.yaml
# 各物理集群内的 Prometheus Agent:采集本集群指标,并强制注入 tenant/app/env/cluster 标签
global:
  scrape_interval: 30s
  external_labels:
    cluster: prod-cluster-01
scrape_configs:
  - job_name: kube-pods
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_namespace]
        target_label: namespace
      - source_labels: [__meta_kubernetes_pod_label_app]
        target_label: app
      # 命名空间名 == tenant,平台保证一致
      - source_labels: [__meta_kubernetes_namespace]
        target_label: tenant
      - target_label: env
        replacement: production
      - target_label: cluster
        replacement: 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_tenant_rbac_recording
    rules:
      - record: paas:rbac:rolebinding_count_by_tenant
        expr: sum by (tenant, cluster) (paas_rbac_rolebinding_total)
      - record: paas:tenant:provision_p99_seconds
        expr: histogram_quantile(0.99, sum by (tenant, le) (rate(paas_tenant_provision_duration_seconds_bucket[5m])))
# file: observability/alert-rules.yaml
groups:
  - name: paas_tenant_rbac_alerts
    rules:
      - alert: TenantRBACSyncFailed
        expr: increase(paas_rbac_sync_errors_total[5m]) > 0
        for: 2m
        labels:
          severity: critical
          team: paas-platform
        annotations:
          summary: "租户 {{ $labels.tenant }} 在集群 {{ $labels.cluster }} RBAC 同步失败"
          description: "5 分钟内出现 RBAC 经网关同步失败,请检查 K8s API 网关鉴权。"
      - alert: TenantQuotaNearLimit
        expr: |
          kube_resourcequota{resource="requests.cpu", type="used"}
            / kube_resourcequota{resource="requests.cpu", type="hard"} > 0.9          
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "租户 {{ $labels.tenant }} CPU 配额使用超过 90%"
# file: observability/alertmanager.yaml
global:
  resolve_timeout: 5m
route:
  receiver: default
  group_by: ['cluster', 'env', 'tenant']
  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:开发者抱怨「看不到自己项目的 Pod」

  • 排查:先确认 RoleBinding 的 namespaceproject 标签是否匹配;再查 Grafana 是否因 tenant 标签缺失被过滤掉。
  • 优化:权限中心在分配时强制校验项目标签一致性,避免人为配错。
# file: runbook/tenant-ops.sh(节选:故障1 排查)
kubectl --context prod-cluster-01 -n subcompany-a-order get rolebinding -l tenant=subcompany-a -o wide
kubectl --context prod-cluster-01 auth can-i get pods -n subcompany-a-order --as=dev-lisi
# 期望 yes;跨 namespace 应 no

故障 2:离职员工账号仍保留生产权限

  • 排查:检查 LDAP/HR 同步是否中断;查审计确认最后一次权限变更时间。
  • 优化:开启「账号停用即冻结」+ 季度复审自动回收,双保险。
# 查某用户全部 RoleBinding(跨租户审查)
kubectl --context prod-cluster-01 get rolebindings -A -o json \
  | jq -r '.items[] | select(.subjects[]?.name=="dev-lisi") | .metadata.namespace + "/" + .metadata.name'
# 查看审计中该用户最近动作
grep '"operator":"dev-lisi"' /var/log/paas/audit.jsonl | tail -5

故障 3:跨租户误访问(A 看到 B 指标)

  • 排查:查中心 Prometheus 的 tenant 标签是否被正确注入;查 Grafana 数据源代理是否绕过了标签过滤。
  • 优化:联邦采集端强制注入标签,Grafana 前端按登录态附加 tenant 过滤器,后端再校验一次。
# 校验联邦视图里 tenant 标签是否存在
curl -s 'http://prom-center:9090/api/v1/query?query=paas_rbac_rolebinding_total' \
  | jq '.data.result[].metric.tenant'

完整运维 runbook(runbook/tenant-ops.sh)覆盖章节四 SOP 与本章排查:

#!/usr/bin/env bash
# file: runbook/tenant-ops.sh
set -euo pipefail

# ---- 章节四 SOP:平台管理员创建租户 ----
paas-cli tenant create --name subcompany-a --env production
# 校验 Namespace 与标签、配额已自动生成
kubectl --context prod-cluster-01 get ns subcompany-a --show-labels
kubectl --context prod-cluster-01 get resourcequota,limitrange -n subcompany-a

# ---- 子公司管理员分配「仅 project=order 只读」角色 ----
kubectl --context prod-cluster-01 apply -f deploy/tenant-rbac.yaml
kubectl --context prod-cluster-01 create rolebinding order-ro \
  --role=order-readonly --user=dev-lisi -n subcompany-a-order

# ---- 验证越权被拒(使用只读用户身份)----
kubectl --context prod-cluster-01 auth can-i delete pods -n subcompany-a-order --as=dev-lisi
# 期望:no

# ---- 安全合规员:季度复审回收闲置账号(生产需人工确认)----
paas-cli rbac review --env production --auto-recycle=false

# ---- 查询审计(≥90 天留存)----
grep '"action":"grant-readonly"' /var/log/paas/audit.jsonl | tail -5

# ---- Grafana 多租户过滤:仅看自己 tenant ----
# PromQL: paas_rbac_rolebinding_total{tenant="subcompany-a"}

# ---- 故障3:校验联邦指标 tenant 标签 ----
curl -s 'http://prom-center:9090/api/v1/query?query=paas_rbac_rolebinding_total' \
  | jq '.data.result[].metric.tenant'

故障排查统一流程如下:

flowchart TD
    A[收到权限/可见性异常] --> B{是授权问题?}
    B -->|是| C[查 RoleBinding 与标签匹配]
    B -->|否| D{是监控可见性问题?}
    D -->|是| E[查 tenant 标签注入与 Grafana 过滤]
    D -->|否| F[查 SSO 同步与审计日志]
    C --> G[修正绑定并重发网关]
    E --> G
    F --> G
    G --> H[回归验证 + 写故障单]

自测题与动手练习

5 道自测题

  1. 租户(Tenant)在 K8s 中映射为什么对象?它和团队、项目的关系是什么?
  2. RBAC 三层模型指哪三层?数据级权限靠什么机制实现?
  3. 为什么权限中心的授权变更必须写审计日志且留存 ≥ 90 天?
  4. 联邦 Prometheus 下,如何实现「租户 A 看不到租户 B 的指标」?
  5. 生产环境删除 Namespace 与测试环境有何管控差异?

3 个动手练习

  1. 在测试环境用 paas-cli 建一个子公司租户,观察其 Namespace 与 ResourceQuota 是否自动生成(对照 deploy/tenant-quota.yaml)。
  2. 定义一个「仅 project=order 只读」的 Role(deploy/tenant-rbac.yaml),绑定给测试账号,验证其访问其他项目被拒(kubectl auth can-i)。
  3. 在 Grafana 用 tenant=你的租户 过滤,截图对比开启/关闭过滤时的曲线差异(指标 paas_rbac_rolebinding_total)。

本章小结

  • 权限中心是 PaaS 的信任基石:解决「身份、授权、审计」三件事。
  • 组织树(集团→子公司→团队→项目)映射为租户与 Namespace,租户强隔离是★禁止删减红线。
  • RBAC 三层(Namespace/API/数据)落地最小权限;SSO 对接客户既有身份源;Tenant CRD 由 DynamicClient 操作。
  • 授权经统一 K8s API 网关下发(Go 用 rest.Config + Clientset/DynamicClient 按租户 env 路由到物理集群),指标注入 tenant 标签实现联邦下的数据隔离。
  • Go 上报指标 paas_rbac_rolebinding_total / paas_rbac_sync_errors_total 与 Prometheus 联邦、recording/alert 规则名完全一致。
  • 一切敏感操作走审批、留审计,测试与生产差异化管控。

下一章我们将进入《多 K8s 集群纳管模块》,看看这些 Namespace 与配额是运行在哪些物理隔离集群之上、平台又是如何把它们统一收敛纳管的。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!