- DevOps
- 云原生
- 后端
【免费下载链接】digger
Digger is an open source IaC orchestration tool. Digger allows you to run IaC in your existing CI pipeline ⚡️
导读
本文以 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:8080make 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-approve5) 查看 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-approve7) 再次查看状态
cd ../../.. ./taco unit status --prefix org/app/预期结果:
- B 为 green(确认完成);
- C 仍为 yellow,直到 C 自身写入一次。
8) 应用 C,确认 B→C 边
cd examples/dependencies/c terraform apply -auto-approve9) 最终状态
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)的执行流程:
- 加锁:对
__opentaco_system单元获取锁(deps-<nanotimestamp>形式的锁 ID)。若图缺失或已被他人锁定,静默跳过——绝不让依赖图问题拖垮调用方的写入。 - 读取图 state:从存储下载
__opentaco_system的 tfstate 并解析为TFState结构(只关心serial、lineage、resources[].instances[].attributes)。 - 解析新状态 outputs:从写入的 tfstate 中提取
outputs。 - 遍历边(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"。
- A) 源刷新(outgoing):当
- 原子写回:只有发生变更(
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 中的路由注册):
- 读取
__opentaco_system图 tfstate,构建边列表与邻接表from → to,并收集目标单元的入边(incoming)详情。 - 标红:所有
status == "pending"的边,其to_unit标为 red。 - 黄色传播:从所有红色单元出发做BFS,沿邻接表向下游传播,标记为 yellow(不覆盖已有的 red)。
- 汇总:对入边按状态统计
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 ⚡️
相关推荐
OpenTaco 输出级依赖实战:用 `opentaco_dependency` 在无数据库状态下实现 red/yellow/green 依赖状态追踪
OpenTaco 输出级依赖实战:用 opentaco_dependency 在无数据库状态下实现 red/yellow/green 依赖状态追踪 本文基于 D
DevOps云原生后端superpowers SDD 计划级隔离工作区:跨计划账本冲突的 RED→GREEN 实证评估
superpowers SDD 计划级隔离工作区:跨计划账本冲突的 RED→GREEN 实证评估 本文以 superpowers 仓库中 SDD(Subagen
AI 技能AI 插件开发工具大麦网抢票脚本:把开售瞬间的 3 秒人工延迟压到接近 0
大麦网抢票脚本:把开售瞬间的 3 秒人工延迟压到接近 0 开票倒计时结束时,你的鼠标还在找"立即购买"按钮,页面已经弹出"已售罄"。这套大麦抢票脚本做的事情很简
网页爬虫工作流自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考