OpenTofu 通过 OCI 镜像仓库分发与安装 Provider 与 Module:从 RFC 设计到落地实现
【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu
本文基于 rfc/20241206-oci-registries.md 及其 10 个配套章节整理,系统讲解 OpenTofu 如何复用 OCI Distribution 协议(即 Docker/容器镜像仓库协议)来分发 Provider 与 Module。文中涵盖社区调研结论、OCI 协议速览、Provider 镜像源(
oci_mirror)与 Module 新地址方案(oci://)的配置方法、认证体系、制品布局规范、签名/SBOM 等安全议题,以及贯穿源码级的实现细节,帮助读者在隔离网络、私有分发等场景中直接落地 OCI 镜像仓库作为 OpenTofu 依赖分发通道。
背景与动机:为什么 OpenTofu 要接入 OCI 镜像仓库
OCI registry(历史上也称 Docker registry)是容器生态的基础设施:它以 HTTP 接口提供manifests(描述内容元数据)与blobs(实际数据),通过分层(layer)机制实现高效的增量发布。得益于其通用架构,社区已经出现如 ORAS(OCI Registry As Storage)等项目,将 OCI 仓库用作容器镜像之外的任意数据存储。
OpenTofu 自己的 Provider Registry Protocol 与 Module Registry Protocol 与 OCI 标准化几乎是同期演进,但设计目标不同:
- OpenTofu 的 Provider 注册协议关注制品签名,并将索引(index)与下载(download)解耦;
- OCI Distribution 协议更关注层拉取效率等容器场景问题;
- 截至 RFC 撰写时,OCI 制品签名仍处于发展中(sigstore/cosign 等项目正在解决)。
企业用户的现实痛点
- 隔离网络(air-gapped):大型组织常存在无法访问公共仓库的网络环境。而 OCI 镜像仓库在 Kubernetes 普及的今天几乎处处可用(云厂商提供托管服务、Docker Hub、GitHub Container Registry 等),无需额外的合规负担;
- 无需额外服务:运行一个 OpenTofu/Terraform 私有仓库需要部署独立软件,而 OCI 仓库通常已经就绪;
- 安全能力开箱即用:Harbor 等仓库自带漏洞/许可证扫描(Trivy、Clair),并能生成 SBOM(软件物料清单)——这些是 OpenTofu/Terraform 注册表今天不具备的。
RFC 明确将 OCI 定位为运营商配置的 Provider 镜像与Module 的新地址方案,而非替代现有注册协议成为默认安装方式。
社区调研:OCI 需求与使用现状
RFC 附带的 2-survey-results.md 记录了面向社区的一次调研(103 份有效回答),关键结论如下。
Provider 分发需求(82% 需要私有仓库)
- 96/103(93%)对使用 OCI 分发 Provider 感兴趣;
- 其中 85 人(82%)希望用 OCI 镜像现有公共 Provider(64%)或发布私有 Provider(61%);
- 13 人(13%)正在创作公共 Provider 并希望发布到 OCI 仓库;
- 有受访者提醒:通用 OCI 仓库不会像官方仓库那样自动生成并发布 Provider 文档。
Module 分发需求(90% 需要私有仓库)
- 93/103(90%)对 OCI 分发 Module 感兴趣,其中 84% 希望用于私有发布(79%)或镜像公共 Module(59%);
- 当前 Module 分发方式分布:私有 Git 仓库 72、默认公共 Registry 37、公共 Git 仓库 37、私有 OpenTofu/Terraform 仓库 32 等。
安全扫描与隔离网络
- 85% 的受访者希望使用安全扫描工具(Trivy 47、TFLint 34、Snyk 26……);
- 33%(35 人)存在不同程度的隔离网络环境,这是 RFC 中最令人意外的发现,也是 OCI 镜像方案的核心驱动场景。
期望使用的仓库实现
GitHub(ghcr.io)57、AWS ECR 54、Azure Container Registry 31、自建 Harbor 27、自建 registry 27、GCR 25、Docker Hub 22、自建 GitLab 22、自建 JFrog 19、GitLab.com 11、Quay.io 9 等。
工具与凭据偏好
- 27 人希望用 Podman/Docker/Containerd 工作流推送制品;24 人希望 OpenTofu 内置工具;24 人无偏好但要求能在 GitHub Actions 中工作;
- 凭据方面:55 人希望复用容器生态既有凭据;36 人使用云集成凭据;19 人使用 Docker/Podman credential helper。
OCI 协议速览:Manifest、Blob、Pull/Push 与 ORAS
若对 OCI 协议不熟悉,1-oci-primer.md 提供了一份入门指南,要点如下(其中的 curl 示例均可自行复现)。
认证:从WWW-Authenticate到 Bearer Token
访问仓库 API 时,服务端可能返回WWW-Authenticate头指示需要认证。例如访问https://ghcr.io/v2/opentofu/opentofu/tags/list会得到:
www-authenticate: Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:opentofu/opentofu:pull"realm字段即认证端点,可用GET请求换取临时 bearer token:
curl -u user:password 'https://ghcr.io/token?service=ghcr.io&scope=repository:opentofu/opentofu:pull'成功响应形如{"token":"djE6b3Blb..."},随后在请求头携带Authorization: Bearer ...即可。
Index manifest 与 Image manifest
- Index manifest(
application/vnd.oci.image.index.v1+json或application/vnd.docker.distribution.manifest.list.v2+json)包含多个平台对应的 image manifest 列表,用于分发多平台制品; - Image manifest(
application/vnd.oci.image.manifest.v1+json或application/vnd.docker.distribution.manifest.v2+json)包含config与layers列表,每个 layer 指向一个 blob。
例如 ghcr.io 上 OpenTofu 1.8.0 的 index manifest 中列出386、amd64、arm64、arm/v7等平台条目;而1.8.0-amd64的 image manifest 则列出多个application/vnd.docker.image.rootfs.diff.tar.gzip层。
Pull 类 API
- Manifest 端点:
/v2/<name>/manifests/<reference>(可基于Accept头做内容协商); - Blob 端点:
/v2/<name>/blobs/<digest>(可能返回 HTTP 重定向,客户端必须跟随Location头)。
<name>必须匹配[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*,即可由多个路径段组成(hostname+name 合计最多 255 字符);<reference>可以是 digest 或 tag(tag 匹配[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127});digest 形如scheme:value,规范要求支持sha256。
注意:OCI 生态中 “reference” 一词有歧义——
latest是仅指定 tag 的本地引用,而example.com/foo/bar/baz:latest是全限定引用。
Push 类 API
上传 blob 有两种方式:
- 先
POST /v2/<name>/blobs/uploads,再向响应Location头中的 URL 执行PUT(支持用PATCH分块上传); - 直接
POST /v2/<name>/blobs/uploads/?digest=<digest>携带预计算 digest。
blob 上传完毕后,用PUT /v2/<name>/manifests/<reference>推送 manifest(reference 为 tag 名)。
Content discovery 与_catalog
可选端点包括:
/v2/<name>/tags/list:列出仓库所有 tag(通常分页,必须跟随Link头),可用于实现基于 semver 的版本选择;/v2/<name>/referrers/<digest>:列出引用某 blob 的 manifest 列表。manifest 可通过subject属性引用另一 manifest 形成制品树,常被用于附加签名、SBOM 等事后元数据(注意 Cosign 等工具更习惯用特殊命名的 tag 而非此 API)。
非标准的/v2/_catalog端点在 Docker Registry 中用于列出全部镜像,但公共仓库通常禁用或需认证。
ORAS:非容器布局的制品存储
ORAS 通过 layer 的mediaType区分制品类型,例如将 Provider 包声明为archive/zip层,并为 manifest 设置artifactType:
{ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "artifactType": "application/vnd.opentofu.provider", "config": { "mediaType": "application/vnd.oci.empty.v1+json", "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", "size": 2, "data": "e30=" }, "layers": [ { "mediaType": "archive/zip", "digest": "sha256:54b0178fd0fcbd60ce806b2569974694af59faaf0b2c734f703753f1fdfb1f21", "size": 146839280, "annotations": { "org.opencontainers.image.title": "terraform-provider-aws_5.84.0_linux_amd64.zip" } } ], "annotations": { "org.opencontainers.image.created": "2025-02-03T11:47:34Z" } }ORAS 默认使用固定空配置 blob(application/vnd.oci.empty.v1+json,digest 恒为sha256:44136f...),ORAS 感知的软件会跳过它,但传统容器引擎(如 Docker)遇到此类非容器制品可能报 “invalid rootfs in image configuration” 等错误。可用如下命令体验:
oras push \ --artifact-type application/vnd.opentofu.provider \ localhost:5000/oras:latest \ terraform-provider-aws_5.84.0_linux_amd64.zip:archive/zip oras manifest fetch localhost:5000/oras:latest --pretty注意:撰写 RFC 时 ORAS 尚不支持多平台镜像(可借助
oras manifest子命令推送外部生成的 manifest)。
设计考虑:Provider 虚拟地址、OCI 布局与安全边界
3-design-considerations.md 阐述了若干关键设计决策。
Provider 地址是“虚拟”的
OpenTofu 中 Provider 使用HOSTNAME/NAMESPACE/TYPE形式的虚拟地址(HOSTNAME默认为registry.opentofu.org,NAMESPACE因历史原因默认为hashicorp)。该地址独立于实际下载 URL:默认情况下 OpenTofu 通过 Remote Service Discovery 与 Provider Registry Protocol 联系 origin registry,但运营商可重配置安装策略,此时 hostname 仅作为 Provider 唯一标识的一部分(例如状态快照中记录哪个 Provider 管理哪个资源,需在 plan/apply 之间保持一致)。
关键约束:不能让地址语法强制使用 OCI 协议或绑定某个具体 OCI 仓库。因此首个版本只把 OCI 作为新的 Providermirror类型(通过 CLI 配置显式启用),Provider Registry Protocol 仍是唯一默认安装方式。
OCI 布局:为什么坚持用archive/zip而不是容器层格式
RFC 最初考虑复用容器镜像的 differential-tar 布局(甚至可用 Dockerfile 构建):
FROM scratch ADD * / LABEL "org.opentofu.artifact-type"="provider"但原型验证后放弃,核心原因:包校验和(checksum)必须与上游一致。OpenTofu 用 Provider 作者签名的.zip包校验和(记录在依赖锁文件 dependency-lock 中)验证镜像包。OCI digest(sha256:方案)与 OpenTofu 的 zip-checksum 格式本质等价,因此:
- 每个 OS/架构的 Provider 包以
.zip形式直接作为 OCI blob(不用 tar); - 每个平台对应一个含单个
archive/zip层的 image manifest; - 主 manifest 为 index manifest,
artifactType设为application/vnd.opentofu.provider; - 复用现有解包代码,降低维护量,保证解包结果与其它来源一致。
SBOM:安全扫描的间接路径
Trivy 等主流扫描器开箱只支持容器层的 differential-tar 格式,暂不支持自定义制品类型;部分工具支持单镜像制品但不支持多平台 index manifest;TFLint 则完全不支持 OCI Distribution。因此 RFC 计划借道SBOM 间接扫描:由 Provider 开发者发布 SBOM,扫描器直接消费 SBOM 而非自行检查制品。但初始版本不做 SBOM 生成与消费(相关讨论见 PR #2494),优先实现 “能在 OCI 仓库里托管 Provider/Module 包” 这一基础能力。
制品签名:推迟到后续版本
现状:
- Provider 用 GPG 签名(OpenTofu 注册表作为独立于作者的验证方);Sigstore/Cosign 支持仍有 issue 待解决(依赖稳定 Go 库);
- Module 包目前完全没有签名机制;
- OCI 生态对签名方案无共识:Cosign 用特殊命名 tag,Notary Project 用 Referrers 机制建立签名与制品的双向关系。
因此首个版本不做签名验证(与 Provider Network Mirror Protocol 现状一致),理由是 mirror 由运营商显式配置、默认可信。但可通过subject属性发布组织内部的事后签名/审批制品(如“某团队已审阅此依赖”),由 OpenTofu 之外的独立工具扫描仓库检查未签名制品。未来实现验证时,合法签名的 OCI manifest 能提供与 Provider Registry Protocol 等价的信息(签名公钥 + 覆盖各平台 SHA256 校验和的签名),可无缝接入现有签名验证模型。
Provider:把 OCI 仓库配置为镜像源
4-providers.md 定义了首个版本的 Provider 方案:仅支持 “mirroring” 场景——OCI 仓库作为 origin registry 之外的可选下载源;也允许手工构造并发布内部 Provider 制品,但更完善的支持留待后续。
provider_installation中的oci_mirror块
OpenTofu 的 Provider 安装方式分两类:“direct” 与 “mirror”。oci_mirror属于 mirror 类:源地址中的 hostname 只作为标识,包实际从 OCI 仓库获取。在 CLI 配置文件(Provider Installation)中声明:
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/*/*"] } }repository_template必须为include中每个通配(*)的地址组件提供替换变量:${hostname}、${namespace}、${type}(分别对应example.net/foo/bar的 hostname、foo与bar;两段式地址如hashicorp/kubernetes的 hostname 默认为registry.opentofu.org);direct块可选:未匹配任何 mirror 的 Provider 仍从 origin registry 安装。
隔离网络示例
为支持无法直连registry.opentofu.org的隔离环境,可将所需 Provider 包复制到 OCI 仓库的固定命名模式中:
provider_installation { oci_mirror { repository_template = "example.com/opentofu-provider-mirror/${namespace}_${type}" include = ["registry.opentofu.org/*/*"] } }此后依赖hashicorp/kubernetes的模块初始化时,OpenTofu 会从example.com的opentofu-provider-mirror/hashicorp_kubernetes仓库安装,而registry.opentofu.org仅作为标识符。这与现有network_mirror安装方式对等(后者用的是 OpenTofu 专有协议)。
AWS 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/*/*"] } }这样无需改写任何模块源码即可从 ECR 安装hashicorp/kubernetes。
OCI 中的存储规范
借鉴 ORAS 但在 OpenTofu 内直接实现兼容的 index manifest 布局(撰写 RFC 时 ORAS 的多平台支持仍在进行中):
- 每个 OS/架构(如
linux_amd64)以.zip文件直接存为 OCI blob,不使用 tar; - 每个平台必须有 image manifest,含单个
mediaType为archive/zip的层,且该层必须是 Provider 开发者官方分发包的逐字节副本; - 制品主 manifest 必须是index manifest,为每个支持的平台提供条目;
artifactType必须为application/vnd.opentofu.provider。OCI 与 OpenTofu 恰好共享 Go 工具链的 OS/架构命名(linux_amd64↔"os":"linux","architecture":"amd64"); - 制品必须挂在与上游版本号同名的 tag 下;OpenTofu 会忽略无法识别为 semver 的 tag(包括
latest)。semver 的+构建元数据在 OCI tag 中不允许,需替换为_; - index manifest 的
manifests中所有条目都视为 Provider 包,不得混入其它 manifest;但单个 image manifest 可以包含额外mediaType的层(OpenTofu 会忽略),且必须恰好有一个archive/zip层。
可用subject属性发布引用 index manifest 或 image manifest 的附加制品(签名、SBOM 等),通过 Referrers API 发现;OpenTofu 初始版本不会消费它们。
警告:OCI 中的 Provider 制品必须使用多平台(index)manifest,OpenTofu 会拒绝下载非多平台制品;而 Module 制品禁止使用多平台 manifest。
镜像/发布 Provider 的工具现状
撰写 RFC 时没有第三方工具能直接推送所需格式。若 ORAS 的多平台 manifest 提案未能及时落地,OpenTofu 将发布手工编写 index manifest 并使用oras manifest低层命令推送的指南(效果等价于 ORAS 提案中的oras manifest index create)。RFC 也考虑过提供类似tofu providers mirror的内置镜像工具,但为了控制首版范围而推迟。
Module:把oci://作为新的模块来源
与 Provider 不同,Module 的源地址本就支持多种 scheme,因此 Module 可直接在配置中用oci://前缀引用 OCI 仓库(见 5-modules.md):
module "foo" { source = "oci://example.com/opentofu-vpc-module" }OpenTofu 会查找该仓库名为latest的 tag 并取回对应制品作为模块包。
显式 tag / digest 选择
沿用现有远程模块源地址的惯例,oci:scheme 支持两个互斥的查询参数:
tag=NAME:指定其它 tag,覆盖默认的latest;digest=DIGEST:直接指定 image manifest 的 digest,完全绕过 tag 命名空间。
module "foo" { source = "oci://example.com/opentofu-vpc-module?tag=1.0.0" }说明:使用查询字符串语法与 OCI 惯用的
:TAG/@DIGEST后缀不一致,原因在于 OpenTofu 的模块安装委托给第三方库 go-getter,查询字符串是其既有惯例;跟随 OCI 惯例反而会与 go-getter 的假设冲突,不利于用户在不同源地址类型间迁移知识。
基于版本约束的选择(当前不支持)
OpenTofu 目前只对模块注册表源地址保留“版本约束选择”能力(这类地址由 OpenTofu 直接处理而非 go-getter)。因此首版依赖直接选择 tag 或 digest(类似 Git 源用 ref 选择 commit)。未来可能为 OCI、Git、Mercurial 等统一引入版本约束(见 issue #2495),但这属于更广泛的架构变更,超出本 RFC 范围。
包内子目录
模块安装的最小单元是模块包(一个可能包含零个或多个模块目录的文件系统树,如一个 Git 仓库整体)。OCI 制品默认在根目录放置模块,也可用惯用的子目录语法:
module "foo" { source = "oci://example.com/opentofu-vpc-module//subnets" }与tag/digest组合时,查询字符串必须放在子目录之后:oci://example.com/opentofu-vpc-module//subnets?tag=1.0.0。
设计过程中也考虑过更 “OCI 原生” 的oci://example.com/opentofu-vpc-module:TAG-NAME或oci://...@DIGEST写法,但 go-getter 会把:TAG-NAME、@DIGEST当作路径一部分,导致子路径语法与其它源类型不一致,故最终选择与 OpenTofu 自身一致的查询字符串方案。
模块制品布局
模块包是平台无关的,因此采用直接的 ORAS 风格 image artifact,不使用多平台 index manifest:
- image manifest 的
artifactType必须为application/vnd.opentofu.modulepkg; - 必须恰好有一个
mediaType为archive/zip的层,指向包含模块包内容的.zipblob; - 可包含其它
mediaType的层(OpenTofu 会忽略,未来版本可能识别)。
用 ORAS CLI 发布模块包
oras push \ --artifact-type application/vnd.opentofu.modulepkg \ example.com/opentofu-vpc-module:latest \ opentofu-vpc-module.zip:archive/zip通过 OpenTofu Module Registry 间接暴露 OCI 地址
现有 Module Registry Protocol 是对任意远程源地址的门面(façade),因此支持 OCI 的版本也允许注册表在 “Download Source Code for a Specific Module Version” 中返回oci://前缀的 location:
{"location":"oci://example.com/repository?digest=sha256:1d57d25084effd3fdfd902eca00020b34b1fb020253b84d7dd471301606015ac"}这样可以隐藏 “包实际来自 OCI” 的实现细节,但代价是需要运行额外的 OpenTofu 专用注册服务。
警告:只有OpenTofu v1.10 及以上才支持
oci:scheme,旧版本会直接拒绝此类模块源地址。
认证配置:既有凭据复用与 OpenTofu 专属凭据
6-authentication.md 定义了 OCI 认证体系:默认对要求认证的仓库尝试匿名认证;附加凭据可来自 Docker/Podman 等工具的配置文件,或来自 OpenTofu CLI 配置的新块类型。
自动发现环境凭据(ambient credentials)
默认情况下,OpenTofu 按containers-auth.json的约定搜索凭据——该格式兼容docker login、podman login、oras login等写入的位置,从而复用既有凭据、避免凭据泛滥。
新块类型oci_default_credentials可定制自动发现行为:
oci_default_credentials { # 完全禁用自动凭据发现,强制只使用 CLI 配置中显式提供的凭据 discover_ambient_credentials = false }oci_default_credentials { # 覆盖 Docker 风格配置文件的默认搜索位置 #(设置后仅搜索列出的文件,默认搜索位置被禁用) docker_style_config_files = [ "/etc/awesome-oci-tool/auth.json", ] }还可指定默认的 Docker-style credential helper(用于没有更具体凭据配置的仓库):
oci_default_credentials { docker_credentials_helper = "osxkeychain" }未来的环境凭据发现方法都必须在oci_default_credentials中有独立开关;若想用新方法替代 Docker 风格文件,可将docker_style_config_files设为空列表再配置新方法。
OpenTofu 专属显式配置:oci_credentials
对于只在 OpenTofu 中使用 OCI 仓库的用户,新增oci_credentials块,按仓库前缀匹配:
oci_credentials "example.com/foo/bar" { # 适用于该仓库下所有以 "foo/bar" 开头的仓库路径 username = "foobar" password = "example" }每个oci_credentials块必须且只能设置以下互斥组之一:
username+password:基础认证风格;access_token+refresh_token:OAuth 风格;- 单独
docker_credentials_helper:间接提供用户名/密码。
每个仓库一个独立顶层块,沿用了 OpenTofu 自有服务认证的既有惯例(如tofu login会把生成的凭据写入独立的 CLI 配置文件)。首版不提供tofu login风格的 OCI 凭据获取命令,后续版本再考虑。
凭据选择优先级
沿袭 Docker CLI 配置格式的匹配规则,OpenTofu 在所有 ambient 与显式凭据源中寻找对目标仓库最具体的匹配:
example.com/foo/bar比example.com/foo更具体;example.com/foo比example.com更具体;- 任何匹配域名的条目都比全局设置更具体(只有默认的 Docker 凭据 helper 属于 “全局”)。
当同一仓库地址同时存在显式与 ambient 配置时,显式oci_credentials优先。CLI 配置解析器会拒绝同一仓库前缀的多个oci_credentials块;ambient 凭据无此限制,按发现顺序(或docker_style_config_files中的顺序)靠前者优先。
实现细节:凭据策略层、Provider 源与 go-getter Getter
RFC 的后三个附录(8-auth-implementation-details.md、9-provider-implementation-details.md、10-module-implementation-details.md)记录了面向核心团队的实现蓝图。需要说明的是,这些代码签名是“示意性的”,以package main→package cliconfig的依赖倒置为主线。
认证是横切关注点
认证逻辑应被 Provider 与 Module 安装器共享,并集中管理凭据设置。CLI 配置由internal/command/cliconfig解码校验oci_default_credentials与oci_credentials块(对应实现见 oci_credentials.go),隐式配置(Docker 等配置文件)也被映射到同一内部数据类型。
凭据选择策略封装在独立的package ociauthconfig(位于internal/command/cliconfig/ociauthconfig)。核心抽象包括:
CredentialsConfig接口:CredentialsSourcesForRepository(ctx, registryDomain, repositoryPath)返回匹配该仓库的凭据源序列;CredentialsSource接口:CredentialsSpecificity()+Credentials(ctx)——先用特异性选源,再取具体凭据,避免过早执行凭据 helper;Credentials:封装具体凭据,初始实现仅提供ForORAS()方法转为 ORAS 库的orasauth.Credential;CredentialsSpecificity:分层级(NoCredentialsSpecificity<GlobalCredentialsSpecificity<DomainCredentialsSpecificity<RepositoryCredentialsSpecificity(pathSegments)),内部实现为整数但对外不承诺;- 入口
CredentialsConfigs.CredentialsSourceForRepository:遍历所有配置源,选出特异性最高者;同特异性时取“最早声明”的源。package cliconfig将显式块排在 ambient 配置之前,实现显式优先。
选用自研实现而非第三方库的理由:候选库难以扩展接入 OpenTofu CLI 配置的显式方式,且常伴随大量非必要间接依赖,可能触发仅按 Go 模块粒度扫描的安全工具误报。
Provider 安装源:OCIMirrorSource
provider_installation中每种安装方法对应一个getproviders.Source实现:
direct→getproviders.RegistrySource;filesystem_mirror→getproviders.FilesystemMirrorSource;network_mirror→getproviders.HTTPMirrorSource;- 新增
oci_mirror→getproviders.OCIMirrorSource(实际实现见 oci_registry_mirror_source.go)。
示意构造签名:
func NewOCIMirrorSource( getRepositoryAddress(addrs.Provider) (registryDomainName, repositoryPath string, err error), getRegistryClient func(ctx context.Context, registryDomainName, repositoryPath string) (*orasregistry.Registry, error), ) *OCIMirrorSource其中getRepositoryAddress委托package cliconfig求值repository_template(HCL 字符串模板);getRegistryClient先用ociauthconfig.CredentialsConfigs选择凭据,再实例化 ORAS 库的 registry client。ORAS 的 repository 客户端提供实现所需能力:枚举 tag(Tags)、拉取 manifest(ManifestStore)、拉取 blob(BlobStore)。整体实现预计与HTTPMirrorSource高度相似,只是改用 OCI Distribution 协议。
校验和与签名细节
- 依赖锁文件记录已装 Provider 的版本与可接受校验和集合;
- 从 origin registry 获取 GPG 签名后,OpenTofu 验证包匹配签名覆盖的校验和之一,并将签名覆盖的全部平台校验和记入锁文件;
- Provider 包始终是
.zip,签名覆盖含全部平台 SHA256 校验和的文档;对应zh:校验和,另生成本地内容校验和h1:; - 由于 OCI 层就是官方
.zip的逐字节副本,每个层的sha256:digest 可直接转成zh:校验和,无需下载后重算; - 首版不做签名:锁文件只记录下载制品本身的
h1:与zh:校验和;未来支持签名后,可用签名证明全部平台的zh:校验和,并在tofu init输出中公布签名 key ID(由运营商自行核对); tofu providers lock默认忽略已配置安装方法、直连 origin registry,以支持“origin 取官方校验和 + mirror 校验镜像包”的隔离工作流;未来应增加-oci-mirror选项(首版不做,另有 issue 跟踪)。
模块安装:go-getter 的新Getter
OpenTofu 的远程模块包抓取大多委托给 go-getter。go-getter 两个关键概念:
Detector:预处理原始地址(如把github.com/example/example重写为git::https://github.com/example/example.git),Detector 可“堆叠”,但最后一个必须产出 URL-like 语法;Getter:真正抓取并解包。地址可用git::双冒号前缀显式选择 Getter,否则 URL scheme 即 Getter 名(如https://...属于 “https” getter)。
oci:scheme 无显式前缀,因此需要向 OpenTofu 的 getters 表添加新条目"oci"(无需新增 Detector,地址本身符合 URL 语法)。上游 go-getter 不接受新Getter实现(HashiCorp 代表曾表示 “It can be hard to get changes through in go-getter”),故在package getmodules内实现 OpenTofu 专属 Getter(实际实现见 oci_getter.go)。
示意构造签名:
func NewOCIGetter( getRegistryClient func(ctx context.Context, registryDomainName, repositoryPath string) (*orasregistry.Registry, error), ) getter.Getter该 Getter 用 ORAS 库完成:tag 解析为 manifest digest → 取 manifest 得知.zipblob digest → 取 blob 并解包到目标目录。解包直接复用 go-getter 的getter.ZipDecompressor,确保与 HTTP 等来源解包行为完全一致(含防路径穿越等安全检查)。go-getter 的通用Decompressor机制按文件后缀(.zip)而非 media type 匹配,不适合本场景,故不采用。
模块校验和现状
OpenTofu 目前对模块包没有通用校验和/签名/锁文件机制(属于跨源类型的更大议题)。现有常见折中是 Git getter +ref指向具体 commit(提供 SHA-1 校验)。OCI 的digest参数提供类似机制:它校验 manifest,manifest 内又含.zipblob 的校验和,因此使用digest而非tag的地址可保证 “匹配到的 manifest 必须与该 digest 一致”。未来或可用 Referrers API 自动发现 manifest 的签名,但不在本项目范围内。
开放问题与未来规划
7-open-questions.md 与主文档记录了尚待解答的问题与后续方向。
与 Terragrunt 的兼容性
Terragrunt 的 Provider Cache Server 通过未文档化的host块重写 remote service discovery,强制注册表请求走向缓存服务器——这只对使用 Provider Registry Protocol 的direct方式有效。因此首版中oci_mirror与 Terragrunt 缓存方案互斥:改用oci_mirror会绕过 Terragrunt 依赖的拦截机制。不过首版不改direct行为,未使用oci_mirror的既有部署不会受影响。
多认证域配置
同一 host 在不同代码库中可能需不同凭据。目前只能间接支持:用TF_CLI_CONFIG_FILE环境变量为特定代码库指定专属 CLI 配置文件。若后续发现这是普遍需求,再考虑引入认证上下文切换(应同时覆盖 OpenTofu 原生与 OCI 凭据)。
非 ASCII 字符的 Provider 地址
OCI 仓库名只允许 ASCII 字母数字与少量标点,而 OpenTofu 的 Provider 源地址(按 RFC 3491 Nameprep 规则)允许宽泛的 Unicode 字符。因此某些合法 Provider 地址无法机械地代入模板。初始原型直接报错:
requested provider address example.com/foo/ほげ contains characters that are not valid in an OCI distribution repository name, so this provider cannot be installed from an OCI repository as ghcr.io/examplecom-otf-providers/foo-ほげ若未来确需支持,可考虑 Punycode 类编码变换,但可读性差且通用 OCI 仓库 UI 大概率不会自动转码。
未来计划
- Provider 方向:当前 OCI 仅作为运营商配置的镜像;待制品签名方案成熟后,有望把 OCI Distribution 变为一种无需 CLI 配置的新 “origin registry” 类型;
- 搜索与文档:OCI 实现尚无对应 OpenTofu Registry Search 的能力,未来可能仿照 Linux 发行版发布附带
doc包的做法,需要 OpenTofu 内置文档渲染工具(可复用 registry-ui 源码,但需适配改造); - 替代方案对比:改善现有注册协议工具链(如基于静态文件维护私有注册表、复用 OpenTofu Registry 数据)与 OCI 代理(用 Provider Network Mirror 协议做翻译)虽可行,但均不如 OCI 基础设施“普遍且廉价”,且模块没有 mirror 协议对应物,代理方案无法覆盖模块场景。
仓库中的落地证据
RFC 描述的设计已在当前仓库中落地为实际代码,可作为深入阅读的入口:
- CLI 配置解析:oci_credentials.go(
oci_credentials/oci_default_credentials块的解码与校验)、provider_installation.go(oci_mirror安装方法); - 凭据策略层:ociauthconfig/(
docker_cli_credentials_config.go、repository_addr.go等); - Provider 镜像源实现:oci_registry_mirror_source.go(对应
getproviders.OCIMirrorSource); - 模块 OCI Getter:oci_getter.go 及配套测试 oci_getter_test.go。
如需验证本文涉及的示例命令(token 换取、manifest 拉取、blob 重定向、ORAS 推送等),可对照 1-oci-primer.md 中的 “Try it yourself” 片段逐一复现。
【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考