External Secrets Operator 的 ClusterExternalSecret 详解:多命名空间 ExternalSecret 分发、直连轮询优化与强制同步
【免费下载链接】external-secretsExternal Secrets Operator reads information from a third-party service like AWS Secrets Manager and automatically injects the values as Kubernetes Secrets.项目地址: https://gitcode.com/GitHub_Trending/ex/external-secrets
ClusterExternalSecret是 External Secrets Operator(ESO)提供的一个集群作用域(cluster-scoped)资源,用于在指定命名空间中批量创建和管理ExternalSecret。本文以官方 API 文档为核心,结合仓库源码(CRD 类型定义、控制器实现与运行时工具)深入讲解其配置字段、命名空间选择机制、大规模场景下的 provider 调用优化(fan-out 模式)、强制同步与弃用项,帮助你用它把一份密钥安全地复制到任意多个命名空间,同时避免对上游 provider 的重复轮询。
什么是 ClusterExternalSecret
ClusterExternalSecret是一个集群级资源,它的作用是把一份ExternalSecret规格批量"分发"到符合条件的一个或多个命名空间。你只需要定义一次密钥来源与目标 Secret 的形态,ESO 控制器就会在每一个匹配的命名空间里创建对应的ExternalSecret,再由这些ExternalSecret各自把远程密钥同步为命名空间内的 KubernetesSecret。
它特别适合以下场景:
- 多租户 / 多环境集群中,需要在多个命名空间里部署同一份配置或凭证;
- 应用发布在多个命名空间,但密钥源(如 AWS Secrets Manager、Vault)只有一个;
- 希望用"标签驱动"的方式声明"这些命名空间都需要这份密钥",命名空间一打标签即自动生效。
一个需要注意的约束是:如果目标命名空间中已存在同名资源,控制器会报错(对应状态里的failedNamespaces,原因通常是external secret already exists in namespace)。
从资源定义看,ClusterExternalSecret的类型声明位于 apis/externalsecrets/v1/clusterexternalsecret_types.go,其控制器实现在 pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go。API 组为external-secrets.io/v1,短名(shortName)为ces(见 apis/externalsecrets/v1/clusterexternalsecret_types.go#L119),因此文档中常用kubectl annotate ces ...来操作它。
工作方式与核心配置示例
下面是官方文档中的完整ClusterExternalSecret示例(原始文件为 docs/snippets/full-cluster-external-secret.yaml):
apiVersion: external-secrets.io/v1 kind: ClusterExternalSecret metadata: name: "hello-world" spec: # 生成的 ExternalSecrets 使用的名称。 # 省略时默认使用 ClusterExternalSecret 自身的名称。 externalSecretName: "hello-world-es" # 可选:为每个被创建的 ExternalSecret 设置 labels 和 annotations。 externalSecretMetadata: labels: {} annotations: {} # 基本的 label selector,用于选择要部署 ExternalSecrets 的命名空间。 # 已弃用(Deprecated):请改用 namespaceSelectors。 # namespaceSelector: # matchLabels: # cool: label # 一组基本 label selector,用于选择要部署 ExternalSecrets 的命名空间。 # 多个 selector 之间是“或”(OR)关系:只要任意一个匹配,就会在该命名空间部署。 namespaceSelectors: - matchLabels: cool: label # 按名称选择命名空间,与 namespaceSelectors 匹配的结果取“或”。 # 已弃用(Deprecated):请改用 namespaceSelectors。 # namespaces: # - my-namespace # ClusterExternalSecret 自身的调和(reconcile)频率。 # 决定控制器多久检查一次匹配的命名空间中 ExternalSecrets 是否存在。 # 省略时使用控制器的默认 requeue 间隔。 refreshTime: "1m" # 待创建 ExternalSecrets 的 spec 模板,内容与 ExternalSecret 示例一致。 externalSecretSpec: secretStoreRef: name: secret-store-name kind: SecretStore # RefreshPolicy 决定 ExternalSecret 如何刷新: # - CreatedOnce: 仅当 Secret 不存在时创建,之后不再更新 # - Periodic:(默认)按 refreshInterval 指定的间隔同步 # - OnChange: 仅当 ExternalSecret 的 metadata 或 spec 变化时同步 refreshPolicy: Periodic refreshInterval: "1h0m0s" target: name: my-secret creationPolicy: 'Merge' template: type: kubernetes.io/dockerconfigjson metadata: annotations: {} labels: {} data: config.yml: | endpoints: - https://{{ .data.user }}:{{ .data.password }}@api.exmaple.com templateFrom: - configMap: name: alertmanager items: - key: alertmanager.yaml data: - secretKey: secret-key-to-be-managed remoteRef: key: provider-key version: provider-key-version property: provider-key-property dataFrom: - key: provider-key version: provider-key-version property: provider-key-property status: # 列出创建 ExternalSecret 失败的命名空间。 # 注意:这里不会列出 ExternalSecret 自身的问题,需要单独查看那些 ExternalSecret。 failedNamespaces: - namespace: "matching-ns-1" # 下面是最常见的失败原因之一 reason: "external secret already exists in namespace" # 所有匹配且成功部署的命名空间都列在这里 provisionedNamespaces: - "matching-ns-3" - "matching-ns-2" # 唯一的 condition 类型是 Ready。 # 全部匹配命名空间同步成功时 status 为 "True"; # 有任意一个命名空间失败时 status 为 "False"(失败的列在上面的 failedNamespaces)。 conditions: - type: Ready status: "False" message: "one or more namespaces failed" lastTransitionTime: "2022-01-12T12:33:02Z"各字段的行为说明
对照 apis/externalsecrets/v1/clusterexternalsecret_types.go 中的ClusterExternalSecretSpec,可以把上述字段归纳为几类:
| 字段 | 类型 | 说明 | 源码要点 |
|---|---|---|---|
externalSecretSpec | ExternalSecretSpec(必填) | 所有被创建 ExternalSecret 的规格模板 | 控制器直接把它复制到每个 ExternalSecret 的spec |
externalSecretName | string(可选) | 生成的 ExternalSecret 名称,省略时默认使用 CES 自身名称 | 有MinLength=1、MaxLength=253与 DNS 子域名正则校验;改名时会先删除旧名称的 ExternalSecret(见控制器reconcile) |
externalSecretMetadata | 结构体 | 给每个生成的 ExternalSecret 附加的 labels / annotations | 见ExternalSecretMetadata类型 |
namespaceSelector | *LabelSelector(已弃用) | 单个标签选择器 | 控制器会把旧字段与新字段合并后一起使用 |
namespaceSelectors | []*LabelSelector(推荐) | 多个标签选择器,OR关系 | 与namespaces的结果再取 OR |
namespaces | []string(已弃用) | 按名称精确选择命名空间 | 在GetTargetNamespaces中被转换为kubernetes.io/metadata.name的标签选择器 |
refreshTime | *metav1.Duration | CES 自身的重新调和间隔,省略则用控制器默认 requeue 间隔 | 控制器Reconcile返回RequeueAfter: refreshInt |
关于命名空间的最终匹配逻辑,可以看 runtime/esutils/utils.go#L687 的GetTargetNamespaces:它先把namespaces列表转换为kubernetes.io/metadata.name的标签选择器,再与namespaceSelectors合并,逐个执行List并去重。也就是说,多个选择器之间全部是"或"的关系,命中任何一个即被选中。而命名空间标签一旦变化,控制器会通过NamespacePredicate()(runtime/esutils/utils.go#L726)触发对应 CES 的重新调和——因此"给命名空间打标签 → 自动生成 ExternalSecret"是即时生效的。
状态字段
failedNamespaces:创建/更新失败的命名空间及原因。常见原因external secret already exists in namespace来自控制器对同名 ExternalSecret 的属主检查(clusterexternalsecret_controller.go#L226):如果目标命名空间里已存在同名 ExternalSecret 且不是该 CES 创建的,就会报错并跳过;provisionedNamespaces:匹配且成功部署的命名空间列表;conditions:唯一的 condition 类型是Ready。全部成功时为True,任一失败为False,错误信息统一为one or more namespaces failed(构造逻辑见 pkg/controllers/clusterexternalsecret/util.go)。
另外,控制器还会维护两条 finalizer:CES 自身的externalsecrets.external-secrets.io/clusterexternalsecret-cleanup,以及按 CES 名称命名的命名空间 finalizerexternalsecrets.external-secrets.io/ces-<cesName>(见 clusterexternalsecret_controller.go#L69 与buildCESFinalizer)。这意味着删除 CES 时,它创建的所有 ExternalSecret 都会被清理,避免孤儿资源,也防止命名空间删除被阻塞。
大规模命名空间集合:如何减少 provider 调用
这是ClusterExternalSecret最重要的设计考量之一。一个ClusterExternalSecret会为每个匹配的命名空间创建一个ExternalSecret,而每个ExternalSecret都会按照自己的refreshInterval独立轮询上游 provider。
这意味着:provider API 调用次数与匹配的命名空间数量成正比。如果选择器匹配了几十个甚至上百个命名空间,每个命名空间的 ExternalSecret 都在自己的刷新周期内独立访问 AWS Secrets Manager / Vault 等后端,成本会线性增长,也很容易触及 API 速率限制。这是该设计的已知特性(known characteristic),官方文档明确指出了这一点。
如果你的选择器匹配的不只是寥寥几个命名空间,官方推荐的做法是:从上游 provider 只拉取一次到集群内的单个 Secret,再用 Kubernetes provider 通过ClusterExternalSecret扇出(fan-out)到所有目标命名空间,而不是让每个命名空间都直连云厂商。
Fan-out 模式三步走
官方文档给出了完整的推荐流程(示例见 docs/snippets/cluster-external-secret-fanout.yaml):
第 1 步:一个命名空间级ExternalSecret从上游 provider 拉取,写入一个位于专用"源命名空间"的 Secret。
apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: shared-credentials namespace: eso-fanout-source spec: refreshInterval: "1h" secretStoreRef: name: my-upstream-store kind: ClusterSecretStore target: name: shared-credentials dataFrom: - extract: key: path/to/shared-credentials第 2 步:一个使用 Kubernetes provider 的ClusterSecretStore指向源命名空间中的那个 Secret。
apiVersion: external-secrets.io/v1 kind: ClusterSecretStore metadata: name: shared-credentials-store spec: provider: kubernetes: remoteNamespace: eso-fanout-source server: caProvider: type: ConfigMap name: kube-root-ca.crt namespace: eso-fanout-source key: ca.crt auth: serviceAccount: name: eso-fanout-reader namespace: eso-fanout-source第 3 步:ClusterExternalSecret引用这个ClusterSecretStore,把 Secret 复制到每个匹配的命名空间。
apiVersion: external-secrets.io/v1 kind: ClusterExternalSecret metadata: name: shared-credentials spec: externalSecretName: shared-credentials namespaceSelectors: - matchLabels: shared-credentials: "true" externalSecretSpec: refreshInterval: "1h" secretStoreRef: name: shared-credentials-store kind: ClusterSecretStore target: name: shared-credentials dataFrom: - extract: key: shared-credentials这个模式下,无论匹配多少个命名空间,上游 provider 都只被第 1 步的那个源 ExternalSecret 调用一次,其余命名空间全部通过集群内的 Kubernetes provider 复制数据,provider 负载大幅下降。Kubernetes provider store 所需的 ServiceAccount 与 RBAC 配置,与 Kubernetes provider 文档 中描述的一致。
什么时候仍然可以直接直连
官方文档强调:直接使用ClusterExternalSecret对接云厂商 store 仍然适用以下场景:
- 命名空间集合很小(只有少量命名空间时,线性增长的调用量可以接受);
- 刻意需要按命名空间隔离刷新(例如不同命名空间期望不同的刷新节奏,或者个别命名空间需要独立拉取)。
这是一个"如何减少 provider 负载"的推荐,而不是对前面直连模式的弃用(deprecation)——两种用法都会长期支持。
同步对应的 ExternalSecrets:定时刷新与强制同步
ClusterExternalSecret对已生成 ExternalSecret 的刷新控制分为两层:
- 定期刷新:通过
refreshPolicy与refreshInterval控制。注意这两个字段位于externalSecretSpec中,随模板复制给每个 ExternalSecret,决定每个 ExternalSecret 自身的同步节奏;而refreshTime只控制 CES 控制器"检查匹配命名空间、补齐缺失 ExternalSecret"的频率。 - 临时/手动同步:可以随时通过设置、更新或删除
external-secrets.io/force-sync注解来触发一次 ad-hoc 同步:
kubectl annotate ces my-ces external-secrets.io/force-sync=$(date +%s) --overwrite该注解的常量定义在 apis/externalsecrets/v1/externalsecret_types.go#L773(AnnotationForceSync = "external-secrets.io/force-sync")。对 CES 注解的任何改动都会被同步到它所拥有的全部 ExternalSecret 上——控制器在createOrUpdateExternalSecret中会读取 CES 上的该注解值并覆写到生成的 ExternalSecret 上,CES 上注解被删除时也会从 ExternalSecret 上同步删除(见 clusterexternalsecret_controller.go#L397)。$(date +%s)每次生成不同的时间戳,确保注解值发生变化、从而可靠触发一次新的同步。
弃用项说明
namespaceSelector(单数)
字段namespaceSelector(单数形式)已被namespaceSelectors(复数形式)取代,并将在未来的版本中移除。迁移方式很简单:
- 把单个
namespaceSelector.matchLabels改写成namespaceSelectors列表中的一项; - 同样地,
namespaces(按名称选择)字段也已弃用,建议改用namespaceSelectors来表达同样的选择逻辑(例如用kubernetes.io/metadata.name标签)。
迁移期间旧字段仍会被控制器识别——从 clusterexternalsecret_controller.go#L154-L158 可以看到,控制器会把已弃用的namespaceSelector与新的namespaceSelectors合并后再统一计算目标命名空间,因此新老字段共存不会导致行为差异。但请尽早迁移,避免未来版本升级时资源失效。
与相关资源的关系与建议阅读
- 想了解 CES 生成的单个资源如何工作,可阅读 ExternalSecret API 文档;
- fan-out 模式依赖的 Kubernetes provider 详见 Kubernetes provider 文档;
- 多租户场景下的命名空间隔离实践可参考 multi-tenancy 指南;
- 端到端测试与完整清单可查看 tests/clusterexternalsecret_test.yaml,控制器的单元测试见 pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go。
实践要点小结:命名空间选择用namespaceSelectors(OR 语义);密钥分发用 fan-out 模式避免 provider 调用随命名空间数线性膨胀;按需同步用external-secrets.io/force-sync注解;namespaceSelector/namespaces两个旧字段尽快迁移到namespaceSelectors。
【免费下载链接】external-secretsExternal Secrets Operator reads information from a third-party service like AWS Secrets Manager and automatically injects the values as Kubernetes Secrets.项目地址: https://gitcode.com/GitHub_Trending/ex/external-secrets
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考