
OpenTofu 与 OCI 注册表从 OCI Distribution 协议入门到镜像化 Provider/Module 分发【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu导读OCIOpen Container Initiative注册表不仅仅是容器镜像的分发枢纽它通过一套通用、基于 HTTP 的协议同时管理元数据manifest与内容blob因而也适合承载任意类型的软件制品。本篇文章以 OpenTofu 仓库中《A primer on the OCI protocol》文档rfc/20241206-oci-registries/1-oci-primer.md为骨架系统讲解 OCI Distribution 协议的核心概念、认证流程、Pull/Push/内容发现三类 API 以及 ORAS 制品布局并结合 OpenTofu 实际源码说明它是如何将这套协议用于分发 Provider 与 Module 的。读完本文你将掌握 OCI 注册表的地址命名规则、Bearer Token 认证、manifest 解析、blob 拉取与上传流程并能理解 OpenTofu 中oci_mirror与oci://模块源的底层工作原理。一、协议总览Manifest 与 Blob 两大核心对象OCI 注册表对外暴露的是一套 HTTP 接口用于访问两类对象Manifest清单描述注册表中内容的元数据是 JSON 文档带有特定的媒体类型media type。Blob数据块真正的二进制数据按内容寻址即通过其摘要digest校验和来引用。这套交互协议遵循 OCI Distribution Specification分发规范而注册表中存储的内容格式则必须遵循 OCI Image Format Specification镜像格式规范。需要特别指出的是目前许多注册表实现并未完全遵循 OCI 镜像格式而是返回 Docker Image Manifest 文档。所幸两者差异非常微小primer 文档以 OCI 规范为准展开讲解同时提醒在落地实现时必须处理这些差异。[!TIP] 由于数据存储方式存在灵活性社区还衍生出了ORASOCI Registry as Storage——把 OCI 注册表当作通用对象存储来使用。详见本文ORAS章节。[!WARNING] 原文档中的示例仅用于演示纯粹的 OCI Distribution 协议并不代表 OpenTofu 最终捕获 Provider/Module 制品的具体提案格式。OpenTofu 自身的制品布局规范见 rfc/20241206-oci-registries/4-providers.md 与 rfc/20241206-oci-registries/5-modules.md。协议设计动机从 OpenTofu 的视角看为什么要在既有 Provider/Module Registry 协议之外拥抱 OCI主 RFCrfc/20241206-oci-registries.md给出了背景OCI 注册表历史上也称 Docker 注册表是容器生态的骨干其分层差分设计让镜像更新非常高效同时得益于其通用架构已经出现多种把任意数据放进 OCI 注册表的实现。对于大型组织尤其是隔离网络/内网环境OCI 注册表往往已随 Kubernetes 等基础设施广泛部署无需额外合规负担而 OpenTofu/Terraform 专用注册表则需要额外部署一套软件。这正是 OpenTofu 支持 OCI 注册表分发 Provider 与 Module 的根本原因。二、认证流程WWW-Authenticate与 Bearer Token注册表并非必须要求认证但许多公共注册表包括ghcr.io和 Docker Hub即使访问公共镜像也要求先获取一个匿名token。当访问某个端点时注册表服务器可能返回WWW-Authenticate响应头提示需要进行认证。例如访问https://ghcr.io/v2/opentofu/opentofu/tags/list会返回www-authenticate: Bearer realmhttps://ghcr.io/token,serviceghcr.io,scoperepository:opentofu/opentofu:pull关键要点不要用/v2/基础端点做认证探测在ghcr.io上访问/v2/不会得到有效的 scope不应将其用于认证。OpenTofu 源码也印证了这一点——在 internal/oci/oci_distribution.go 中有一段明确注释构造 OCI 客户端时故意不调用reg.Ping(ctx)因为Ping方法会请求GET /v2/端点某些注册表即便是匿名拉取配置的仓库也会对其返回 401 质询从而被误判为错误ORAS 会在第一次真实 API 调用时透明地完成认证。realm字段指示实际执行认证的端点。认证方式是向该端点发起GET请求用可选的凭据换取临时 Bearer Tokencurl -u user:password https://ghcr.io/token?serviceghcr.ioscoperepository:opentofu/opentofu:pull请求成功后注册表服务器返回一个临时 Bearer Token用于后续所有请求{token:djE6b3Blb...}[!TIP] 动手试一试匿名请求即可curl -v https://ghcr.io/token?serviceghcr.ioscoperepository:opentofu/opentofu:pull在 OpenTofu 的实现中凭据的解析被抽象为OCICredsPolicyBuilder回调函数见 internal/oci/oci_distribution.go这样可以在大多数不需要访问 OCI 注册表的场景下跳过凭据构建工作。GetOCIRepositoryStore会为同一(registryDomain, repositoryName)组合缓存已实例化的存储对象以便复用已签发的临时 token省去重复的会话建立往返。凭据来源支持多种形式CLI 配置、环境变量、Docker 风格凭据助手等调试日志会列出所有被考虑的凭据位置便于排查OpenTofu 选用了与操作者预期不同的凭据这类问题。三、两类 ManifestIndex Manifest 与 Image ManifestManifest 分为两种类型理解它们的区别是解析 OCI 制品的第一步。3.1 Index Manifest索引清单多平台分发媒体类型为application/vnd.oci.image.index.v1json或application/vnd.docker.distribution.manifest.list.v2json。它包含一组 image manifest 的列表以及每个清单对应的平台选择信息platform对象。当需要为每个目标平台分别分发制品时例如 OpenTofu 的 Provider 需要区分linux/amd64、linux/arm64等就必须使用索引清单。[!TIP] 动手试一试先按上文认证拿到 tokencurl -H Authorization: Bearer djE6b3Blb... https://ghcr.io/v2/opentofu/opentofu/manifests/1.8.0返回结果节选{ schemaVersion: 2, mediaType: application/vnd.docker.distribution.manifest.list.v2json, manifests: [ { mediaType: application/vnd.docker.distribution.manifest.v2json, size: 951, digest: sha256:105eb6b43b0704093cd48644437934d3eb9200c297756fe4e4d5ed2fccada56c, platform: { architecture: 386, os: linux } }, { mediaType: application/vnd.docker.distribution.manifest.v2json, size: 951, digest: sha256:26b1e5ed87f80d3b2bb36769d90a448246bd1aa786f57bdc0b8907dc3d2b327f, platform: { architecture: amd64, os: linux } }, { mediaType: application/vnd.docker.distribution.manifest.v2json, size: 951, digest: sha256:4ac194402663bf948d022a9b3f79641565fc90cdae476829638f2dfcd4583c77, platform: { architecture: arm64, os: linux } }, { mediaType: application/vnd.docker.distribution.manifest.v2json, size: 951, digest: sha256:4ac9d6209f34675c869a63e41110b596e20a679a3c8451c9bd059bbb5a1ba564, platform: { architecture: arm, os: linux, variant: v7 } } ] }3.2 Image Manifest镜像清单单平台内容媒体类型为application/vnd.oci.image.manifest.v1json或application/vnd.docker.distribution.manifest.v2json。它包含一组layer层列表每个 layer 引用一个 blob。在传统容器镜像中layer 是代表根文件系统内容的.tar.gz归档额外的元数据则通过 manifestconfig字段指向的独立 blob 提供。[!TIP] 动手试一试curl -H Authorization: Bearer djE6b3Blb... https://ghcr.io/v2/opentofu/opentofu/manifests/1.8.0-amd64返回结果节选{ schemaVersion: 2, mediaType: application/vnd.docker.distribution.manifest.v2json, config: { mediaType: application/vnd.docker.container.image.v1json, size: 2061, digest: sha256:f160637911afad6485d75b398c7c62b032f5040e641aff097e3035bcacf697de }, layers: [ { mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip, size: 3415640, digest: sha256:930bdd4d222e2e63c22bd9e88d29b3c5ddd3d8a9d8fb93cf8324f4e7b9577cfb }, { mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip, size: 8158438, digest: sha256:27a55bad853afb2cf5f203297db2e5f132a1f9afffce02a20e59284fed62ab4a }, { mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip, size: 25145807, digest: sha256:22b79cf4f0efedf6423a10f5200cde934aaa8d6c7ece5a45939e87bb5af12e22 } ] }每个 layer 描述符descriptor都带有mediaType、size与digest客户端可据此校验内容完整性。这种manifest 指向 blob、blob 按摘要寻址的层级结构是理解后续所有 API 的基础。四、Pull 类别 API拉取 Manifest 与 BlobOCI Distribution 规范要求注册表必须实现 Pull 类别中的所有端点。该类别包含两个端点端点路径模板说明Manifest 端点/v2/name/manifests/reference返回 manifest 文档特定内容类型的 JSON。注册表允许根据客户端发送的Accept头进行内容协商Blob 端点/v2/name/blobs/digest返回基于摘要校验和寻址的二进制对象。注意该端点可能返回 HTTP 重定向4.1 地址组成与命名规则name仓库名可以包含额外的/字符必须匹配正则[a-z0-9]((\.|_|__|-)[a-z0-9])*(\/[a-z0-9]((\.|_|__|-)[a-z0-9])*)*。也就是说仓库名可以由任意数量的路径段组成主机名 仓库名的总长度上限为255 个字符。值得注意许多注册表实现会对仓库名施加额外限制例如必须包含项目 ID、分组、命名空间等且可能禁止额外的路径段。reference引用可以是摘要digest或标签tag名。标签名必须匹配正则[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}。digest摘要形式为scheme:value理论上可使用任意校验算法但规范规定sha256与sha512是标准化算法合规注册表应支持sha256。[!NOTE] reference 一词在 OCI 生态中被重载为两种含义有时指某个注册表中某个仓库内标签/摘要的完整地址。例如latest是只指定标签名、隐含仓库地址的局部引用而example.com/foo/bar/baz:latest则是完全限定引用指example.com注册表中foo/bar/baz仓库的latest标签。本 primer 中使用的是局部引用含义但生态中其他软件的文档有时使用另一种含义阅读时需注意区分。4.2 Blob 拉取与重定向[!TIP] 动手试一试认证后从前面示例的 manifest 中拉取一个 blob观察响应头中的location字段它指向 blob 的实际下载地址curl -v -H Authorization: Bearer djE6b3Blb... https://ghcr.io/v2/opentofu/opentofu/blobs/sha256:22b79cf4f0efedf6423a10f5200cde934aaa8d6c7ece5a45939e87bb5af12e22返回结果节选省略了 TLS 输出 GET /v2/opentofu/opentofu/blobs/sha256:22b79cf4f0efedf6423a10f5200cde934aaa8d6c7ece5a45939e87bb5af12e22 HTTP/2 Host: ghcr.io User-Agent: curl/8.5.0 Accept: */* Authorization: Bearer djE... HTTP/2 307 content-length: 0 content-type: application/octet-stream docker-distribution-api-version: registry/2.0 location: https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:22b79cf4f0efedf6423a10f5200cde934aaa8d6c7ece5a45939e87bb5af12e22?se2025-02-04T09%3A50%3A00ZsigFGZhe9e3oOMEbhebzW49Stfj9J1Cuy77Go77Ob7w8ro%3Dsprsprhttpssrbsv2019-12-12 date: Tue, 04 Feb 2025 09:40:56 GMT x-github-request-id: D334:1069C0:704BB:71F3D:67A1E0A8该示例演示了 blob 端点返回307 重定向location头指向带签名参数的 CDN 下载地址客户端必须跟随该重定向才能拿到实际内容。在 OpenTofu 的模块安装器中internal/getmodules/oci_getter.go对 blob 的拉取还会额外做两件事一是通过orasContent.NewVerifyReader在流式拷贝的同时校验内容是否与描述符中的摘要和大小一致无需把整个 blob 读入内存二是把 manifest 内容限制在 4 MiB 以内防止恶意的远端注册表占用无界内存该值与 OCI Distribution v1.1 规范建议的 push 端 manifest 大小上限一致。五、Push 类别 API上传 Blob 与 ManifestPush 类别与 Pull 类似但目的是发布manifest 与 blob。上传一个新制品有两种方式两步上传先POST到/v2/name/blobs/uploads端点然后把 blob 内容PUT到第一个响应Location头所指示的 URL。一步上传直接POSTblob 内容到/v2/name/blobs/uploads/?digestdigest同时指明预计算的摘要。[!TIP] 第一种方式的优势在于支持用PATCH请求分块上传 blob详见 OCI Distribution 规范中 Pushing a Blob in Chunks 一节适合大文件场景。Blob 上传完成后即可推送引用这些 blob 的 manifest向/v2/name/manifests/reference发送PUT请求其中 reference 应为该 manifest 希望出现的标签名。六、Content discovery 类别 API标签列表与 Referrer 列表除了必须实现的 Pull 类别外注册表可选实现Content discovery内容发现类别包含两个额外端点6.1 标签列表端点/v2/name/tags/list该端点列出所选仓库中所有拥有关联 manifest 的标签支持额外的过滤与分页参数。它特别适合回答这个制品有哪些可用版本这类问题从而实现基于语义化版本semver的匹配或其他非精确选择技术。[!WARNING] 标签列表端点通常是分页的。客户端实现必须跟随响应中的Link头才能拿到完整的标签列表。[!TIP] 动手试一试curl -v -H Authorization: Bearer djE6b3Blb... https://ghcr.io/v2/opentofu/opentofu/tags/list返回结果节选省略了 TLS 输出 GET /v2/opentofu/opentofu/tags/list HTTP/2 Host: ghcr.io User-Agent: curl/8.5.0 Accept: */* Authorization: Bearer djE6b3B... HTTP/2 200 content-type: application/json docker-distribution-api-version: registry/2.0 link: /v2/opentofu/opentofu/tags/list?last1.6.0-beta5-386n0; relnext date: Tue, 04 Feb 2025 09:47:19 GMT x-github-request-id: D388:2C9949:B77F2:B977F:67A1E226{ name: opentofu/opentofu, tags: [ 1.6.0-alpha1-arm64, 1.6.0-alpha1-amd64, 1.6.0-alpha1-arm, 1.6.0-alpha1-386, 1.6.0-alpha1, 1.6, 1, latest, sha256-722da07b0cdf5b6bdf12aff9339f7c274f70552144a0a28c0d2b970c083ffa5c.sig, ... ] }注意响应中link: ...; relnext头的存在——示例中该响应只返回了第一页。OpenTofu 的 Provider 镜像源internal/getproviders/oci_registry_mirror_source.go正是利用这个端点发现 Provider 可用版本的它通过store.Tags(ctx, , fn)逐页遍历标签把每个标签当作候选版本号解析OCI 标签名不允许字符因此 semver 构建元数据中的在标签里以_占位解析时再替换回来无法解析为 semver 的标签如latest会被直接忽略。6.2 Referrer 列表端点/v2/name/referrers/digest该端点返回引用了某个特定 blob 的 manifest 列表。其机制是manifest 可选地包含一个subject属性指向另一个 manifest从而形成一棵制品树——subject表示当前 manifest 的父节点referrer 列表端点则描述相反的关系返回所有引用给定父 manifest 的子 manifest。截至写作时该能力的生态用例仍在涌现典型场景是作为事后追加的证明attestation或其他元数据例如为父 manifest 附加签名签名者可能并非制品作者本人。但要注意包括 Cosign 在内的某些签名机制期望签名以特殊命名的标签呈现而不是使用该 API。七、_catalog扩展端点/v2/_catalog端点并未在 Distribution 规范中标准化但它被传统地用于 Docker Registry 中列出注册表内的所有镜像。在实际公共注册表中该端点通常被禁用或仅在认证后可用。因此它不适合作为依赖的基础能力。八、ORAS以非标准布局在 OCI 注册表中存储任意制品以上内容都围绕标准的容器镜像布局展开。而ORASOCI Registry as Storage描述了如何在 OCI 注册表中以非标准布局存储制品目前 ORAS 已获得广泛支持。ORAS 的核心思想是利用 layer 描述符的mediaType属性来区分不同类型的 layer而不必拘泥于容器镜像所期望的差分 tar格式。例如可以把 layer 声明为archive/zip而非application/vnd.docker.image.rootfs.diff.tar.gzip{ schemaVersion: 2, mediaType: application/vnd.oci.image.manifest.v1json, artifactType: application/vnd.opentofu.provider, config: { mediaType: application/vnd.oci.empty.v1json, 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 } }[!TIP] 动手试一试安装 ORAS 客户端和一个本地注册表下载一个 Provider 压缩包然后执行oras push \ --artifact-type application/vnd.opentofu.provider \ localhost:5000/oras:latest \ terraform-provider-aws_5.84.0_linux_amd64.zip:archive/zip随后即可列出该 manifestoras manifest fetch localhost:5000/oras:latest --pretty8.1 多文件推送每个文件一个 layer使用 ORAS 向同一个标签推送多个文件时每个文件都会表示为独立的 layer{ schemaVersion: 2, mediaType: application/vnd.oci.image.manifest.v1json, artifactType: application/vnd.opentofu.provider, config: { mediaType: application/vnd.oci.empty.v1json, 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 } }, { mediaType: archive/zip, digest: sha256:aa50fb3769355eeddfec7614bae674d0841c3b0b771e5183ac2db4dfc04b9423, size: 132401007, annotations: { org.opencontainers.image.title: terraform-provider-aws_5.84.0_linux_arm64.zip } } ], annotations: { org.opencontainers.image.created: 2025-02-03T12:00:46Z } }8.2 自定义 config manifestORAS 还可以通过--config选项定制 config manifest产生如下结构config 指向一个真实的校验和文本layers 为空{ schemaVersion: 2, mediaType: application/vnd.oci.image.manifest.v1json, artifactType: application/vnd.opentofu.provider, config: { mediaType: text/plainSHA256SUMS, digest: sha256:2bc757edf7a4532ebe70d994963dd51532fd9907e27a95ffd57763b5795170e0, size: 1122 }, layers: [] }8.3 空 config blob 约定与传统容器引擎的冲突值得注意的是ORAS 默认使用一个固定的 config blob它表示空 JSON 对象{}媒体类型为application/vnd.oci.empty.v1json其摘要恒为sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a。ORAS 感知的软件理解这一约定会跳过拉取这个已知为空的配置 blob但 Docker 等传统容器引擎并未为这些不同的 layer/config 格式而设计当有人用它们处理这类非容器镜像制品时会返回诸如 invalid rootfs in image configuration 之类的有趣错误。[!NOTE] 截至 primer 写作时ORAS 尚不支持多平台镜像。不过可以直接使用oras manifest子命令推送外部生成的 manifest例如手写的 index manifest从而绕过这一限制——这正是 OpenTofu Provider 多平台发布方案所依赖的路径。九、从 Primer 到 OpenTofu 实现协议知识如何落地Primer 明确说明其示例仅演示纯 OCI Distribution 协议OpenTofu 对 Provider/Module 制品有自己的布局提案。结合仓库源码可以看到 primer 中的每个概念几乎都能在实现中找到对应9.1 Provider 镜像oci_mirror与三层 manifest 结构按照 rfc/20241206-oci-registries/4-providers.md 的约定OpenTofu Provider 在 OCI 中的布局是每个 OS/架构如linux_amd64以.zip文件直接存为 OCI blob不使用容器镜像惯用的 tar 格式每个平台都有一个 image manifest其唯一 layer 的mediaType为archive/zip顶层必须是 index manifestartifactType为application/vnd.opentofu.provider其中每个条目对应一个平台版本号直接作为标签名semver 中的替换为_index manifest 中所有条目都被视为 Provider 包image manifest 可以附带其他 mediaType 的 layerOpenTofu 会忽略。这些约束在源码中有硬性校验。例如 internal/getproviders/oci_registry_mirror_source.go 中定义了ociIndexManifestArtifactType application/vnd.opentofu.provider与ociPackageManifestArtifactType application/vnd.opentofu.provider-targetselectOCIImageManifest会依据描述符中的platform对象os/architecture与 OpenTofu 一样继承自 Go 工具链的命名体系选择匹配当前目标平台的 image manifest找不到时返回ErrPlatformNotSupported而 internal/getproviders/package_location_oci_blob_archive.go 则要求最终 blob 的mediaType必须是archive/zip并把其 sha256 摘要直接映射为 OpenTofu 的ziphash校验方案从而与依赖锁文件dependency lock file中记录的校验和交叉验证。9.2 模块安装oci://源与tag/digest参数在 internal/getmodules/oci_getter.go 中模块包的期望artifactType为application/vnd.opentofu.modulepkg支持的 blob 媒体类型偏好为archive/zip通过 go-getter 的解压器解压到目标目录。模块源 URL 还支持查询参数?tagname指定标签名解析 manifest复用 ORAS-Go 的标签校验?digestdigest按摘要直接解析两者不可同时指定默认标签为latest。这正好体现了 primer 中reference 可以是 digest 或 tag的协议知识在具体产品中的运用。9.3 客户端基础设施GetOCIRepositoryStore与缓存复用internal/oci/oci_distribution.go 是 Provider 与 Module 两个安装器共享的 OCI 基础设施它基于 ORAS-Go 库构建客户端按(registryDomain, repositoryName)缓存存储对象以复用临时 token通过OCICredsPolicyBuilder回调延迟加载凭据并特意跳过GET /v2/Ping 探测——这些细节与 primer 中/v2/端点无法用于认证探测的提醒一脉相承。9.4 操作示例把 Provider 镜像配置到 OCI 注册表理解了协议基础后实际配置 OpenTofu 使用 OCI 镜像源只需在 CLI 配置中编写provider_installation块完整语法见 rfc/20241206-oci-registries/4-providers.mdprovider_installation { oci_mirror { repository_template example.com/opentofu-provider-mirror/${namespace}_${type} include [registry.opentofu.org/*/*] } }repository_template必须为include中出现的每个通配符组件提供替换${hostname}、${namespace}、${type}。上述配置意味着依赖hashicorp/kubernetesProvider 的模块将从example.com注册表的opentofu-provider-mirror/hashicorp_kubernetes仓库安装registry.opentofu.org仅作为 Provider 唯一标识的一部分不再作为实际网络位置——这正是为内网/隔离环境准备的 OCI 版network_mirror。结语OCI Distribution 协议的核心其实非常简洁一条路径规则/v2/name/...、两类对象manifest 与 blob、三类 APIPull、Push、Content discovery加上一套基于WWW-Authenticate/Bearer Token 的认证机制。而 ORAS 进一步揭示了该协议的通用性只要通过mediaType与artifactType重新定义 layer 与 manifest 的语义就能把注册表变成任意制品的分发通道。OpenTofu 正是沿着这条路径以 index manifest 承载多平台 Provider 包、以单层 image manifest 承载模块包并为内网场景提供了oci_mirror这一零额外服务端软件的分发方案。若希望继续深入建议按顺序阅读同一 RFC 系列中的 2-survey-results.md生态调研、3-design-considerations.md设计取舍、4-providers.md 与 5-modules.mdProvider/Module 布局以及 6-authentication.md认证方案再对照本文引用的源码路径阅读具体实现。【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考