Rust serde over terraform plan JSON resource_drift and relevant_attributes
Drift is what refresh saw. Relevance is which attributes fed the plan.
<!-- hal:authoritative:yaml -->
Drift is what refresh saw. Relevance is which drifted attributes fed the plan. Classify both before you gate CI.
§I. Frame
09-26 taught resource_changes and the replace that hides in two actions. 09-29 taught apply round-trips. Leave those shelves.
Tonight tf_day_dev_counter is 22 (1 mod 3): Rust-around-TF again. The plan JSON fields are resource_drift and relevant_attributes. HashiCorp documents them together: drift lists external changes detected during refresh; relevant attributes list resource/attribute paths that contributed to planned changes, so you can filter which drift may have shaped the plan.
Crate name: drift_census. It parses a plan, classifies each drift row, and exits 2 when any row is Relevant.
§II. Three classes of drift
Relevance Index (named technique). Before you treat every drift as a pager, ask whether the plan emitted a relevance index.
| Class | Meaning |
|---|---|
| Relevant | Address appears in relevant_attributes |
| Irrelevant | Drift present, index present, address absent |
| Unmarked | Index omitted or empty; drift still real, relevance unknown |
Terraform may emit resource_drift without relevant_attributes. That is Unmarked, not Irrelevant. Do not invent relevance when the index is missing.
§III. A real plan with drift
fixture/main.tf uses hashicorp/local local_file plus terraform_data. No cloud credentials.
Sequence on the lab Mac (Terraform 1.14.3):
terraform applywithphase=1(markercontentalpha).- Hand-edit
marker.txttobeta-external(outside Terraform). terraform plan -var=phase=2 -out=tfplanthenterraform show -json tfplan > plan.json.
Result: real resource_drift for local_file.marker with actions ["delete"] (local provider treats content/id mismatch as gone). resource_changes includes a create for the phase-2 content. This run did not emit relevant_attributes.
unmarked local_file.marker Delete
summary drift=1 relevant=0 irrelevant=0 unmarked=1 planned_changes=3
fixture/plan-refresh-only.json is the same hand-edit under terraform plan -refresh-only: drift present, resource_changes empty. Cert owns the refresh-only CLI story; Dev only needs the JSON shape.
§IV. Teaching twin for the relevance index
Because the real local_file plan omitted relevant_attributes, fixture/plan-with-relevant.json keeps the real marker drift object, adds a synthetic terraform_data.noise update drift, and injects:
"relevant_attributes": [
{"resource": "local_file.marker", "attribute": ["content"]}
]
That lets the census print Relevant vs Irrelevant on a format-faithful document. fixture/README.md records which rows are real and which are teaching.
RELEVANT local_file.marker Delete
irrelevant terraform_data.noise Update
summary drift=2 relevant=1 irrelevant=1 unmarked=0 planned_changes=3
Exit code 2 when relevant > 0.
§V. The types
#[derive(Debug, Deserialize)]
pub struct Plan {
pub format_version: String,
pub terraform_version: String,
#[serde(default)]
pub resource_drift: Vec<ResourceChange>,
#[serde(default)]
pub resource_changes: Vec<ResourceChange>,
#[serde(default)]
pub relevant_attributes: Vec<RelevantAttribute>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DriftClass { Relevant, Irrelevant, Unmarked }
Action is a closed kebab-case enum (no-op, create, read, update, delete, forget). An unknown verb fails at parse time, same discipline as 09-26.
classify_drift builds a set of addresses from relevant_attributes. Empty index → every drift row is Unmarked. Non-empty index → membership decides Relevant vs Irrelevant.
§VI. Tests and run
cd crate
cargo test # 4 passed
cargo run -- ../fixture/plan.json
cargo run -- ../fixture/plan-with-relevant.json # exit 2
Tests cover: real plan unmarked marker drift; teaching fixture split; empty plan quiet; unknown action parse error.
§VI.b. Why local_file drift looked like delete
The real fixture used hashicorp/local local_file. That provider keys id off content hashes. When you hand-edit the file, Read no longer finds the object Terraform remembered, so plan JSON records drift actions ["delete"] and a normal phase-2 plan may show a matching create. That is still drift: the remote (the file) changed outside apply.
Cloud providers more often emit ["update"] drift for in-place attribute edits. The census does not special-case delete-shaped drift. It only asks: is there a relevance index, and does this address appear in it?
Paired Cert shows the same hand-edit under -refresh-only, where resource_changes stays empty and state update is the only apply goal.
§VII. Close
09-26 gated on destroy-first replace inside resource_changes. Tonight gates on Relevant drift when the plan supplies a relevance index, and refuses to pretend Unmarked is Irrelevant. Ops declares Azure lifecycle policy. Cert teaches refresh-only as the CLI that updates state for the same drift story.
Related
- Tome: TRPL ch6/9/12 — grounded-in; Brikman pp.548-549 — referenced
- Bootcamp: tfpro Lab 22 — referenced
- Prior Dev: plan_gate 09-26 · apply round-trip 09-29 · Python plan census 09-05