Basics
核心概念
- Resources 定义了当用户创建自定义 API 实例时,KRO 将创建和管理的 Kubernetes 对象
- 每个 Resource 都是有效的 Kubernetes YAML,可以使用 CEL 表达式实现动态值注入
Resource 结构
每个 Resource 必须包含两个核心字段
1 2 3 4 5 6 7 8 9
| resources: - id: deployment template: apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3
|
| 字段 |
说明 |
| id |
在 RGD 内唯一标识资源,用于在 CEL 表达式中引用 |
| template |
有效的 Kubernetes 资源清单 |
ID 命名规范(重要)
Resource ID 必须使用 lowerCamelCase 格式
1 2 3 4
| ✓ 有效: deployment, webServer, postgresDatabase ✗ 无效: web-server (会被解析为减法运算) WebServer (首字母应小写) postgres_database (不推荐下划线)
|
原因:ID 会被用作 CEL 表达式中的标识符,连字符会被解释为减法运算符
CEL 表达式引用
可引用的三种数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| graph TD subgraph Instance["实例 Instance"] SchemaSpec["schema.spec<br/>用户配置"] SchemaMetadata["schema.metadata<br/>实例元数据"] end
subgraph Resources["Resources"] R1["Resource 1<br/>database"] R2["Resource 2<br/>deployment"] end
R2 -->|"引用"| SchemaSpec R2 -->|"引用"| SchemaMetadata R2 -->|"引用"| R1
style SchemaSpec fill:#e1f5fe style SchemaMetadata fill:#e1f5fe style R1 fill:#fff3e0 style R2 fill:#c8e6c9
|
引用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| resources: - id: database template: metadata: name: ${schema.spec.name}-db namespace: ${schema.metadata.namespace} labels: ${schema.metadata.labels}
- id: deployment template: metadata: annotations: ${database.metadata.annotations} spec: containers: - env: - name: DATABASE_HOST value: ${database.status.endpoint} - name: DATABASE_VERSION value: ${database.spec.version}
|
隐式依赖机制
关键点:当 Resource A 引用 Resource B 的字段时,自动创建依赖关系
1 2 3 4 5
| graph LR A["deployment"] -->|"依赖<br/>database.status.endpoint"| B["database"]
style A fill:#c8e6c9 style B fill:#fff3e0
|
KRO 会自动确定创建顺序
- database 必须先于 deployment 创建
- 因为 deployment 需要等待 database 的 endpoint 可用
验证机制(核心优势)
验证时机
1
| RGD 创建时 → 验证所有模板和 CEL 表达式 → 失败则拒绝 RGD
|
验证内容
| 验证类型 |
说明 |
示例 |
| 模板语法 |
检查 ${} 配对 |
${schema.spec.name} ✓ |
| CEL 语法 |
验证表达式有效性 |
${schema..name} ✗ |
| 类型检查 |
匹配字段类型期望 |
replicas: ${name} ✗ (字符串→整数) |
| 字段存在性 |
确认引用字段存在 |
${deployment.spec.podReplicas} ✗ |
| Schema 验证 |
自定义字段存在性 |
${schema.spec.nonExistent} ✗ |
| 静态字段 |
验证非 CEL 字段 |
replicas: “three” ✗ |
处理流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| flowchart TB subgraph Phase1["阶段1: 验证 RGD 创建"] A1[解析资源模板] --> A2[提取 CEL 表达式] A2 --> A3[验证 CEL 语法] A3 --> A4[获取 K8s OpenAPI Schema] A4 --> A5[类型检查] A5 --> A6[构建依赖图] A6 --> A7{验证通过?} A7 -->|否| Reject[拒绝 RGD] A7 -->|是| Accept[接受 RGD] end
subgraph Phase2["阶段2: 实例创建"] B1[按拓扑顺序处理] --> B2[计算 CEL 表达式] B2 --> B3{字段可用?} B3 -->|否| B4[重新排队等待] B3 -->|是| B5[创建资源] B5 --> B6[等待 readyWhen] B6 --> B7[处理下一个资源] end
Accept --> Phase2
|
类型安全示例
1 2 3 4 5 6 7 8 9 10 11 12 13
| # 整数字段 replicas: ${schema.spec.replicas} # ✓ replicas: ${schema.spec.replicas * 2} # ✓ replicas: ${schema.spec.name} # ✗ 类型错误
# 字符串字段 name: ${schema.spec.name} # ✓ name: ${schema.spec.name + "-deployment"} # ✓ name: ${schema.spec.replicas} # ✗ 类型错误
# 数组字段 containers: ${deployment1.spec.template.spec.containers} # ✓ containers: ${deployment1.spec.template.spec.containers[0]} # ✗ 单对象→数组
|
常见验证错误
| 错误类型 |
示例 |
错误信息 |
| 未知 API/Kind |
kind: PostgreSQL |
schema not found |
| 未知字段 |
spec.unknownField |
no such member |
| 类型不匹配 |
replicas: ${name} |
type mismatch: string but expected integer |
关键优势
传统模板引擎 vs KRO
| 特性 |
传统模板引擎 |
KRO |
| 验证时机 |
运行时 |
RGD 创建时 |
| 类型检查 |
无 |
完整类型检查 |
| Schema 验证 |
无 |
基于 K8s OpenAPI |
| 错误反馈 |
部署后才发现 |
即时反馈 |
核心价值:KRO 在 RGD 创建阶段就完成所有验证,确保用户在创建实例时不会遇到类型错误或字段不存在的问题
Conditionals
核心概念
问题场景:并非所有资源都需要在每个实例中创建。例如
- 仅当用户请求时才启用监控
- 仅在特定环境才启用备份
- 仅当需要时才启用 TLS
解决方案:KRO 提供 includeWhen 字段,使资源变为可选的
1 2 3 4 5 6 7
| flowchart TD A[实例创建] --> B{评估 includeWhen} B -->|所有条件为 true| C[创建资源] B -->|任一条件为 false| D[跳过资源]
style C fill:#c8e6c9 style D fill:#ffcdd2
|
基础示例
RGD 定义
1 2 3 4 5 6 7 8 9
| resources: - id: ingress includeWhen: - ${schema.spec.ingress.enabled} template: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ${schema.spec.name}
|
实例示例
1 2 3 4 5 6 7 8 9
| apiVersion: example.com/v1 kind: Application metadata: name: my-app spec: name: my-app ingress: enabled: true
|
工作原理
| 条件结果 |
行为 |
| 所有表达式为 true |
资源被包含 |
| 任一表达式为 false |
资源被跳过 |
| 条件从 true → false |
资源被修剪 |
| 条件从 false → true |
资源被创建 |
关键特性
- 每个表达式必须返回布尔值(true 或 false)
- 条件在每次协调时重新评估
- 对集合资源,includeWhen 应用于整个集合
可引用的内容
引用 schema.spec
1 2 3 4 5 6 7 8 9
| # ✓ 有效 - 返回布尔值 includeWhen: - ${schema.spec.ingress.enabled} # 布尔字段 - ${schema.spec.environment == "production"} # 比较表达式 - ${schema.spec.replicas > 3} # 数值比较
# ✗ 无效 - 必须返回布尔值 includeWhen: - ${schema.spec.appName} # 返回字符串,非布尔值
|
引用上游资源
1 2 3
| # ✓ 有效 - 引用上游资源 includeWhen: - ${deployment.status.availableReplicas > 0}
|
注意:当 includeWhen 引用其他资源时,KRO 将其视为依赖关系,条件基于上游资源的实际状态进行评估
依赖等待机制
1 2 3 4 5 6 7 8
| flowchart TB A[deployment 创建] --> B{等待 status 就绪} B -->|availableReplicas 未就绪| C[重新排队等待] B -->|availableReplicas > 0| D[评估 includeWhen] D --> E[创建 serviceMonitor]
style C fill:#fff3e0 style E fill:#c8e6c9
|
示例:等待上游资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| resources: - id: deployment template: apiVersion: apps/v1 kind: Deployment metadata: name: ${schema.spec.name}
- id: serviceMonitor includeWhen: - ${deployment.status.availableReplicas > 0} template: apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor
|
行为:
- 如果 deployment.status.availableReplicas 尚未填充,KRO 会等待并重新评估
- 不会将未就绪的状态视为 false
- 如果 deployment 后来缩容到 0,KRO 会修剪 ServiceMonitor
引用上游资源的风险
问题:状态波动导致翻转
1 2 3 4 5 6 7 8 9 10
| flowchart LR A[availableReplicas > 0] -->|true| B[创建 monitor] B --> C[副本波动] C -->|availableReplicas = 0| D[删除 monitor] D --> E[副本恢复] E -->|availableReplicas > 0| F[重新创建 monitor]
style B fill:#ffcdd2 style D fill:#ffcdd2 style F fill:#ffcdd2
|
风险示例
1 2 3 4 5 6 7 8 9
| - id: monitor includeWhen: - ${deployment.status.availableReplicas > 0}
- id: monitor includeWhen: - ${schema.spec.monitoring.enabled}
|
决策流程
1 2 3
| 引用上游资源前问自己:该字段在正常运行期间会来回变化吗? ├─ 是 → 使用 readyWhen 控制顺序,而非 includeWhen └─ 否 → 可以使用 includeWhen(如 ConfigMap data 等创建后不变的字段)
|
readyWhen vs includeWhen
| 特性 |
includeWhen |
readyWhen(推断) |
| 作用 |
控制资源是否存在 |
控制资源是否就绪 |
| 条件为 false |
资源被删除/跳过 |
等待,不删除资源 |
| 适用场景 |
可选功能 |
等待依赖就绪 |
| 状态波动 |
可能导致翻转 |
安全,只是等待 |
依赖传播:跳过资源的影响
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| graph TD subgraph "条件为 true 的分支" A1[Deployment ✓] --> B1[Service] B1 --> C1[Ingress] end
subgraph "条件为 false 的分支" A2[Deployment ✗] -.-> B2[Service 跳过] B2 -.-> C2[Ingress 跳过] end
style A1 fill:#c8e6c9 style B1 fill:#c8e6c9 style C1 fill:#c8e6c9 style A2 fill:#ffcdd2 style B2 fill:#ffcdd2 style C2 fill:#ffcdd2
|
- 关键规则:当资源因 includeWhen 被跳过时,所有依赖它的资源也会被跳过
- 这确保资源图保持一致性,防止资源引用不存在的依赖

