OpenTofu 基于 OCI 注册表的 Provider 镜像安装:oci_mirror配置、OCI 制品布局与源码实现全解析
【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu
本文基于 OpenTofu 仓库中 OCI registries RFC 的 Providers in OCI 章节撰写,并结合 OCI 设计考量、Provider 安装实现细节 以及
internal/getproviders、internal/command/cliconfig等目录下的实际源码与测试展开讲解。读者读完本文后,将能够:在 OpenTofu CLI 配置中编写oci_mirror块,把任意来源地址的 Provider 重定向到自建的 OCI 注册表(含 air-gapped 与 Amazon ECR 场景);理解 OpenTofu 在 OCI 中存储 Provider 制品时必须遵循的多平台 index manifest 布局与版本标签规则;并掌握校验和、依赖锁文件、签名与未来工具化方向。
为什么要把 Provider 放进 OCI 注册表
OpenTofu 的 Provider 使用形如HOSTNAME/NAMESPACE/TYPE的虚拟地址来标识,例如hashicorp/kubernetes(其完整地址为registry.opentofu.org/hashicorp/kubernetes)。这类地址刻意与实际的下载 URL 解耦:默认情况下,OpenTofu 通过远程服务发现与 Provider Registry 协议联系来源注册表(origin registry)来发现"官方"发布包的位置,但运营商可以单方面地为部分或全部地址重配置安装策略,此时地址中的主机名仅作为 Provider 的唯一标识符参与 OpenTofu 的状态与锁文件追踪,真正的安装包则来自完全不同的位置。
这一解耦特性正是 OCI 镜像源方案的基石。很多大型组织运行着 air-gapped(物理隔离)环境,而由于 Kubernetes 的普及,OCI 注册表(历史上称 Docker 注册表)几乎处处可用且无额外合规负担;相比之下,自行部署一套 OpenTofu/Terraform 注册表则需要额外的软件与合规成本。此外,Harbor 等注册表自带镜像安全扫描(Trivy、Clair)与 SBOM 能力,这是传统 Provider 注册表不具备的。关于这些背景与替代方案的完整论述,可参见 OCI registries RFC 主文档 与 设计考量章节。
需要特别强调的是:当前迭代只聚焦于"镜像(mirroring)"用例,即把 OCI 注册表用作某个来源为传统 OpenTofu Provider 注册表的 Provider 的替代安装源。首个版本尚不把 OCI 注册表作为新的"来源注册表"(origin registry)默认安装方式,也不打算支持 OCI 制品签名——因为镜像总是由运营商显式配置、默认被信任。这也意味着,虽然理论上可以按 OpenTofu 约定的 manifest 格式手工构造并发布自研 Provider 的制品,但该场景的完善支持要留待后续版本。
OpenTofu 的两种 Provider 安装方式:direct 与 mirror
OpenTofu 的 Provider 安装方式大致分两类:
- direct(直接):根据 Provider 源地址的主机名部分去查找 Provider,要求该主机名提供 OpenTofu Provider Registry 协议服务。
- mirror(镜像):源地址中的主机名只作为 Provider 唯一标识的一部分,物理分发包托管在第二个位置(通常是组织内私有部署)。OpenTofu 现有的镜像类方法包括
filesystem_mirror(本地目录镜像)与network_mirror(基于 OpenTofu 自有协议的 HTTP 网络镜像),而本文要讲的oci_mirror正是这一家族的新成员——它是network_mirror的 OCI Distribution 协议等价物。
在仓库源码中,每种安装方法块都对应getproviders包中一个Source接口的实现:direct对应RegistrySource,filesystem_mirror对应FilesystemMirrorSource,network_mirror对应HTTPMirrorSource,而oci_mirror对应 oci_registry_mirror_source.go 中的OCIRegistryMirrorSource(该文件注释明确写道:"conceptually similar to HTTPMirrorSource, but … uses the OCI Distribution protocol when making requests instead of OpenTofu's own network mirror protocol")。这四类安装位置统一由 provider_installation.go 中的ProviderInstallationLocation接口及其具体实现描述。
配置oci_mirror:CLI 配置文件详解
要启用 OCI 镜像源,运营商需要修改 OpenTofu CLI 配置文件(即~/.opentofu/config.tofu之类的 CLI 配置),在provider_installation块中写入至少一个oci_mirror块。RFC 给出的完整示例:
provider_installation { oci_mirror { repository_template = "example.com/examplenet-mirror/${namespace}-${type}" include = ["example.net/*/*"] } oci_mirror { repository_template = "example.com/exampleorg-mirror/${namespace}-${type}" include = ["example.org/*/*"] } direct { exclude = ["example.net/*/*", "example.org/*/*"] } }该配置的效果:凡是源地址匹配某个oci_mirror块include参数的 Provider,都会被重定向到对应的 OCI 注册表安装;不归属于这两个配置主机名的其他 Provider,则因末尾(可选的)direct方法而照常从来源注册表安装。direct块中的exclude用于避免与已由 OCI 镜像覆盖的地址发生歧义。
repository_template模板语法与三个变量
模板是必需的,因为 OCI 注册表地址的工作方式与 OpenTofu Provider 地址不同,且部分注册表要求特定的仓库路径布局。repository_template必须为include参数中以通配符(*)写出的每一个源地址分量提供替换符:
${hostname}:源地址中的主机名。对于只有两段的源地址(如hashicorp/kubernetes),默认值为registry.opentofu.org。${namespace}:命名空间,即example.net/foo/bar中的foo。${type}:Provider 类型,即example.net/foo/bar中的bar。
从源码看,模板校验相当严格。在 provider_installation.go 的decodeOCIMirrorInstallationMethodBlock中:
repository_template是必填参数,缺失会直接报错(见 L348-L355)。- 模板使用 HCL 2 的模板引擎解析(
hclsyntax.ParseTemplate,L371),支持任意合法的 HCL 模板表达式(包括条件、拼接等)。 prepareOCIMirrorRepositoryMapping(L396 起)会静态扫描模板中引用的符号:只允许hostname、namespace、type三个变量,出现其他符号立即报错(L399-L419)。- 若
include通配了某个分量(即该分量未在 include 中被精确限定),则模板必须引用对应的变量,否则映射会产生歧义而被拒绝。例如include = ["registry.opentofu.org/*/*"]通配了 namespace 与 type,模板就必须包含${namespace}与${type};而include = ["registry.opentofu.org/opentofu/foo"]精确指定了全部三段,模板就可以完全不用变量(见测试配置 provider-installation-oci 中四个oci_mirror块由粗到细的写法)。 - 模板最终必须求值为字符串(非 null),且求值结果必须能解析为"注册表主机名 + 斜杠 + 仓库名"的合法 OCI 仓库地址(通过
ociauthconfig.ParseRepositoryAddressPrefix校验,L514)。
典型场景:为 air-gapped 环境镜像registry.opentofu.org
如今绝大多数常用 Provider 都隶属于registry.opentofu.org这一由 OpenTofu 项目运营的公共注册表。对于无法直连该注册表的 air-gapped 系统,组织可以把所用 Provider 的包从其来源位置复制到某个 OCI 注册表下的系统性仓库命名方案中,然后配置一个匹配registry.opentofu.org/*/*的oci_mirror块:
provider_installation { oci_mirror { repository_template = "example.com/opentofu-provider-mirror/${namespace}_${type}" include = ["registry.opentofu.org/*/*"] } }配置完成后,当初始化一个依赖hashicorp/kubernetesProvider 的模块时,OpenTofu 会从example.com上 OCI 注册表中的opentofu-provider-mirror/hashicorp_kubernetes仓库安装该 Provider,而完全不再直连registry.opentofu.org——registry.opentofu.org仅作为 Provider 唯一标识的一部分存在,不再是一个物理网络位置。
注意命名分隔符的差异:本例仓库名使用下划线(
${namespace}_${type})而不是斜杠。OCI 仓库名可以包含斜杠层级(如opentofu-provider-mirror/hashicorp/kubernetes),但许多注册表对路径层级有约定或限制,选择哪种分隔由运营商的repository_template决定。RFC 中的多个示例同时展示了-、_与/三种分隔风格,说明该模板具有完全的表达自由。
实战示例:映射到 Amazon ECR
RFC 提供了一个非常实用的 ECR 提示:ECR 注册表的地址格式为aws_account_id.dkr.ecr.region.amazonaws.com/repository:tag,因此可以这样映射:
provider_installation { oci_mirror { repository_template = "YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/${namespace}_${type}" include = ["registry.opentofu.org/*/*"] } }例如hashicorp/kubernetes会被安装自YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/hashicorp_kubernetes仓库,而无需改写任何模块中的 Provider 源地址——这正是"源地址与安装位置解耦"设计带来的核心收益,也是该方案对比"逐一改写模块"路径的显著优势。
OCI 中的存储布局:Provider 制品的规范
OpenTofu 从 ORAS(OCI Registry As Storage)的制品存储方式中汲取了灵感,但在本文档撰写时 ORAS 对多平台 index manifest 的支持仍在推进中,因此 OpenTofu 直接在自身内实现了一个兼容的 index manifest 布局。其规范要点如下:
- Zip 文件直接作为 OCI blob:每个 OpenTofu Provider 的 OS/架构组合(例如
linux_amd64)会被存储为一个.zip文件,直接作为 OCI blob。OpenTofu不使用容器镜像常见的 tar 文件格式。 - 每平台一个 image manifest,单一
archive/zip层:每个 OS/架构都必须有一个 image manifest,其中包含一个mediaType为archive/zip的层,该层内容是 Provider 开发者官方分发包逐字节的副本。 - 顶层必须是指 index manifest:制品的顶层 manifest 必须是 index manifest,为该 Provider 版本支持的每个 OS/架构各包含一个条目,并且
artifactType属性必须设置为application/vnd.opentofu.provider,OpenTofu 才会把它当作合法的 Provider 镜像接受。- 巧合的是,OCI index manifest 使用的操作系统与 CPU 架构代码与 OpenTofu 完全一致,因为两者都继承了 Go 语言工具链的命名方案:OpenTofu 的
linux_amd64平台在 OCI index manifest 条目中表现为"os": "linux"、"architecture": "amd64"。
- 巧合的是,OCI index manifest 使用的操作系统与 CPU 架构代码与 OpenTofu 完全一致,因为两者都继承了 Go 语言工具链的命名方案:OpenTofu 的
- 版本标签规则:Provider 制品必须发布在名称与上游版本号一致的 tag 上。OpenTofu 会忽略所有无法识别为 semver 版本号的 tag(包括
latest)。由于 semver 用+表示"构建元数据",而该字符不允许出现在 OCI tag 名中,因此版本号中的任何+都必须替换为_再作为 tag 名。 - 清单纯度与容错:index manifest 的
manifests数组中的所有条目都被视为 Provider 包,因此不得再列出其他 manifest;但单个 image manifest 可以附带mediaType不同于archive/zip的额外层,OpenTofu 会忽略它们。每个 image manifest 必须恰好有一个archive/zip层。
此外,发布者可以借助 OCI Distribution 的subject属性,发布引用 index manifest 或某个 image manifest 的附加制品,从而通过 OCI 的"referrers"列表 API 让子制品可被发现。这通常用于给制品附加签名或 SBOM 等元数据,而无需直接修改原制品。OpenTofu 首版不会消费 referrer 制品,但未来版本可能开始使用特定artifactType的 referrers。
⚠️ 强制要求:OCI 中的 Provider 制品必须使用多平台(index)manifest。OpenTofu 会拒绝下载和使用非多平台的制品作为 Provider manifest。与此相对,Modules in OCI 章节规定模块禁止使用多平台 manifest——Provider 与模块在 OCI 布局上恰好是互补的两极。
源码视角:这些规则如何被执行
oci_registry_mirror_source.go 用常量与校验函数把上述规则落到了实处:
ociIndexManifestArtifactType = "application/vnd.opentofu.provider"(L33)与ociPackageManifestArtifactType = "application/vnd.opentofu.provider-target"(L58)分别定义了顶层 index manifest 与每个平台 image manifest 的 artifactType。后者是仓库实际实现相对 RFC 的一个细化:index 中每个平台条目需要带provider-target类型并附platform对象(含os、architecture),OpenTofu 会静默忽略类型不符或缺少 platform 的条目,以便未来版本在不破坏兼容性的前提下扩展。- tag 解析时(
fetchOCIDescriptorForVersion,L474 起)会先把版本号中的+替换为_生成 tag 名,然后检查返回描述符的 artifactType 与 mediaType:若 tag 直接指向 image manifest(而非 index manifest),会报出"providers require an index manifest for multi-platform support"的专门错误;若指向模块包(application/vnd.opentofu.modulepkg),则会提示用户混淆了 Provider 与模块。 - 平台选择(
selectOCIImageManifest,L648 起)要求恰好一个条目匹配当前目标平台(os/architecture 均需匹配,os.version暂不支持),多个匹配或零匹配分别产生歧义与ErrPlatformNotSupported错误。 - 层选择(
selectOCILayerBlob,L718 起)要求 image manifest 中恰好一个archive/zip层,其余 mediaType 的层会被忽略并计数。 - 为避免恶意注册表耗尽内存,manifest 内容有 4 MiB 的大小上限(
ociImageManifestSizeLimitMiB,L60-L65),且每次取回都会用描述符中的 digest 校验内容一致性(fetchOCIManifestBlob,L749 起)。
安装流程的源码实现:从列出版本到定位 Zip 包
OCIRegistryMirrorSource实现了getproviders.Source接口的两个核心方法(L160-L378):
AvailableVersions:通过 ORAS 客户端枚举 OCI 仓库的全部 tag(Tags),把每个 tag 中的_替换回+后尝试解析为 semver 版本;解析失败的 tag(如latest)直接忽略。仓库不存在(404 或NAME_UNKNOWN/NAME_INVALID错误码)会被转译为ErrProviderNotFound,从而让MultiSource可以正确地把多个安装源的结果混合起来,只有当所有可选源都找不到时才报错(见errRepresentsOCIProviderNotFound,L227 起)。PackageMeta:注释中明确描述了五步流程(L297-L331):- 把版本号转成 tag 名并解析其描述符;
- 取回该描述符指向的 blob(应为 index manifest),拿到每个平台条目的描述符;
- 选出与请求平台匹配的平台描述符;
- 取回第二层描述符指向的 blob(应为 image manifest),并从其层列表中选出承载 zip 包的
archive/zip层; - 返回一个
PackageOCIBlobArchive类型的PackageMeta,其 Location 就是该 zip blob。
其中,OCI blob 的
sha256:digest 会被直接转换为 OpenTofu 风格的包校验和(hashFromOCIDigest),用于生成"checksum verified"认证结果——这正是下一节要讲的依赖锁文件机制能够与 OCI 天然衔接的关键。
OCIRegistryMirrorSource通过依赖注入获得两个函数(NewOCIRegistryMirrorSource):resolveRepositoryAddr(把 Provider 源地址映射为"注册表域名 + 仓库名",由package cliconfig基于repository_template求值提供)与getRepositoryStore(返回预配置了凭据的 ORAS 仓库客户端,由package main结合ociauthconfig.CredentialsConfigs提供)。RFC 附录 9-provider-implementation-details.md 中给出的NewOCIMirrorSource函数签名与上述实现基本一致,并注明该签名是示意性的、具体形态在实现阶段确定。
校验和、依赖锁文件与签名策略
OpenTofu 在依赖锁文件中记录每个已安装 Provider 的选定版本,以及该版本被认为可接受的一组校验和。传统 Provider 注册表协议使用.zip归档作为包,并要求开发者签名覆盖包含该版本全部平台.zip的 SHA256 校验和文档;这些校验和对应锁文件中的zh:前缀校验和,而基于已解包内容生成的通用校验和则记为h1:。
OCI 布局之所以刻意选用archive/zip层,正是为了让 blob 与开发者的官方签名.zip包逐字节一致(参见设计考量章节对"放弃 tar 布局"原因的分析:tar 布局会让镜像后的包产生与上游不同的校验和,破坏"锁文件先由来源注册表生成、再在镜像环境中校验"的工作流)。由此,OpenTofu 可以直接把每个层的sha256:digest 转译为zh:风格校验和写进锁文件,无需下载后重新计算。
首版实现不支持 OCI 制品签名(这与 Provider Network Mirror 协议同样不支持签名保持一致,且镜像总是由运营商显式信任)。没有签名时,锁文件只记录实际下载制品本身的校验和(h1:与zh:都有),这与今天从不签名源安装的保证一致。未来若支持对 index manifest 签名,OpenTofu 就能像现在对待注册表签名那样,用签名来证明把全部平台制品的zh:校验和都纳入锁文件的合理性,并在tofu init输出中宣告签名密钥 ID,由运营商在提交锁文件前自行核对该密钥 ID。
另外需要留意tofu providers lock的行为:该命令默认忽略运营商配置的安装方法、始终尝试从来源注册表安装,以支持"先取官方校验和、再到镜像源校验一致性"的工作流;它带有-net-mirror与-fs-mirror选项用于少数"仅镜像可得"的场景,但首版尚未提供对应的-oci-mirror选项(RFC 明确将其排除以控制范围、减少破坏性变更压力,并计划在后续创建独立的 feature request)。
发布与镜像 Provider:现状与工具化路线
撰写 RFC 时,尚没有第三方工具能以本规范所需的格式推送 OCI 制品。OpenTofu 不希望该提案依赖 ORAS 的实现进度,因此计划:若 ORAS 的多平台 manifest 提案未能在 OpenTofu 完成 OCI 镜像安装实现前发布,则先发布手工编写多平台 index manifest 并用 ORAS 底层 manifest 命令推送的指引,其效果等价于 ORAS Multi-arch Image Management 提案中的oras manifest index create命令。
同时,OpenTofu 正在考虑提供内置的自动镜像工具,类似于现有tofu providers mirror命令自动填充文件系统镜像目录的做法,用于把一组 Provider 从来源注册表自动镜像进 OCI 镜像;但为控制首版范围、给反馈留出调整空间,该能力同样被推迟到后续版本。
测试验证:仓库中的证据
仓库为该特性提供了从配置解析单元测试到端到端测试的多层验证:
- 配置解析:
internal/command/cliconfig/testdata/provider-installation-oci展示了四个由粗到细的oci_mirror块(通配 hostname/namespace/type 的各级粒度均给出合法写法),配套的provider-installation-oci-missinghostname、-missingnamespace、-missingtype、-valueerror、-typeerror、-dynerror等测试夹具则覆盖了模板校验的各种失败路径。 - 端到端测试:provider_oci_mirrors_test.go 中的
TestProviderOCIMirrors会启动一个本地 fake OCI 注册表(基于testdata/oci-provider-mirror/fake-registry下的 OCI 布局夹具),然后以配置了oci_mirror的 CLI 配置运行真实的tofu init,验证"在 CLI 配置中配置 OCI 镜像源确实会让系统使用该源"这一依赖装配链路(测试注释明确建议:属于getproviders、cliconfig、ociauthconfig包的行为应优先写单元测试,e2e 测试仅作为最后手段)。
总结与后续路线
本文完整覆盖了 OpenTofu 通过oci_mirror从 OCI 注册表安装 Provider 的方方面面:
- 配置:
provider_installation块中的oci_mirror与repository_template模板(${hostname}/${namespace}/${type})、include/exclude匹配、与direct方法的配合; - 布局:
archive/zip单层 image manifest +artifactType为application/vnd.opentofu.provider的多平台 index manifest、semver 版本 tag(+→_)、referrers 扩展; - 实现:
OCIRegistryMirrorSource的 tag 枚举与五步包定位流程、严格的模板与 manifest 校验、zh:/h1:校验和与依赖锁文件的衔接; - 路线:手工推送指引、
tofu providers lock -oci-mirror选项、自动镜像工具与签名支持均明确推迟到后续版本。
对运营商而言,这意味着:只要组织内已存在一个 OCI 注册表(ECR、Harbor、自建 Distribution 等),就可以在不改写任何模块的前提下,用几行 CLI 配置把 Provider 分发全面迁移到该注册表上,从而服务 air-gapped 环境并复用现有的安全扫描与合规基础设施。若要深入了解 OCI 协议基础、认证配置与模块侧的对应设计,可继续阅读同系列文档 OCI 入门、认证 与 Modules in OCI。
【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考