Hedronite · Ops Lesson · 01-Earth-DevOps · Mon 2026-10-05

Terraform Azure storage management policy one policy per account, rule prefixes, Rust census

One policy per account. Rules own prefixes. Missing policy is not an empty lifecycle.

Lesson Class: Ops (T2 · Terraform + Rust ops automation)
Cloud Referent: Azure · azurerm_storage_management_policy · prefix_match
Automation: cargo script + azure_mgmt_storage 0.21 + Nix writeBashBin
Checked: terraform validate · cargo check · census illustrative (no Azure creds)
Paired Dev: serde resource_drift / relevant_attributes
Paired Cert: refresh-only plans and drift handling
Policy Cardinality
Ask how many policy resources exist before naming a rule.
Missing ≠ Empty
ARM 404 on default is no lifecycle, not zero rules.
A green plan can leave an account with no lifecycle while reviewers point at a rule that never shipped.

<!-- hal:authoritative:yaml -->

One management policy resource per storage account. Rules own prefixes and the enabled flag. Missing the policy is not an empty lifecycle.

§I. Frame

Tonight is TF day 75, trio #141. Recent TF Ops sat on AWS and GCP (CloudWatch retention 10-02, Artifact Registry cleanup 09-29, ECR scan 09-26). Rebalance to Azure. The cloud-native referent is Blob lifecycle: azurerm_storage_management_policy on a storage account, plus a read-only Rust census of which accounts have a policy and which rule prefixes are actually declared.

10-01 already counted Service Bus queues with azure_mgmt_servicebus. 09-17 taught Key Vault ephemeral/write-only. Those shelves are adjacent and not this claim.

The problem for today: declare one management policy with two rules (one enabled with prefixes, one disabled), then build a cargo-script census that lists storage accounts, fetches policy name default, and flags missing policies, disabled rules, and empty prefix_match.

§II. Three owners on one account

Policy Cardinality (named technique). Ask how many policy resources exist before asking what a rule deletes.

  1. Policy belongs to azurerm_storage_management_policy. ARM stores one management policy per storage account. The Terraform resource maps to that singleton. The ARM name is always default.
  2. Rules belong inside the policy. Each rule has name, enabled, filters (blob_types, prefix_match), and actions (base_blob tier/delete, optional snapshot/version).
  3. Prefixes belong to the rule filter. Microsoft expects prefixes that start with the container name (for example logs/active/). An empty prefix_match matches broadly; that is a different claim than "no policy".

If you mix up the owners, a green plan can leave an account with no lifecycle while reviewers point at a rule block that never shipped.

§III. The Terraform

This module is the teaching referent. Validate with terraform init -backend=false and terraform validate against hashicorp/azurerm ~> 4.0. Do not apply from this fire.

resource "azurerm_storage_management_policy" "demo" {
  storage_account_id = azurerm_storage_account.demo.id

  rule {
    name    = "logs-cool-archive-delete"
    enabled = true
    filters {
      blob_types   = ["blockBlob"]
      prefix_match = ["logs/active/"]
    }
    actions {
      base_blob {
        tier_to_cool_after_days_since_modification_greater_than    = 30
        tier_to_archive_after_days_since_modification_greater_than = 180
        delete_after_days_since_modification_greater_than          = 2555
      }
    }
  }

  rule {
    name    = "scratch-delete"
    enabled = false
    filters {
      blob_types   = ["blockBlob"]
      prefix_match = ["scratch/"]
    }
    actions {
      base_blob {
        delete_after_days_since_modification_greater_than = 7
      }
    }
  }
}

Fact one. One azurerm_storage_management_policy per storage account. A second resource against the same account fights the singleton.

Fact two. enabled = false keeps the rule text in ARM and out of active evaluation. Disabled is not deleted.

Fact three. Missing the policy resource is not the same as a policy with zero rules. ARM 404 on managementPolicies/default means no lifecycle policy.

Fact four. blob_types is required in the filter. Valid values are blockBlob and appendBlob. Tiering actions apply to block blobs.

Bundle file: storage-management-policy.tf next to this lesson.

§IV. The Rust census

Automation is a cargo script, not a Python CLI. Client: azure_mgmt_storage 0.21 default tag package_2023_05. Flow matches the 10-01 Service Bus pattern: DefaultAzureCredential, Client::new against https://management.azure.com, then:

  1. storage_accounts_client().list(&sub).into_stream() for accounts.
  2. Parse resource group from the account id.
  3. management_policies_client().get(&rg, &name, &sub, "default").await.
  4. Classify: missing policy (404), disabled rules, empty prefix_match.
let mut pages = client.storage_accounts_client().list(&sub).into_stream();
// for each account → management_policies_client().get(..., "default")
// print rule rows; flag ATTENTION for missing_policy / disabled / empty_prefix

Full script: storage-management-policy-census.rs (nightly -Zscript). Nix wrapper: storage-management-policy-census.nix (writeBashBin).

Illustrative output (no Azure credentials on the lab Mac as of this fire; the lab must export working credentials before a live census):

rg-app/hedronitesa01	rule=logs-cool-archive-delete	enabled=true	blob_types=blockBlob	prefix=logs/active/
rg-app/hedronitesa01	rule=scratch-delete	enabled=false	blob_types=blockBlob	prefix=scratch/
rg-app/hedronitesa01	ATTENTION	disabled=scratch-delete
rg-legacy/oldsa01	ATTENTION	missing_policy
summary accounts=2 with_policy=1 missing_policy=1 disabled_rules=1 empty_prefix=0 attention=2

§V. How to run

# Validate the referent (no apply)
terraform init -backend=false
terraform validate

# Census (needs AZURE_SUBSCRIPTION_ID + DefaultAzureCredential)
cargo +nightly -Zscript ./storage-management-policy-census.rs
# or: nix-build -E 'with import <nixpkgs> {}; callPackage ./storage-management-policy-census.nix {}'

If credential resolution fails, stop. Re-auth is a human step on the lab Mac. Do not paste keys into the lesson tree.

§VI. Close

Azure Blob lifecycle is one policy per account. Tonight's Ops claim is cardinality, rule ownership of prefixes and enabled, and a Rust census that reads what ARM reports. Paired Dev parses plan JSON resource_drift / relevant_attributes. Paired Cert covers plan -refresh-only and why terraform refresh is gone.

Related