Terraform describes desired infrastructure and compares that configuration with known resources and state to propose changes. The operational result depends on configuration, selected versions, provider APIs, and actual state. Keeping infrastructure files as code makes intent and changes reviewable; it does not turn execution into a global transaction.

This note describes a decision and recovery flow. It does not require creating resources: before operating an account, define the environment, permissions, and permitted blast radius.

Configuration, plan, and apply

Configuration declares resources, references, and dependencies. Terraform builds a graph and may execute independent operations in parallel. A reference helps order dependencies; correct ordering does not guarantee that several APIs change atomically or that a service is ready for traffic as soon as creation finishes.

Plan calculates expected actions using available information. It normally refreshes its resource view and compares it with configuration. It is a snapshot conditioned by that moment, permissions, unknown values, and provider behavior. An external change between plan and apply can affect the outcome; state checks may reject a saved plan that became stale, but do not freeze remote APIs.

Apply executes the plan. A saved plan allows review of the intended set without silently recalculating another; protect it as a sensitive artifact and bind it to the reviewed commit, environment, and versions. It may contain secrets even when output marks them sensitive. Review should identify replacements, deletions, and network, identity, or data changes affecting security and recovery.

---
config:
  look: handDrawn
---
flowchart TD
    code["Reviewed desired configuration"] --> plan["Plan with pinned versions"]
    cloud["Observed provider resources"] --> plan
    state[("Remote state<br/>Protected and versioned")] --> plan
    plan --> review["Review changes and blast radius"]
    review --> apply["Apply approved artifact"]
    identity["Short-lived deployment identity"] --> apply
    lock["Backend lock"] -->|"Coordinate state writers"| apply
    apply -->|"Resource API operations"| cloud
    apply -->|"Record results"| state
    cloud --> reconcile["Reconcile partial failure or drift"]
    reconcile --> plan

State as an operational relationship

State connects Terraform addresses to remote objects and retains attributes needed to manage their lifecycle. It is not a complete inventory of the account. Deleting it does not automatically remove resources; it may make Terraform lose its relationship with objects that still exist.

State needs access control, encryption, and recovery. It may contain secrets and operational data even when the repository holds only references. A remote backend shares it among authorized executions. Separating states by environment or domain reduces execution scope when dependencies between them are explicit. Many files without a clear boundary can also complicate coordinated changes.

S3 object versioning helps recover file versions. Restoring an older version does not undo infrastructure: it may describe resources that have already changed. Recovery first compares state, real resources, and intent before deciding how to reconcile them. Versioning must also not be confused with a tested restore.

S3 locking

The S3 backend supports native locking with use_lockfile = true from Terraform 1.10.0; it must be enabled, and compatibility depends on the version used by each worker. DynamoDB-based locking is deprecated in current documentation. A migration must account for all executors and preserve coordination during the transition, rather than merely edit one line.

The following fragment belongs to the root module for one environment or domain boundary. The REPLACE_* values are placeholders. The bucket, its encryption, versioning, and permissions must exist through a prior bootstrap before using the backend; this block creates none of them and contains no credentials. The minimum version enables native locking, while project policy should pin permitted versions.

terraform {
  required_version = ">= 1.10.0"
 
  backend "s3" {
    bucket       = "REPLACE_STATE_BUCKET"
    key          = "REPLACE_ENV_OR_DOMAIN/terraform.tfstate"
    region       = "REPLACE_AWS_REGION"
    encrypt      = true
    use_lockfile = true
  }
}

Permissions should distinguish the state object from the .tflock file. State needs object reads and writes; the lockfile needs reads, writes, and deletion to manage the lock. For default state, do not grant object deletion merely because the lockfile requires it; deleting other workspace states has different requirements. Listing operations and their scope must also match the backend and path in use.

The lock coordinates writers that use the same backend and key and respect that mechanism. It does not protect other states, manual account changes, or executions that bypass locking. If an execution fails, releasing its lock requires checking that the owner is no longer operating. Forcing it while work continues may permit concurrent writers and damage coordination.

Identity and versions

A runner can obtain temporary credentials through OIDC, with a trust policy validating the issuer, audience, and actual subject of authorized tokens. GitHub.com uses immutable IDs in subjects for repositories created, renamed, or transferred after July 15, 2026, or opted in; others may retain the older format. The subject may also be customized; a name-based pattern must not be copied as a universal format. If the job uses an environment, its subject and restrictions must match that context and its protection rules. These claims belong to GitHub Actions and must not be extrapolated to Forgejo.

The identity should change only the intended scope and access the required backend. Separate plan and apply permissions may reduce exposure, although the provider may need broad reads to calculate changes. The pipeline binds identity and artifact to the reviewed environment.

Pin compatible Terraform and provider versions, review the dependency lockfile, and control module source and version. A provider lockfile does not itself freeze every module or the external behavior of an API.

Failures and verification

If apply creates a resource and fails later, some changes already exist. Rerunning without observation may worsen the incident. Preserve diagnostics, check state, and inspect real resources before a new plan. Importing or moving addresses requires understanding the relationship being repaired; blindly editing state may lose it.

Useful verification uses an isolated environment or suitable fixtures to check plans, permissions, and recovery without affecting production. Cover two executions competing for one state, an interrupted runner, drift, and failure after a successful operation. Signals include planned and actual changes, duration, locks, provider errors, and resources awaiting reconciliation.

The expected result is a reviewable change with explicit scope and a recovery path when only part of the graph finishes. Declared infrastructure still requires observation and operation after apply.

References