☰
OpenTaco 输出级依赖实战:用 `opentaco_dependency` 与 `__opentaco_system` 图工作区实现单元状态(red/yellow/green)计算
2026/10/14 18:12:39 网站建设 项目流程
  • DevOps
  • 云原生
  • 后端

【免费下载链接】digger

Digger is an open source IaC orchestration tool. Digger allows you to run IaC in your existing CI pipeline ⚡️

项目地址:https://gitcode.com/gh_mirrors/di/digger
点击查看免费下载

导读

本文以 OpenTaco(digger 仓库中的 Layer-0 状态控制组件)的官方依赖示例taco/examples/dependencies/为主线,完整讲解如何在不引入任何外部数据库的前提下,通过opentaco_dependency这个 Terraform Provider 资源声明"输出级依赖"(A → B → C),并借助一个专用图工作区__opentaco_system的普通 Terraform state 计算每个单元(unit)的状态:red(需重新 apply)、yellow(上游红色、可能需重新 apply)、green(无需操作)。读完本文,你将掌握:图工作区与依赖边的声明方式、状态写入时的"源刷新 + 目标确认"钩子机制、摘要式 digest 哈希存储原理,以及taco unit status命令的完整用法与源码级实现依据。

一、为什么需要"输出级依赖"与一个零数据库的图

在 IaC 多状态(multi-state)场景下,一个工作区的输出往往会被另一个工作区消费(例如数据库连接串db_url被应用 B 引用、镜像标签image_tag被应用 C 引用)。OpenTaco 的做法是:显式声明输出级依赖边(edge),由服务在每次状态写入时自动更新依赖边上的摘要信息,从而在查询时推导出每个单元的健康状态。

该方案最核心的设计约束是:所有依赖元数据只存于一个专用工作区__opentaco_system的普通 Terraform state 中,不需要新增数据库、不需要额外中间件。正如 taco/README.md 所述:"OpenTaco supports output-level dependencies between units without any external database. All metadata is stored as digests in a dedicated graph workspace unit."(OpenTaco 支持单元之间的输出级依赖,且无需任何外部数据库,所有元数据以 digest 摘要形式存储在专用图工作区单元中)。

从源码结构看,依赖图相关实现集中在taco/internal/deps/graph.go(图更新与状态计算)、taco/providers/terraform/opentaco/resources/dependency_resource.go(Provider 资源)与taco/cmd/taco/commands/unit.go(CLI 命令)三处,本文后续将逐一对应讲解。

二、核心概念与数据模型

在动手之前,先厘清这套机制中的关键概念(与 taco/agents_done/004_dependencies.md 中的设计定义一致):

概念说明
Unit(单元)一份受管 Terraform state 的逻辑标识,例如org/app/A;后端对象实际存储于<prefix>/<unit-id>/terraform.tfstate
Source(源)边的起点,由(from_unit_id, from_output)描述,即"哪个单元的输出"
Target(目标)边的终点,由(to_unit_id, to_input)描述,to_input仅用于文档/展示用途
Edge(依赖边)从源输出指向目标输入的一条依赖,对应一个opentaco_dependency资源
in_digest源输出最新值的哈希摘要(由服务在源单元写入时刷新)
out_digest目标最后一次确认(acknowledge)的哈希摘要
status边的状态:pending(上游有新变更未确认)、ok(已确认一致)、unknown(源输出缺失或无法解析)
last_in_at/last_out_at源刷新、目标确认的 RFC3339 UTC 时间戳

状态写入时的两条更新规则(即 taco/internal/deps/graph.go 中UpdateGraphOnWrite的职责):

  • 源刷新(outgoing):当单元 S 写入状态时,找出所有from_unit_id == S的边。若from_output存在于新状态的 outputs 中,则用其摘要更新in_digest,并重新计算status(in_digest == out_digest时为ok,否则为pending);若源输出缺失或 outputs 无法解析,则置为unknown。
  • 目标确认(incoming):当单元 S 写入状态时,找出所有to_unit_id == S的边。若in_digest非空,则执行out_digest ← in_digest(复制摘要),设置last_out_at,并将status置为ok。

三、示例全景:A → B → C 依赖演示

官方示例位于 taco/examples/dependencies/,目录结构如下:

taco/examples/dependencies/ ├── system/ # 图工作区:声明单元与依赖边(backend 指向 __opentaco_system) ├── a/ # 平凡 Terraform 项目,输出 db_url(每次 apply 都会变化) ├── b/ # 平凡 Terraform 项目,输出 image_tag ├── c/ # 平凡 Terraform 项目,输出 name └── README.md