多条件逻辑
逻辑 AND(默认)
1 2 3 4 5 6 7 8
| resources: - id: certificate includeWhen: - ${schema.spec.ingress.enabled} - ${schema.spec.ingress.tls} template: apiVersion: cert-manager.io/v1 kind: Certificate
|
1 2 3 4 5 6 7 8 9
| flowchart TB A[评估条件] --> B{ingress.enabled?} B -->|false| D[跳过 Certificate] B -->|true| C{ingress.tls?} C -->|false| D C -->|true| E[创建 Certificate]
style E fill:#c8e6c9 style D fill:#ffcdd2
|
逻辑 OR(单表达式内)
1 2
| includeWhen: - ${schema.spec.env == "staging" || schema.spec.env == "production"}
|
1 2 3 4 5 6 7 8
| flowchart TB A{env == staging?} -->|true| B[创建资源] A -->|false| C{env == production?} C -->|true| B C -->|false| D[跳过资源]
style B fill:#c8e6c9 style D fill:#ffcdd2
|
最佳实践总结
核心原则:includeWhen 应用于稳定的、用户控制的条件,而非波动的系统状态
| 场景 |
推荐方案 |
原因 |
| 用户可选功能 |
includeWhen: ${schema.spec.feature.enabled} |
稳定、用户可控 |
| 环境判断 |
includeWhen: ${schema.spec.env == “prod”} |
条件明确 |
| 等待资源就绪 |
readyWhen |
避免资源翻转 |
| 状态监控 |
不使用 includeWhen |
status 字段易波动 |
Readiness
- readyWhen 是 KRO 中用于控制资源何时才算真正可用的机制
- 解决了 Kubernetes 资源创建后需要时间才能达到可用状态的问题
核心问题
- Kubernetes 中资源创建和可用之间存在时间差:
- Deployment 创建了,但 Pod 还在启动中(replicas = 0)
- LoadBalancer Service 创建了,但还没有分配外部 IP
- 如果依赖资源直接使用这些还不存在的值,会失败或获取无效数据
基本用法
1 2 3 4 5
| resources: - id: database readyWhen: - ${database.status.conditions.exists(c, c.type == "Ready" && c.status == "True")} - ${database.status.?endpoint != ""}
|
含义:数据库资源必须同时满足两个条件才算 Ready:
- Ready condition 的 status 为 True
- endpoint 字段非空
工作原理
| 情况 |
行为 |
| 无 readyWhen |
资源创建且所有 CEL 表达式能解析后就 Ready |
| 有 readyWhen |
资源创建后等待,直到所有条件为 true |
| 全部 true |
标记为 Ready |
| 任一 false |
继续等待 |
依赖该资源的其他资源会等它 Ready 后才创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| flowchart TD A[资源创建] --> B{有 readyWhen?}
B -->|否| C[CEL 表达式可解析?] C -->|是| D[✅ 标记为 Ready] C -->|否| E[⏳ 等待解析] E --> C
B -->|是| F[执行 readyWhen 条件检查]
F --> G{所有条件都 true?} G -->|是| D G -->|否| H[⏳ 继续等待] H --> F
D --> I[🚀 依赖资源可以创建]
style D fill:#90EE90 style E fill:#FFD700 style H fill:#FFD700 style I fill:#87CEEB
|
语法规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| readyWhen: - ${deployment.status.availableReplicas > 0}
readyWhen: - ${each.status.phase == 'Running'}
readyWhen: - ${service.status.loadBalancer.ingress.size() > 0}
readyWhen: - ${schema.spec.replicas > 3}
readyWhen: - ${deployment.status.availableReplicas}
|
设计哲学
核心原则:局部性与单一职责
1 2 3 4 5
| ┌─────────────────────────────────────────────────────────────┐ │ readyWhen 设计哲学 │ ├─────────────────────────────────────────────────────────────┤ │ "每个资源只负责判断自己的 readiness,不关心外部世界" │ └─────────────────────────────────────────────────────────────┘
|
只能引用自身 → 局部确定性
1 2 3 4 5 6 7
| readyWhen: - ${deployment.status.availableReplicas > 0}
readyWhen: - ${service.status.loadBalancer.ingress.size() > 0}
|
设计理由
| 理由 |
说明 |
| 避免循环依赖 |
A 引用 B,B 引用 A → 死锁 |
| 职责分离 |
依赖关系由模板引用处理,readyWhen 只管自己 |
| 可调试性 |
问题隔离在单个资源内 |
| 可测试性 |
单个资源的 readiness 可独立验证 |
1 2 3 4 5 6 7 8 9 10
| 依赖关系 (模板引用) ┌─────────────────────────┐ │ │ ┌────▼────┐ ┌────▼────┐ │ A │ │ B │ │ readyWhen │ │ readyWhen │ └────┬────┘ └────┬────┘ │ │ └─────────────────────────┘ 只检查各自状态
|
不能引用 schema → 关注运行时状态
1 2 3 4 5 6 7
| readyWhen: - ${schema.spec.replicas > 3}
readyWhen: - ${deployment.status.readyReplicas == deployment.spec.replicas}
|
设计理由 - Readiness 只关注实际状态
1 2 3 4 5 6 7
| 输入 处理 输出 ┌─────────┐ ┌─────────┐ ┌─────────┐ │ schema │ ────────► │Kubernetes│ ────────► │ status │ │(期望状态)│ │ 调谐 │ │(实际状态)│ └─────────┘ └─────────┘ └─────────┘ ↑ readiness 只关心这里
|
| 概念 |
特性 |
readyWhen 应该用? |
| schema.spec |
用户输入的期望值 |
❌ 这是配置,不是状态 |
| status |
Kubernetes 控制器反馈的实际值 |
✅ 这才是 readiness |
核心思想:Readiness 是运行时概念,应该基于实际观察到的状态,而非配置的期望
必须返回布尔值 → 语义清晰
1 2 3 4 5 6 7
| readyWhen: - ${deployment.status.availableReplicas}
readyWhen: - ${deployment.status.availableReplicas > 0}
|
设计理由
1 2 3 4 5 6 7 8 9
| readyWhen 的本质 ┌────────────────────────┐ │ "准备好了吗?" │ │ 答案只能是: │ │ • true (准备好了) │ │ • false (还没好) │ └────────────────────────┘ ↑ 除此之外的回答都是无效的
|
- 避免歧义:数字 5 是 ready 还是 not ready?
- 统一接口:所有 readyWhen 条件都是布尔表达式
- 组合友好:多个条件可以用 AND 逻辑自然组合
集合使用 each → 粒度控制
1 2 3 4 5
| forEach: - pod: ${schema.spec.pods} readyWhen: - ${each.status.phase == 'Running'}
|
设计理由
1 2 3 4 5 6 7 8 9 10 11 12
| 集合 Readiness 的两种语义
┌────────────────┐ ┌────────────────┐ │ 整体级别 │ │ 元素级别 │ ├────────────────┤ ├────────────────┤ │ "至少一个Ready" │ │ "全部Ready" │ │ │ │ │ │ □ □ □ ■ □ │ │ ■ ■ ■ ■ ■ │ │ Ready! │ │ Ready! │ └────────────────┘ └────────────────┘
KRO 选择后者:更安全、更可预测
|
each 让每个元素独立判断 readiness,集合整体 ready 当且仅当所有元素都 ready
设计理念总结
readyWhen 是一个局部的、基于状态的、布尔返回的自描述 readiness 检查机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ┌─────────────────────────────────────────────────────────────────┐ │ readyWhen 设计原则 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. 局部性 → 只关心自己,不依赖外部 │ │ 2. 实时性 → 基于 status,不基于 spec │ │ 3. 布尔性 → 答案明确,非真即假 │ │ 4. 组合性 → 依赖关系由模板层处理,readyWhen 不重复 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 架构分离 │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 模板引用 │ 决定 │ 创建顺序 │ │ │ │ (dependencies) ───► │ (ordering) │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ readyWhen │ 决定 │ 等待时长 │ │ │ │ (readiness) ───► │ (timing) │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ 两个机制各司其职,互不干扰 │ └─────────────────────────────────────────────────────────────────┘
|
可选操作符 ?
? 返回 null 而不是报错,适合处理可选字段
1 2 3 4 5
| - ${service.status.?loadBalancer.?ingress.size() > 0}
- ${database.status.endpoint != ""}
|
核心机制
1
| ${service.status.?loadBalancer.?ingress.size() > 0}
|
? 是 CEL 的安全导航操作符(null-safe navigation),类似其他语言中的
1 2 3
| - JavaScript: service?.status?.loadBalancer - Rust: service.as_ref()?.status.as_ref()?.loadBalancer - Swift: service?.status?.loadBalancer
|
行为对比
1 2 3 4 5 6 7 8 9
| # 没有 ? - 传统访问 ${service.status.loadBalancer.ingress.size()}
# 路径中任何一环为 null/undefined → ❌ 报错
# 有 ? - 安全访问 ${service.status.?loadBalancer.?ingress.size()}
# 路径中任何一环为 null → ✅ 返回 null,继续执行
|
使用场景决策树
1 2 3 4 5 6 7 8 9 10 11
| 字段是否存在可预测? │ ├─ 可预测且最终一定存在 │ └─ 不用 ? │ └─ ${database.status.endpoint != ""} │ └─ KRO 会等待字段出现 │ └─ 可预测性未知或真正可选 └─ 用 ? └─ ${service.status.?loadBalancer.?ingress.size() > 0} └─ 字段不存在时返回 null,不阻塞
|
场景分类
场景 1:延迟出现但最终必有的字段 → 不用 ?
1 2
| readyWhen: - ${database.status.endpoint != ""}
|
为什么不用 ?
1 2 3 4 5 6
| 时间轴: ─────────────────────────────────────────────────────────→ t=0 t=5s t=10s t=15s 创建 endpoint=null endpoint="" endpoint="10.0.0.1" ↑ 此时条件变为 true
|
- endpoint 字段最终会存在
- KRO 知道在等待什么,会持续检查
- 一旦字段出现且值有效,条件满足
场景 2:真正可选的字段 → 用 ?
1 2
| readyWhen: - ${service.status.?loadBalancer.?ingress.size() > 0}
|
为什么用 ?
1 2 3 4
| Service 类型决定: ├─ ClusterIP → loadBalancer 永远不存在 ├─ NodePort → loadBalancer 永远不存在 └─ LoadBalancer → loadBalancer 可能存在
|
- loadBalancer 字段可能永远不存在
- 不用
? 会导致:ClusterIP 类型的 Service 永远不 ready
- 用
? 后:null.size() → null → 条件为 false → 继续等待或按业务逻辑处理
场景 3:结构未知的字段 → 用 ?
1 2
| readyWhen: - ${config.data.?endpoint != ""}
|
为什么用 ?
1 2 3 4 5 6 7 8 9
| apiVersion: v1 kind: ConfigMap data: endpoint: "http://api.example.com"
|
- data.endpoint 的存在与否取决于用户配置
- 无法预测字段是否存在
- 用
? 避免”字段不存在“的错误
执行流程对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ┌─────────────────────────────────────────────────────────────────┐ │ 无 ? 操作符 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ${service.status.loadBalancer.ingress.size() > 0} │ │ │ │ 1. service.status → ✅ 对象 │ │ 2. .loadBalancer → ❌ null │ │ 3. .ingress → 💥 报错:无法读取 null 的属性 │ │ 4. 整个条件 → 💥 异常,可能触发重试或失败 │ │ │ └─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐ │ 有 ? 操作符 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ${service.status.?loadBalancer.?ingress.size() > 0} │ │ │ │ 1. service.status → ✅ 对象 │ │ 2. .?loadBalancer → ⚠️ null │ │ 3. .?ingress → ⚠️ null (短路求值) │ │ 4. .size() → ⚠️ null (null.size() = null) │ │ 5. null > 0 → ⚠️ false/null │ │ 6. 整个条件 → ✅ false,继续等待 │ │ │ └─────────────────────────────────────────────────────────────────┘
|
常见模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| readyWhen: - ${resource.status.?nested.?deep.?field != ""}
readyWhen: - ${resource.status.?optionalField != null && resource.status.optionalField.length > 0}
readyWhen: - ${resource.status.?items.size() > 0}
readyWhen: - ${service.status.?loadBalancer != null ? service.status.loadBalancer.ingress.size() > 0 : true}
|
决策指南
不用 ? - 确定存在 + 最终一定会出现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 字段是否确定存在? │ ┌───────────────┴───────────────┐ │ │ 是 否 │ │ │ │ 字段最终一定会出现? 必须使用 ? │ ┌─────┴─────┐ │ │ 是 否 │ │ 不用 ? 用 ? │ │ KRO 等待 容忍不存在
|
| 场景 |
是否用 ? |
原因 |
| 数据库连接字符串 |
❌ |
创建后必然出现 |
| LoadBalancer ingress |
✅ |
ClusterIP 时不存在 |
| Pod 的 status.phase |
❌ |
Pod 必有 status |
| CRD 的可选 status 字段 |
✅ |
取决于控制器实现 |
| ConfigMap 的 data 键 |
✅ |
用户自定义,不确定 |
总结
? 是容错机制:用于处理”字段可能不存在“的结构性不确定性,而非”字段尚未出现“的时序性延迟
依赖链示例
KRO 自动按依赖顺序处理:先创建 database → 等待其 Ready → 再创建 app(此时 endpoint 已有值)
1 2 3
| database (readyWhen: endpoint != "") ↓ 等待 database Ready app (使用 database.status.endpoint)
|
Collections
核心设计动机
- 传统 KRO 资源定义是 1:1 的 - 一个 resource 条目创建一个 K8s 对象
- 如果需要 5 个 worker Pod,就得写 5 个 resource 定义
- 在数量固定时没问题,但数量依赖运行时数据时就无能为力了
- 比如可用区数量、租户数、worker 数等都是动态的
forEach 把 1:1 变成了 1:N,一个 resource 定义变成一个”模板“,根据迭代数据动态创建 N 个资源
forEach 的本质
1 2
| forEach: - worker: ${schema.spec.workers}
|
关键点
| 维度 |
说明 |
| 语法 |
数组,每个元素是一个单 entry map:{变量名: CEL数组表达式} |
| 变量名 |
在 template 中作为 CEL 变量可用,建议用语义化名称(region、dbSpec)而非 item、i |
| CEL 必须返回数组 |
不是数组的表达式会失败 |
| 数组顺序 = 资源顺序 |
确定性、可预测的索引和命名 |
多迭代器 = 笛卡尔积
这是最容易出问题的地方,两个迭代器不是”分别创建“,而是全排列组合
1 2
| regions: ["us-east", "us-west"] × tiers: ["web", "api"] = 4 个 Deployment:us-east-web, us-east-api, us-west-web, us-west-api
|
- 嵌套顺序:第一个迭代器是外层循环,后续迭代器依次嵌套,顺序是确定性的
- 危险:维度爆炸
- 3 regions × 5 tiers × 10 shards = 150 个资源
- KRO 默认上限是 1000 个/集合、10 个维度,都是可配置的
- 空的任意迭代器 = 零资源(2 × 0 = 0),符合笛卡尔积数学语义
资源命名的硬性要求
每个集合内资源必须有全局唯一的 name,必须把所有迭代变量都编入 metadata.name:
1 2 3 4 5
| # 正确:包含 region + tier name: ${schema.metadata.name + '-' + region + '-' + tier}
# 错误:省略了 tier → 多个资源会撞名覆盖 name: ${schema.metadata.name + '-' + region}
|
- 还要包含 schema.metadata.name 避免跨实例冲突
- 对于集群范围的资源(无 namespace),所有维度都必须放在 metadata.name 里
集合的数据源
迭代数组可以从多个地方来
| 来源 |
示例 |
特点 |
| 实例 Spec |
${schema.spec.regions} |
用户直接输入 |
| 资源 Spec |
${database.spec.shards} |
依赖上游资源的状态 |
| 资源 Status |
${cluster.status.brokers} |
运行时动态数据 |
| 数组字面量 |
${[“a”, “b”, “c”]} |
静态列表 |
| CEL 函数 |
${lists.range(3)} → [0,1,2] |
生成索引序列 |
- 引用其他资源的 spec/status 时,自动创建依赖
- kro 会等待上游集合完全 ready 后才开始创建下游
集合间的引用与依赖
集合暴露为一个数组,其他资源可以用 CEL 函数操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| - id: summary template: data: podNames: ${workerPods.map(p, p.metadata.name).join(', ')} count: ${string(size(workerPods))}
# 集合引用集合 - id: backupJobs forEach: - db: ${databases} # 遍历另一个集合 template: metadata: name: ${schema.metadata.name + '-backup-' + db.metadata.name}
|
依赖传播机制
- 集合 B 引用集合 A → kro 等待 A 中所有资源都 ready 才开始创建 B
- 这是一种”全或无“的屏障语义
readyWhen 与集合
集合使用 each 关键字做逐项检查:
1 2 3 4
| forEach: - worker: ${schema.spec.workers} readyWhen: - ${each.status.phase == 'Running'}
|
- 语义是 AND - 所有元素都通过才算 ready,而空集合视为 ready(没有元素需要等待)
- 这与单个资源的 readyWhen 只能引用自身的设计哲学一致 - each 是集合的”自身”
includeWhen 与集合
- includeWhen 作用于整个集合,不能过滤单项,条件为 false → 整个集合跳过
- 要过滤单项,用 filter() 在 forEach 表达式中实现:
1 2
| forEach: - dbSpec: ${schema.spec.databases.filter(d, d.backupEnabled)}
|
集合生命周期
| 事件 |
行为 |
| 扩容(数组增加元素) |
创建新资源 |
| 缩容(数组减少元素) |
自动删除对应资源(通过 applyset 机制修剪) |
| 空数组 |
零资源,不是错误,集合视为 ready |
| 漂移 |
外部修改被自动恢复为期望状态 |
内部机制
- RGD 验证时(静态分析)
- 分析 forEach 表达式推导元素类型(如 []string → 每个迭代变量是 string),实现静态类型检查
- 运行时
- 集合节点在依赖图中是单个节点,但创建多个资源
- 每个资源独立追踪,单个失败不影响其他
- 自动标签
- kro.run/node-id - RGD 中的资源 ID
- kro.run/collection-index — 集合内位置(0-indexed)
- kro.run/collection-size — 集合总数
- kro.run/instance-id — 实例 UID
Map 迭代的坑
Go 中 map 迭代顺序是随机的,如果迭代 map keys/values,必须先转为排序数组:
1 2
| forEach: - key: ${schema.spec.labels.map(k, k).sort()}
|
建议:设计 Schema 时优先用数组而非 map - 更好的 patch 语义、可扩展性、确定性顺序
External References