2. OPA 规则基础:Rego 语言入门、规则与查询、数据与输入文档
在深入排查 OPA 准入控制延迟之前,必须牢固掌握 Rego 语言的核心概念。本章将系统讲解 Rego 的基本语法、规则与查询的结构,以及 data 和 input 两个关键文档的作用。这些基础是理解后续规则复杂度分析和性能优化的前提。
2.1 Rego 语言入门
Rego 是一种专为策略定义和策略决策而设计的声明式查询语言。它并非通用编程语言,而是专注于“策略即代码”(Policy as Code)场景。Rego 的核心思想是:策略是数据查询。你定义一组规则(Rule),然后通过查询(Query)来获取策略决策结果。
关键认知: Rego 不是命令式语言(如 Python、Go),不关心“如何做”,只关心“要什么”。你描述策略的期望状态,Rego 引擎负责计算是否满足。
2.1.1 基本语法元素
| 语法元素 | 说明 | 示例 |
|---|---|---|
| 标量 | 字符串、数字、布尔值、null | "admin", 8080, true, null |
| 复合值 | 对象(Object)、数组(Array)、集合(Set) | {"name": "app"}, [1,2,3], {"a","b"} |
| 变量 | 以 _ 或字母开头,不可变绑定 |
user, _role, namespace |
| 引用 | 通过点号或方括号访问嵌套数据 | input.request.kind, data.roles["admin"] |
| 数组/对象推导 | 类似 Python 列表推导,用于生成新集合 | [x | x = data.users[_]; x.role == "admin"] |
| 内置函数 | 字符串、正则、时间、加密等标准库 | startswith(path, "/api/"), regex.match("^v[0-9]+", version) |
2.1.2 表达式与逻辑
Rego 中的表达式是逻辑条件,多个表达式之间用 ; 或换行分隔,表示逻辑“与”(AND)。同一规则内不同子句之间是“或”(OR)关系。
# 逻辑与:所有条件必须同时满足
allow {
input.user == "admin"
input.action == "delete"
}
# 逻辑或:使用不同规则体实现
allow {
input.user == "admin"
}
allow {
input.user == "superuser"
}
2.2 规则(Rule)与查询(Query)的基本结构
2.2.1 规则结构
规则是 Rego 策略的基本单元。每条规则由一个头部和一个或多个体组成。
# 规则头部:规则名(参数) [= 值]
# 规则体:条件表达式序列
# 布尔规则(默认返回 true/false)
allowed_namespace {
input.namespace == "production"
}
# 完整规则(返回具体值)
allowed_ports[port] {
port := input.ports[_]
port > 1024
port < 65535
}
# 带默认值的规则
default allow = false
allow {
input.user == "admin"
}
| 规则类型 | 头部形式 | 返回值 | 典型用途 |
|---|---|---|---|
| 布尔规则 | rule_name { ... } |
true(体为真时) |
准入控制中的 allow/deny 判断 |
| 完整规则 | rule_name = value { ... } |
指定的值 | 计算配置、生成报告 |
| 集合规则 | rule_name[element] { ... } |
元素集合 | 收集满足条件的资源列表 |
| 对象规则 | rule_name[key] = value { ... } |
键值对对象 | 构建映射关系 |
| 默认规则 | default rule_name = value |
默认值 | 避免未定义错误 |
2.2.2 查询结构
查询是向 OPA 引擎发起的策略评估请求。在 Kubernetes 准入控制场景中,查询通常由 API Server 自动构造。
# 简单查询:检查某个规则是否为真
data.kubernetes.admission.allow
# 带变量的查询:获取具体值
x := data.kubernetes.admission.allowed_namespaces[x]
# 复杂查询:组合多个规则
allow := data.kubernetes.admission.allow
violations := data.kubernetes.admission.violations
性能陷阱: 查询中如果使用未绑定的变量(如
data.roles[_]),OPA 会进行全量遍历。在规则复杂度高时,这种查询可能成为延迟瓶颈。后续章节将详细分析。
2.3 数据文档(data)与输入文档(input)的概念
OPA 的评估模型基于两个核心文档:data 和 input。理解它们的区别和用途是编写高效策略的基础。
2.3.1 数据文档(data)
data 是 OPA 的全局持久化数据,包含所有加载的策略规则和外部数据。它由以下部分组成:
- 策略规则: 所有通过
package声明的规则自动挂载到data下。例如package kubernetes.admission中的规则可通过data.kubernetes.admission访问。 - 外部数据: 通过 Bundle、API 推送或文件加载的 JSON/YAML 数据,如 RBAC 配置、命名空间白名单、资源配额等。
- 内置数据: OPA 内置的元数据,如
data.opametadata。
# 假设加载了以下外部数据(JSON)
{
"allowed_registries": ["docker.io", "gcr.io"],
"admin_users": ["alice", "bob"]
}
# 在 Rego 中访问
allowed_registry {
input.image.startswith(data.allowed_registries[_])
}
is_admin {
input.user == data.admin_users[_]
}
2.3.2 输入文档(input)
input 是每次策略评估时传入的临时请求数据。在 Kubernetes 准入控制中,input 包含 AdmissionReview 请求的全部内容。
# 典型的 Kubernetes 准入控制 input 结构(简化)
{
"kind": "AdmissionReview",
"request": {
"uid": "12345",
"operation": "CREATE",
"object": {
"kind": "Pod",
"metadata": {
"name": "nginx-pod",
"namespace": "default"
},
"spec": {
"containers": [
{"name": "nginx", "image": "nginx:latest"}
]
}
},
"userInfo": {
"username": "kubernetes-admin",
"groups": ["system:masters"]
}
}
}
# 在 Rego 中访问 input
deny[msg] {
input.request.operation == "CREATE"
input.request.object.kind == "Pod"
not startswith(input.request.object.spec.containers[_].image, "allowed-registry.com/")
msg := sprintf("容器镜像 %v 不在允许的仓库中", [input.request.object.spec.containers[_].image])
}
2.3.3 data 与 input 的对比
| 维度 | data | input |
|---|---|---|
| 生命周期 | 持久化,跨多次评估共享 | 每次评估独立传入,评估后丢弃 |
| 数据来源 | Bundle、API、文件加载 | 调用方(如 API Server)传入 |
| 更新方式 | 通过 Bundle 更新或 API 推送 | 每次请求重新构造 |
| 典型内容 | 策略规则、白名单、配置、RBAC | AdmissionReview、HTTP 请求、事件 |
| 访问性能 | 可被 OPA 缓存优化 | 每次评估需重新解析 |
| 安全影响 | 需保护 data 不被篡改 | 需验证 input 的合法性 |
2.4 规则评估流程与文档关系
下面的 SVG 图展示了 OPA 在 Kubernetes 准入控制场景中的规则评估流程,以及 data 和 input 如何协同工作。
2.5 实战示例:一个完整的准入控制规则
下面是一个完整的 Rego 规则示例,展示了 data 和 input 的协同使用。该规则用于检查 Pod 是否使用了允许的镜像仓库。
package kubernetes.admission
# 默认拒绝
default deny = false
# 从 data 中读取允许的镜像仓库列表
allowed_registries := data.config.allowed_registries
# 拒绝规则:如果镜像不在允许列表中,则拒绝
deny[msg] {
input.request.operation == "CREATE"
input.request.object.kind == "Pod"
container := input.request.object.spec.containers[_]
not startswith(container.image, allowed_registries[_])
msg := sprintf("容器 %v 的镜像 %v 不在允许的仓库列表中", [container.name, container.image])
}
# 审计规则:记录所有创建 Pod 的操作
audit_log[entry] {
input.request.operation == "CREATE"
input.request.object.kind == "Pod"
entry := {
"pod_name": input.request.object.metadata.name,
"namespace": input.request.object.metadata.namespace,
"user": input.request.userInfo.username,
"time": time.now_ns()
}
}
性能观察: 上述规则中
allowed_registries[_] 会遍历整个列表。如果 data.config.allowed_registries 包含数千个条目,每次评估都会产生 O(n) 的复杂度。后续章节将介绍如何通过索引、集合和缓存来优化这类查询。
2.6 常见误区与最佳实践
| 误区 | 说明 | 正确做法 |
|---|---|---|
| 混淆 data 与 input | 将请求相关的数据硬编码到 data 中,或将配置数据放在 input 中 | data 存持久化配置,input 存请求数据 |
| 过度使用全量遍历 | 在规则中频繁使用 data.list[_] 遍历大数组 |
使用集合(Set)或索引优化查询 |
| 忽略默认值 | 未定义 default 规则,导致查询返回 undefined |
始终为布尔规则定义 default allow = false |
| 规则体过于复杂 | 单个规则体包含过多条件,难以调试和优化 | 拆分为多个子规则,使用中间变量 |
| 忽略 input 验证 | 直接使用 input 字段而不检查是否存在 | 使用 input.request.object.metadata.name != "" 等前置检查 |