三个工作区a/、b/、c/都是极简的 Terraform 项目,通过 OpenTaco 的 HTTP backend 写入自己的 state;system/是唯一的"特殊"工作区,其 state 即依赖图本身。

3.1 图工作区(system/):声明单元与两条边

system/main.tf 的完整内容如下:

terraform { required_providers { opentaco = { source = "digger/opentaco" } } backend "http" { address = "http://localhost:8080/v1/backend/__opentaco_system" lock_address = "http://localhost:8080/v1/backend/__opentaco_system" unlock_address = "http://localhost:8080/v1/backend/__opentaco_system" } } provider "opentaco" { endpoint = "http://localhost:8080" } # Explicitly declare units so they are registered and visible in listings resource "opentaco_unit" "a" { id = "org/app/A" } resource "opentaco_unit" "b" { id = "org/app/B" } resource "opentaco_unit" "c" { id = "org/app/C" } # A -> B on db_url resource "opentaco_dependency" "a_to_b_dburl" { from_unit_id = "org/app/A" from_output = "db_url" to_unit_id = "org/app/B" to_input = "db_url" depends_on = [opentaco_unit.a, opentaco_unit.b] } # B -> C on image_tag resource "opentaco_dependency" "b_to_c_image" { from_unit_id = "org/app/B" from_output = "image_tag" to_unit_id = "org/app/C" to_input = "image_tag" depends_on = [opentaco_unit.b, opentaco_unit.c] }

要点解读:

  • backend 指向:图工作区通过 HTTP backend 把自身 state 存入__opentaco_system,三个地址(address / lock_address / unlock_address)均指向同一路径。
  • opentaco_unit:显式声明单元,使它们在列表中可见(若不声明,边上的单元可能无法在taco unit ls中列出)。其实现见 unit_resource.go,创建时通过 SDK 调用服务端CreateUnit完成注册。
  • opentaco_dependency:每条边一个资源。from_output/to_input分别描述源输出与目标输入(to_input为可选,缺省时自动取from_output)。注意资源间用depends_on显式保证单元先于依赖边创建。

3.2 三个数据工作区(a/、b/、c/)

a/main.tf:

terraform { backend "http" { address = "http://localhost:8080/v1/backend/org/app/A" lock_address = "http://localhost:8080/v1/backend/org/app/A" unlock_address = "http://localhost:8080/v1/backend/org/app/A" } } output "db_url" { # Changes on every apply to simulate new upstream output value = "postgres://a.example/db-${timestamp()}" } output "subnet_ids" { value = ["subnet-1234", "subnet-4567"] }

b/main.tf:

terraform { backend "http" { address = "http://localhost:8080/v1/backend/org/app/B" lock_address = "http://localhost:8080/v1/backend/org/app/B" unlock_address = "http://localhost:8080/v1/backend/org/app/B" } } locals { # Some trivial output to ensure state writes on apply image_tag = "v1.0.0" } output "image_tag" { value = local.image_tag }

c/main.tf:

terraform { backend "http" { address = "http://localhost:8080/v1/backend/org/app/C" lock_address = "http://localhost:8080/v1/backend/org/app/C" unlock_address = "http://localhost:8080/v1/backend/org/app/C" } } # C does not need any particular outputs; any write will acknowledge incoming edges. locals { name = "service-c" } output "name" { value = local.name }

设计要点:A 的db_url输出内嵌timestamp(),每次 apply 都会产生新值,从而天然模拟"上游产生新变更";B、C 的输出是静态的,用于演示"目标确认"后边状态从pending恢复为ok。

四、快速上手:完整演练 9 步

以下操作均需满足前置条件:Go 1.25+与Terraform CLI,并在taco/目录下执行。

1) 构建并启动服务

make build ./statesman -auth-disable -storage memory # Service on http://localhost:8080
  • make build会依次构建服务(cmd/statesman)、CLI(cmd/taco)、Terraform Provider 与 Token 服务(见 taco/Makefile 的build目标,各产物statesman、taco、terraform-provider-opentaco、token_service输出到taco/根目录)。
  • -auth-disable关闭鉴权(对应OPENTACO_AUTH_DISABLE=true,见 cmd/statesman/main.go);-storage memory使用内存存储,适合本地演示(S3 配置缺失时会自动回退到内存存储)。

2) 应用图工作区,注册单元与依赖边

cd examples/dependencies/system terraform init terraform apply -auto-approve

执行后会在__opentaco_system中创建依赖图,注册 A、B、C 三个单元,并建立两条边:A→B (db_url)、B→C (image_tag)。

