- 云原生
- 微服务
【免费下载链接】spin
Spin is the open source developer tool for building and running serverless applications powered by WebAssembly.
导读
本文围绕 Spin 项目的改进提案 SIP 008 - Distributing Spin applications using OCI registries,完整梳理 Spin 应用从传统 Bindle 分发机制迁移到 OCI Registry 的动机、用户交互设计、OCI 镜像结构设计、认证方案与源码级实现细节。读完本文,你将掌握spin oci push、spin oci pull、spin oci login与spin up --oci背后的完整链路:如何把一个包含元数据、Wasm 模块和静态资源的 Spin 应用打包为多层 OCI artifact、如何复用 Docker 凭据、以及 Spin 如何用 locked application 作为配置对象从本地缓存中重建并运行应用。
背景:为什么 Spin 要引入 OCI Registry
自 Spin 首次发布以来,应用分发一直依赖 Bindle——一个最初为分发 WebAssembly 应用及配套文件而设计的实验性聚合对象存储项目。然而 Bindle 作为相对早期的项目,暴露了两个突出问题:
- 没有托管版 Bindle 服务,用户分发 Spin 应用必须自行搭建并维护基础设施,这对最简单的应用来说也显著增加了复杂度;
- 基础设施的横向扩容在 Bindle 项目中是尚未解决的难题。
与此同时,OCI(Open Container Initiative) 已成为打包与分发容器镜像的事实标准,且随着 OCI Artifacts 项目的推进,容器 Registry 的使用范围正扩展到越来越多的 artifact 类型。所有主流云厂商均提供托管 Registry 服务,其中不少已支持分发其他类型的 artifact。因此 SIP 008 提议:让 Spin 直接支持通过 OCI Registry 分发应用,一举解决上述两个问题:
- 借助现成的托管服务(如 GitHub Container Registry、Docker Hub、AWS Elastic Container Registry、Azure Container Registry、Google Artifact Registry 等),用户无需自建分发基础设施;
- 扩容问题交给托管服务解决;即便自建,容器 Registry 的横向扩容也有远比 Bindle 丰富的资源与方案。
提案核心:基于 OCI artifact 的 Spin 应用分发
SIP 008 的核心观点是:一个 Spin 应用由元数据 + 组件信息以及组成这些组件的Wasm 模块和静态资源构成。因此,一个 Spin 应用在概念上并不是“单一 artifact”,而是多个不同对象的集合。
基于此,提案设计:Spin 应用发布到 OCI Registry 后,将成为一个新的 OCIartifact,拥有多个层(layers)(注意 OCI 中image与artifact是两种不同实体)。具体约定如下:
| 实体 | 说明 |
|---|---|
| 配置对象 media type | application/vnd.fermyon.spin.application.v1+config,承载 Spin 应用定义 |
| Wasm 模块层 | 每个组件的 Wasm 模块成为一个独立 layer(application/vnd.wasm.content.layer.v1+wasm) |
| 静态资源层 | 组件引用的每个静态资源也成为独立 layer(application/vnd.wasm.content.layer.v1+data) |
正因为每个文件与每个 Wasm 模块都成为独立的 layer,Registry 可以高效地对应用进行去重(de-duplicate)与分发:多个应用共享同一 Wasm 模块或静态文件时,Registry 只需存储一份 blob。
应用定义用什么表示:locked application
剩下的关键问题是如何表示 Spin 应用定义。Spin 引入了locked application(锁定应用)这一内部表示——它是 Spin 应用的一种中间表示,能够以内容寻址(content-address)的方式引用 Wasm 模块与静态资源。SIP 008 的实现正是将 locked application 作为 OCI 配置对象(即 OCI manifest 中的config项)使用。
用户体验设计:push / pull / run 三命令
提案给出的目标用户体验非常直接:把应用推送到兼容 Registry、拉取到本地、然后运行:
$ spin oci push ghcr.io/<username>/my-spin-application:v1 INFO spin_publish::oci::client: Pushed "https://ghcr.io/v2/<username>/my-spin-application/manifests/sha256:9f4e7eebb27c0174fe6654ef5e0f908f1edc8f625324a9f49967ccde44a6516b" $ spin oci pull ghcr.io/<username>/my-spin-application:v1 INFO spin_publish::oci::client: Pulled ghcr.io/<username>/my-spin-application:v1@sha256:9f4e7eebb27c0174fe6654ef5e0f908f1edc8f625324a9f49967ccde44a6516b $ spin up --oci ghcr.io/<username>/my-spin-application:v1 INFO spin_publish::oci::client: Pulled ghcr.io/<username>/my-spin-application:v1@sha256:9f4e7eebb27c0174fe6654ef5e0f908f1edc8f625324a9f49967ccde44a6516b Serving http://127.0.0.1:3000其中spin up --oci <reference>会在启动前自动完成拉取,然后基于本地缓存直接运行应用。
认证:复用 Docker 凭据 + 内置spin oci login
历史上dockerCLI 是与容器镜像、Registry 交互的主流工具链,因此提案要求spin oci能够复用已登录用户的 Docker 凭据,同时大多数容器 Registry 服务也都有基于docker login的登录说明。
但本地装有 Docker 不应成为使用 Spin 的前提。为此提案设计了spin oci login命令,让 Spin CLI 直接向目标 Registry 完成认证:
$ spin oci login --username <username> --password <password> # OR 通过 stdin 读取密码,避免密码出现在命令行历史中: $ echo $CONTAINER_REGISTRY_PASSWORD | spin oci login --username <username> --password-stdin这一交互方式对标docker login命令。
源码印证:凭据的获取与存储优先级
从仓库源码看,认证逻辑已经落地在 crates/oci/src/client.rs 的Client::auth中,其凭据解析优先级为:
- 优先读取 Spin 自身的认证配置文件(
$XDG_CONFIG_HOME/fermyon/registry-auth.json,由 crates/oci/src/auth.rs 维护,auths字段以 registry server 为 key、以 base64 编码的username:password为 value); - 读取失败则回退到 Docker 凭据(
docker_credentialcrate),支持UsernamePassword形式;若 Docker 凭据是IdentityToken形式则退化为匿名认证; - 兜底为匿名认证(
RegistryAuth::Anonymous)。
此外Client::login(crates/oci/src/client.rs)在保存凭据前会先调用validate_credentials向 Registry 发送一次认证请求,提前校验凭据有效性,避免用户在第一次 push/pull 时才遇到认证错误。
从 Bindle 迁移到 OCI Registry
将分发机制从 Bindle 切换到 OCI 对项目而言是破坏性变更。为降低迁移成本,提案建议提供从 Bindle 迁移到 OCI Registry 的功能。考虑到该工具的“临时性”——其价值会随时间迅速衰减,且未来移除它会再次造成破坏性变更——该功能最适合做成一个 Spin 插件,按需安装、独立分发:
spin bindle2oci \ --bindle-server <server> \ --bindle-username <username> \ --bindle-password <password> \ --bindle <name> \ --oci <new-reference>注意:该迁移工具在 SIP 008 成文时尚未开始实现(见下文“实现状态”)。
一个完整的实例:manifest、locked app 与本地缓存结构
提案用github-stars-webhook这个应用完整演示了从spin.toml到 OCI manifest、locked application,再到本地拉取缓存目录的完整形态。
应用定义:spin.toml
spin_version = "1" authors = ["Radu Matei <radu.matei@fermyon.com>"] description = "" name = "github-stars-webhook" trigger = { type = "http", base = "/" } version = "0.1.0" [[component]] id = "github-star-webhook" source = "target/spin-http-js.wasm" files = ["my-file.json"] allowed_http_hosts = ["https://hooks.slack.com"] [component.trigger] route = "/..."该应用包含一个组件(github-star-webhook,Wasm 源为target/spin-http-js.wasm)与一个静态资源(my-file.json)。将其发布到 GitHub Container Registry:
$ spin oci push ghcr.io/radu-matei/spin-example:v1 INFO spin_publish::oci::client: Pushed "https://ghcr.io/v2/radu-matei/spin-example/manifests/sha256:8f86a27fbc457416701c4d18680083f598076d0a52dca2a5936e92754a845ed1" $ spin oci pull ghcr.io/radu-matei/spin-example:v1 INFO spin_publish::oci::client: Pulled ghcr.io/radu-matei/spin-example:v1@sha256:8f86a27fbc457416701c4d18680083f598076d0a52dca2a5936e92754a845ed1拉取后的本地缓存目录结构
$ tree /Users/radu/Library/Application\ Support/fermyon/registry └── oci ├── data │ └── sha256:a4699e4f9ef3f4922f38f0d017aa26438908f38caf020a739e0ee27fe796eb02 ├── manifests │ └── ghcr.io │ └── radu-matei │ └── spin-example │ └── v1 │ ├── config.json │ └── manifest.json └── wasm └── sha256:55c29ad4b0ad0c6bd8ec1ffc8f04e63342e5901280037ef706b1b114475d3cbb缓存目录按「Registry 名 / 仓库路径 / tag」组织 manifest,blob 则按 sha256 digest 存放在wasm(Wasm 模块)与data(静态资源)两个目录中。这与仓库中的实现完全对应:Cache::ensure_dirs(crates/loader/src/cache.rs)会创建<root>/registry/{manifests,wasm,data}三个子目录;Client::pull(crates/oci/src/client.rs)将 manifest 写入<cache_root>/registry/oci/manifests/<registry>/<repository>/<tag>/manifest.json,把 Wasm 层写入 wasm 目录、其他层(含归档层解包后)写入 data 目录。
OCI manifest:应用成为多层 artifact
查看manifest.json,可以看到 artifact 配置的顶层 media type 正是application/vnd.fermyon.spin.application.v1+config:
{ "schemaVersion": 2, "config": { "mediaType": "application/vnd.fermyon.spin.application.v1+config", "digest": "sha256:b36160facea3076ad136c09bd4975a429805945ad313b4674363841d5a7f66a0", "size": 643 }, "layers": [ { "mediaType": "application/vnd.wasm.content.layer.v1+wasm", "digest": "sha256:55c29ad4b0ad0c6bd8ec1ffc8f04e63342e5901280037ef706b1b114475d3cbb", "size": 2147122 }, { "mediaType": "application/vnd.wasm.content.layer.v1+data", "digest": "sha256:a4699e4f9ef3f4922f38f0d017aa26438908f38caf020a739e0ee27fe796eb02", "size": 178 } ] }manifest 中出现了两个 layer:组件的 Wasm 模块层与组件引用的静态资源层。每新增一个组件,其 Wasm 模块与静态资源都会成为 manifest 中各自的独立 layer。层与配置对象之间的引用通过sha256digest 建立,这与仓库中assemble_layers系列函数的行为一致:push时先为每个组件的 source、每个依赖、每个 trigger 依赖以及每个静态资源文件生成独立ImageLayer,再以 locked app 序列化结果作为配置层(crates/oci/src/client.rs)。
OCI 配置对象:即 locked application
config.json正是 Spin 能够直接据以运行应用的 locked application manifest:
{ "spin_lock_version": 0, "metadata": { "description": "", "name": "github-stars-webhook", "trigger": { "base": "/", "type": "http" }, "version": "0.1.0" }, "triggers": [ { "id": "trigger--github-star-webhook", "trigger_type": "http", "trigger_config": { "component": "github-star-webhook", "executor": null, "route": "/..." } } ], "components": [ { "id": "github-star-webhook", "metadata": { "allowed_http_hosts": ["https://hooks.slack.com"] }, "source": { "content_type": "application/wasm", "digest": "sha256:55c29ad4b0ad0c6bd8ec1ffc8f04e63342e5901280037ef706b1b114475d3cbb" }, "files": [ { "digest": "sha256:a4699e4f9ef3f4922f38f0d017aa26438908f38caf020a739e0ee27fe796eb02", "path": "my-file.json" } ] } ] }观察组件source与files中的digest字段——它们与 OCI manifest 中各 layer 的 digest 一一对应,即组件内容以内容寻址方式指向 Registry 中的实际 blob。这正构成了 Spin push、pull、run 全流程所需的全部信息:push 时锁定应用的引用,pull 时按 digest 取回内容,run 时从本地缓存还原应用。
这一结构在 crates/oci/src/loader.rs 的OciLoader::load_from_cache中得到印证:读取配置对象后,若包含spin_lock_version字段则将其解析为LockedApp,并把origin元数据写为vnd.fermyon.origin-oci:<reference>(由 crates/oci/src/lib.rs 定义的ORIGIN_URL_SCHEME);随后调用resolve_component_content_refs(crates/oci/src/loader.rs),将每个组件 source、依赖、trigger 依赖的 Wasm 内容替换为缓存中的 wasm 文件路径,把静态资源复制/落盘到临时挂载目录并更新files引用。
关于 Wasm 层 media type 的社区现状
提案明确提示:上述 media type 并非最终定稿,可能随社区对 OCI Registry 中 Wasm 模块的标准化而调整。文档列举了当时的几种既有方案:
wasm-to-oci与oci-distribution使用application/vnd.wasm.content.layer.v1+wasm;solo-io/wasm/spec使用application/vnd.module.wasm.content.layer.v1+wasm;- Docker 的预览实现似乎以
application/vnd.docker.container.image.v1+json分发。
简言之,当时尚无统一标准。倾向于为模块采用application/vnd.module.wasm.content.layer.v1+wasm的理由是未来可能引入components概念。仓库实现中同样保留了这一演进空间:Wasm 层 media type 被定义为常量WASM_LAYER_MEDIA_TYPE,并注释“一旦上游定义规范值将更新”(crates/oci/src/client.rs);拉取时 Wasm 层与数据层分别写入缓存的不同目录,且Cache::wasm_file在 wasm 目录缺失时会回退查找 data 目录(crates/loader/src/cache.rs),以兼容未来 media type 的变化。
spin oci push的设计:宽松的 tag 与版本策略
spin oci push的目标是让用户借助广泛可用的容器 Registry 服务分发应用,并尽可能灵活地与用户现有工作流集成,因此在版本号与 tag 可变性上刻意保持无意见(unopinionated)。
方式一:命令行显式指定引用
$ spin oci push --file <path to spin.toml> myregistry.com/myusername/myapp:v1 # OR $ spin oci push --file <path to spin.toml> myregistry.com/myusername/myapp:latest # OR $ spin oci push --file <path to spin.toml> myregistry.com/myusername/myapp:v0.1.0+r2d2方式二:从spin.toml推导引用与 tag
当spin.toml中的应用名与版本构成完整引用时,无需在命令行重复指定:
name = "myregistry.com/myusername/myapp" version = "1.2.3"$ spin oci push --file <path to spin.toml> ... Pushed the application to myregistry.com/myusername/myapp:v1.2.3 # OR 附加构建信息: $ spin oci push --file <path to spin.toml> --buildinfo ... Pushed the application to myregistry.com/myusername/myapp:v1.2.3+r2d2注意:当未在命令行显式传值、由spin.toml推导引用与 tag 时,应用名必须包含完整限定的引用(fully qualified reference),且 tag 只能是语义化版本。
源码佐证:push 的内部流程
从源码看,Client::push(crates/oci/src/client.rs)的流程为:解析 reference → 获取认证 → 用spin_loader::from_file依据spin.toml构建 locked application → 对每个组件调用validate::ensure_wasms校验 source/dependency/trigger dependency 都是合法 Wasm → 组装层并推送。组装层时支持两种模式:
ComposeMode::All:推送前先用 crates/compose 对组件进行预组合(composition),依赖与 trigger 依赖合并进组件 Wasm,最终每组件一个层;ComposeMode::Skip:跳过组合,依赖与 trigger 依赖(如 HTTP 中间件)各自成为独立层,避免发布结果仍指向推送机器的本地文件系统。
此外还有几项与推送强相关的实现细节:
- 归档层:默认每文件一层(
AssemblyMode::Simple);当层数超过MAX_LAYER_COUNT - 1(常量 500,对齐主流 Registry 的镜像层数上限,如 Elastic Container Registry 的 500 层限制)或设置环境变量SPIN_OCI_ARCHIVE_LAYERS时,改为AssemblyMode::Archive——组件的所有静态资源打包进单个application/vnd.wasm.content.bundle.v1.tar+gzip归档层(crates/oci/src/utils.rs 实现 tar.gz 打包/解包); - 小内容内联:小于
DEFAULT_CONTENT_REF_INLINE_MAX_SIZE(128 字节)的内容直接内联进配置对象的ContentRef,不单独推送 blob,以规避部分 OCI 实现不支持极小 blob 的问题; - 层去重:
assemble_layers对生成的层做unique()去重,多个组件共享同一 Wasm/文件时只推一份; - OCI 注解:除非显式关闭,推送时自动生成
org.opencontainers.image.{authors,title,description,version,created}等预定义注解(显式注解优先),详见all_annotations(crates/oci/src/client.rs); - image config:构建 OCI image config 时写入
architecture: wasm、os: wasip1,并通过 labelcom.fermyon.spin.lockedAppDigest引用 locked app 配置层的 digest,避免不同应用得到相同 image ID。
对spin deploy的影响
与spin oci push的灵活性不同,spin deploy属于有明确观点的意见化工作流,可以继续强制自己的 tag 可变性与版本策略。
提案采用渐进式迁移策略:
- 至少先发布一个包含
spin oci的 minor 版本; - 在 Fermyon Platform 与 Fermyon Cloud 支持接受 OCI 引用之后,先实现
spin deploy --oci功能; - 然后才把
spin deploy的默认行为改为期望 OCI Registry,届时 Fermyon Platform 与 Fermyon Cloud 已同步支持该变更。
实现状态与 FAQ
SIP 008 成文时的实现状态如下:
spin oci push、spin oci pull、spin oci run已在原型 PR 中实现(可参考 crates/oci 模块,其中OciLoader同时支持加载应用与单个 Wasm 包两种 artifact);- 底层通过 Krustlet 的
oci-distributioncrate 与容器 Registry 交互(当时使用一个待回馈上游的 fork); - loader 内部仍需进一步打磨方可合并;
- Bindle → OCI 迁移工具尚未启动;
spin oci login当时尚未实现(其核心逻辑现已在 crates/oci/src/client.rs 与 crates/oci/src/auth.rs 中落地)。
FAQ 还回应了与 Bytecode Alliance Registry 项目(warg)的关系:Spin 维护者也是该 Registry 项目的创建者之一,该项目的目标之一是复用包括 OCI Registry 在内的既有存储作为后端;待其成熟后,Spin 计划将其作为分发机制之一。
关键源码索引
| 关注点 | 位置 |
|---|---|
| SIP 008 原始提案 | docs/content/sips/008-using-oci-registries.md |
| OCI 客户端(push/pull/login/认证) | crates/oci/src/client.rs、crates/oci/src/auth.rs |
| OCI 加载器(pull 后重建可运行应用) | crates/oci/src/loader.rs |
| 归档/解包工具 | crates/oci/src/utils.rs |
OCI 引用识别启发式(is_probably_oci_reference) | crates/oci/src/lib.rs |
| 本地缓存结构(manifests/wasm/data) | crates/loader/src/cache.rs |
| 组件预组合 | crates/compose/src/lib.rs |
补充说明:SIP 008 属于早期设计提案,文中命令语法(如
--file、--buildinfo)与最终 CLI 形态可能存在差异,使用时请以当前版本spin oci push --help的实际输出为准;提案中的参考链接(如原型 PR、oci-distribution)用于理解演进脉络,文中已改为仓库内源码证据。
- 云原生
- 微服务
【免费下载链接】spin
Spin is the open source developer tool for building and running serverless applications powered by WebAssembly.
相关推荐
Spin 服务链式调用(Service Chaining)深度解析:SIP 017 设计决策与源码实现
Spin 服务链式调用(Service Chaining)深度解析:SIP 017 设计决策与源码实现 本篇技术指南围绕 Spin 官方设计提案 SIP 017
云原生微服务Spin 插件体系深度解析:基于 SIP 006 的 `spin plugin` 命令、Manifest 规范与源码实现
Spin 插件体系深度解析:基于 SIP 006 的 spin plugin 命令、Manifest 规范与源码实现 本文以 Spin 开源仓库中的 SIP 0
云原生微服务GSD 工作流工具通过 MCP 暴露实现 Provider Parity:ADR-008 实施计划深度解析
GSD 工作流工具通过 MCP 暴露实现 Provider Parity:ADR 008 实施计划深度解析 本文是 GSD 项目内部架构决策 ADR 008(将
人工智能AI Agent代码智能体Agent 编排CLIAI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考