Skip to main content

Terraform state backend

Orunbase serves Terraform's native backend "http" protocol, so a tenant's Terraform state can live on the platform instead of in an S3 bucket plus OIDC role in the tenant's own AWS account. There is nothing to provision: if a repo is linked and CI can push objects, it can store Terraform state at:

https://api.orunbase.com/v1/organizations/{orgId}/projects/{projectId}/state/tfstate/{component}/{environment}

One state address exists per (org, project, component, environment); a single row holds the state document and its lock atomically, so a lock can never drift from the state it protects.

Methods

MethodBehavior
GETReturns the raw state document (200, content-type: application/json). 404 when no state has been written — Terraform treats that as a fresh state.
POST ?ID=<lockId>Writes the state document, honoring the held lock: a write carrying the wrong (or no) lock id while a lock is held answers 423 with the holder's lock-info document. An empty body or non-JSON body is 422 validation_failed.
DELETEDeletes the state document (200).
LOCKAcquires the lock. The request body is Terraform's lock-info document; a conflicting holder answers 423 with that holder's lock-info.
UNLOCKReleases the lock. The body is {"ID": "…"}; releasing a lock held by someone else answers 423.

LOCK and UNLOCK are only valid on tfstate paths — anywhere else on the API they are 405.

The {component} and {environment} segments must match ^[a-z0-9][a-z0-9._-]{0,127}$ (case-insensitive); anything else is rejected with 422.

The wire shape is Terraform's, not the platform's

These routes speak raw Terraform JSON, not the standard { "data": …, "meta": … } envelope: the GET body is the state document itself, and lock conflicts answer 423 with the holder's lock-info document so Terraform can print the lock id and owner directly. This is a documented exception to the error envelope — Terraform parses these bodies natively and knows nothing about platform envelopes.

Authentication

Terraform's HTTP backend sends Basic auth. The API edge normalizes it: the Basic password is promoted to the request's Bearer token, and the username is ignored (orun by convention). So the backend password is any credential the state plane accepts — in CI, the short-lived run token.

The orun runner does this wiring for you: it exports the TF_HTTP_* variables per job, including TF_HTTP_PASSWORD set to the job's run token, so a Terraform component needs no credential configuration at all.

Configure the backend

terraform {
backend "http" {
address = "https://api.orunbase.com/v1/organizations/org_1f6a3c9e/projects/prj_2d3e4f5a/state/tfstate/network/prod"
lock_address = "https://api.orunbase.com/v1/organizations/org_1f6a3c9e/projects/prj_2d3e4f5a/state/tfstate/network/prod"
unlock_address = "https://api.orunbase.com/v1/organizations/org_1f6a3c9e/projects/prj_2d3e4f5a/state/tfstate/network/prod"
lock_method = "LOCK"
unlock_method = "UNLOCK"
username = "orun"
# password arrives via TF_HTTP_PASSWORD — never commit it
}
}

Authorization

The routes carry the state plane's normal posture, deny-by-default:

OperationRequirement
GETstate.object.read
POST / DELETE / LOCK / UNLOCKstate.object.write, plus the CI repo allow-list (the repo's workspace link), plus an active project

Workflow actors minted via OIDC exchange hold these within their token-bound workspace + project — see CI workflow grants. CI cannot write Terraform state into an archived project.

warning

State documents contain resolved values — that is exactly why they never lived in the repo. Treat the state address like any other secret-adjacent surface: don't log response bodies, and don't widen read access beyond what state.object.read already grants.