3) 依次应用三个工作区,写入初始状态

# A cd examples/dependencies/a terraform init terraform apply -auto-approve # B cd ../b terraform init terraform apply -auto-approve # C cd ../c terraform init terraform apply -auto-approve

为 A、B、C 各创建一份 state,使其出现在列表与状态计算中。

4) 重新应用 A,模拟上游新变更

A 的db_url输出包含时间戳,每次 apply 都会变化。重新 apply A 后,A→B边会被标记为pending:

cd ../a terraform apply -auto-approve

5) 查看 B 与 C 的状态

cd ../../.. ./taco unit status --prefix org/app/

预期结果:

  • B 为 red:有 1 条入边处于pending(A→B);
  • C 为 yellow:位于红色单元 B 的下游(传递影响)。

6) 应用 B,确认入边并转绿

cd examples/dependencies/b terraform apply -auto-approve

7) 再次查看状态

cd ../../.. ./taco unit status --prefix org/app/

预期结果:

  • B 为 green(确认完成);
  • C 仍为 yellow,直到 C 自身写入一次。

8) 应用 C,确认 B→C 边

cd examples/dependencies/c terraform apply -auto-approve

9) 最终状态

cd ../../.. ./taco unit status --prefix org/app/ # All green

全部单元转绿,链条 A→B→C 完全收敛。

4.1 状态语义(红/黄/绿判定)

  • red:单元存在任意一条入边状态为pending(上游有新变更、尚未被本单元确认);
  • yellow:单元没有pending入边,但其上游(传递闭包内)存在红色单元;
  • green:既非 red 也非 yellow。

CLI 输出时会把状态渲染为友好、带颜色的标签:up to date(绿,无 pending 入边)、needs re-apply(红,至少一条 pending 入边)、might need re-apply(黄,入边干净但上游为红),参见 taco/README.md 的 "Dependencies & Unit Status" 一节。

五、源码级原理:图如何更新与状态如何计算

5.1 写钩子:UpdateGraphOnWrite(源刷新 + 目标确认)

依赖图更新由服务在每次状态写入时异步触发,调用点包括:

  • HTTP backend 写入:taco/internal/backend/handler.go;
  • S3 兼容端点写入:taco/internal/s3compat/handler.go;
  • 管理上传:taco/internal/unit/handler.go。

三处均为go deps.UpdateGraphOnWrite(...)的fire-and-forget非阻塞调用,即图更新失败不会阻断用户的 apply 写入。

UpdateGraphOnWrite(taco/internal/deps/graph.go)的执行流程:

  1. 加锁:对__opentaco_system单元获取锁(deps-<nanotimestamp>形式的锁 ID)。若图缺失或已被他人锁定,静默跳过——绝不让依赖图问题拖垮调用方的写入。
  2. 读取图 state:从存储下载__opentaco_system的 tfstate 并解析为TFState结构(只关心serial、lineage、resources[].instances[].attributes)。
  3. 解析新状态 outputs:从写入的 tfstate 中提取outputs。
  4. 遍历边(state surgery):扫描所有type == "opentaco_dependency"的资源实例,按from_unit_id/to_unit_id匹配当前写入单元:
    • A) 源刷新(outgoing):当fromID == 当前单元时,若from_output存在于新 outputs,计算新摘要dig;若in_digest != dig,则更新in_digest与last_in_at,并按dig == out_digest与否置ok/pending;若源输出缺失或 outputs 无法解析,置unknown。
    • B) 目标确认(incoming):当toID == 当前单元时,若in_digest非空,则out_digest ← in_digest、更新last_out_at、置status = "ok"。
  5. 原子写回:只有发生变更(changed == true)时才递增顶层serial并写回图 tfstate(携带锁 ID 完成带锁写入)。

这一"读 → 内存修改 → 递增 serial → 写回 → 解锁"的循环(read-modify-write)确保了对数百条边也能在一次加锁写中批量完成,避免频繁竞争。

5.2 摘要计算:canonical JSON → SHA-256 → base58

依赖图上只存摘要与时间戳,绝不落明文输出值。摘要由digestValue(taco/internal/deps/graph.go)计算:先对输出值做规范化 JSON 序列化(canonicalJSON:对象键按字典序排序、无多余空白、数组保持顺序,见同文件 canonicalJSON),再取 SHA-256,最后用 base58 编码。

摘要相等即代表输出值相等,因此比较永远只是摘要比较,明文输出不会离开各个工作区的 state。

5.3 状态推导:ComputeUnitStatus(red/yellow/green + BFS 传播)

