DocumentsBuilding on it

Terraform

2 min readSource terraform.mdsha256 f3bd68c45f563b79…

A bank can manage its Mirror posture as code:

  • the corridors it proposes and ratifies;
  • the witnesses it vouches for;
  • the https endpoints it wants pushed to.

There are two ways, both shipped as source: a module (terraform/, with examples/bank-a) that needs nothing but Terraform and Node, and a compiled provider (terraform/provider, Go) that adds drift detection and import.

The module#

module "mirror" {
  source            = "./terraform"
  bander_command    = ["/opt/mirror/mirror-bander"]
  signer            = { custody_file = "/etc/mirror/member-signer.json" }
  propose_corridors = { cu_v1 = { product = "cu", version = 1, edges = [7800, 8200, 8600, 9000], tolerance = 1 } }
  webhooks          = { gateway = { url = "https://mirror-hooks.bank.example/deliveries" } }
}

Each resource runs a helper (terraform/mirror-tf.js) that does only the HTTP. Before sending an event, the helper reads the live registry and skips anything this member already proposed or ratified, so re-applying changes nothing. The module does not notice a subscription removed outside Terraform; the provider does.

The provider#

terraform {
  required_providers {
    mirror = { source = "terraform.local/mirror/mirror" }
  }
}

provider "mirror" {
  bander_command = ["/opt/mirror/mirror-bander"]          # signs; has no network
  custody_file   = "/etc/mirror/member-signer.json"       # or key_file, for a trial
}

resource "mirror_webhook_subscription" "gateway" {
  url = "https://mirror-hooks.bank.example/deliveries"
}

resource "mirror_governance_event" "copper" {
  type    = "corridor.propose"
  payload = jsonencode({ product = "cu", version = 1, edges = [7800, 8200, 8600, 9000], closed = true, max_ratio = 1.2 })
}

data "mirror_registry" "live" {}                          # members, ratified corridors, active witnesses
  • mirror_webhook_subscription reads the operator's live list on every plan. A subscription removed outside Terraform shows up as drift and is recreated. An existing one can be adopted with terraform import mirror_webhook_subscription.gateway wh_.... Ids derive from (member, url, events), so a recreated subscription gets the same id.
  • mirror_governance_event sends a signed corridor, witness or member proposal or ratification. It reports applied = false when its effect is already on the registry for this member.
  • Building it: go build -o terraform-provider-mirror . in terraform/provider, using terraform-plugin-framework 1.19. Then point Terraform at it with a CLI configuration dev_overrides block for terraform.local/mirror/mirror.

How both keep their promises#

  • The key never enters Terraform. The bander signs every event and every login (sign-event, sign-login), from a key file or the HSM, and the bander has no network. Neither the configuration nor the state holds key material; both test suites check the state.
  • Signed answers are verified against the operator's JWKS. The helper refuses to run without the verifier, and the provider rejects a subscription receipt that does not verify.
  • Governance is append-only. terraform destroy removes webhook subscriptions. It never un-ratifies anything, and the provider says so in a warning. Retiring a corridor or suspending a witness is its own signed event, decided by quorum.

What is proven#

Both suites were run with Terraform 1.16.4, against a running operator in production posture.

The module: server/terraform-e2e.test.js, 12 checks, drives a real terraform binary:

  • init and validate on the shipped example and on a bank module;
  • apply, and a second plan that wants no changes;
  • no key material in the state;
  • destroy, which leaves governance on the record.

The provider: server/terraform-provider.test.js, 19 checks. It builds the provider with Go 1.27, then checks:

  • validate and apply;
  • drift: a subscription removed behind Terraform's back is recreated;
  • import by id, followed by a plan with no changes;
  • no private key in the state;
  • the destroy warning.

The helper alone is also tested by server/terraform-helper.test.js: propose, ratify and an idempotent re-apply; subscribe and unsubscribe; the bander refusing an event that names another member; loud configuration errors.

What is not done#

  • Not on the Terraform Registry. Publishing needs a registry namespace and a signing key for releases; that is the maintainer's decision. Until then, build it from source.
  • Only dev_overrides was tested. A filesystem mirror for air-gapped installs is standard Terraform, but it has not been exercised here.