状态在查询时实时推导,入口为 taco/internal/deps/graph.go 的ComputeUnitStatus(由GET /v1/units/:id/status调用,见 taco/internal/unit/handler.go 与 routes.go 中的路由注册):

  1. 读取__opentaco_system图 tfstate,构建边列表与邻接表from → to,并收集目标单元的入边(incoming)详情。
  2. 标红:所有status == "pending"的边,其to_unit标为 red。
  3. 黄色传播:从所有红色单元出发做BFS,沿邻接表向下游传播,标记为 yellow(不覆盖已有的 red)。
  4. 汇总:对入边按状态统计IncomingOK/IncomingPending/IncomingUnknown,并按 "pending → unknown → ok" 的优先级排序后返回。

返回的UnitStatus结构包含unit_id、status、incoming(入边详情数组)与summary三部分,即 API 与 CLI 的数据来源。

若图缺失或损坏,ComputeUnitStatus会返回"尽力而为"的空绿色状态(视为无依赖边),与写钩子的"静默跳过"策略保持一致:依赖图故障不应阻塞任何单元操作。

5.4 Provider 端:确定性边 ID 与计算字段

opentaco_dependency资源(dependency_resource.go)的关键行为:

  • 确定性边 ID:computeEdgeID对from_unit_id + "\n" + from_output + "\n" + to_unit_id + "\n" + to_input做 SHA-256 并 base58 编码,保证同一条边在任何环境、任何顺序下 ID 一致(L190-L197)。
  • Schema 字段:必填from_unit_id、from_output、to_unit_id;可选to_input(缺省取from_output);计算字段id、in_digest、out_digest、status、last_in_at、last_out_at(L47-L79)。
  • Create/Update 时初始化计算字段:digest 置空、时间戳为 null、status初始为unknown,确保 apply 后不留 Unknown 值;Update会把既有 state 中的计算值延续下来(L82-L178)。
  • Read 不做远程调用:以本地 state 为准(服务可能通过 state surgery 修改过图 tfstate)。

5.5 CLI:taco unit status的实现

命令实现在 taco/cmd/taco/commands/unit.go:

  • 用法:taco unit status [unit-id],或--prefix <pfx>按前缀批量查询(--prefix /等价于全部)。
  • 无参数时先ListUnits获取单元列表,再逐个调用GetUnitStatus。
  • 输出为对齐表格,列为UNIT / STATUS / PENDING / FIRST OFFENDER,其中PENDING来自Summary.IncomingPending,FIRST OFFENDER为第一条 pending 入边的from_unit_id/from_output。
  • -o json(即--output json)输出机器可读的完整 JSON(json.MarshalIndent)。

六、注意事项与边界行为

  • ID 是逻辑 ID:例如org/app/A,后端按<prefix>/<unit-id>/terraform.tfstate存储对象;normalizeUnitID会容忍带/不带terraform.tfstate后缀的写法(graph.go)。
  • 只存摘要与时间戳:图中不持久化任何明文输出值;opentaco_dependency的 schema 描述也明确 "Stores only digests and timestamps in tfstate"。
  • 非 localhost 场景:若服务不在http://localhost:8080,需自行编辑各工作区的backend块与 provider 的endpoint(也可用环境变量OPENTACO_ENDPOINT覆盖 provider 地址)。
  • __opentaco_前缀保留:__opentaco_system属于平台保留命名空间,不应挪作他用(见 taco/README.md 的 "System Unit Convention")。
  • 图更新失败不影响业务写入:写钩子与状态计算均采用"尽力而为"策略,图缺失/损坏时返回空绿色或静默跳过。

七、延伸阅读

若希望进一步深入,可在当前仓库中继续阅读:

  • 依赖设计与验收标准:taco/agents_done/004_dependencies.md
  • 图更新与状态计算核心实现:taco/internal/deps/graph.go
  • Provider 资源实现:taco/providers/terraform/opentaco/resources/dependency_resource.go、taco/providers/terraform/opentaco/resources/unit_resource.go
  • CLI 命令实现:taco/cmd/taco/commands/unit.go
  • 状态 API 处理器:taco/internal/unit/handler.go
  • 构建与启动方式:taco/Makefile、taco/cmd/statesman/main.go
  • 平台总览:taco/README.md
  • DevOps
  • 云原生
  • 后端

【免费下载链接】digger

Digger is an open source IaC orchestration tool. Digger allows you to run IaC in your existing CI pipeline ⚡️

项目地址:https://gitcode.com/gh_mirrors/di/digger
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询