You create a cloud resource in the console, note it in a document, and nobody updates the document. Six months later, staging diverges from the repository and nobody knows what changed or when. Terraform declares the desired infrastructure in HCL, keeps the real state in a remote backend, and reconciles the difference on every terraform apply. HashiCorp's documentation (2025) treats the state file as the source of truth for what exists.
Remote state and locking
The state file holds IDs, addresses, and sensitive values; it does not go into Git. An S3 backend with DynamoDB locking, or GCS with native locking, serializes apply: two engineers at the same time fight over the lock, not the environment. Environments split state by prefix and by cloud account: dev, staging, and production share no backend. A production database resource gets prevent_destroy; destroy becomes a change that requires removing the guard in a separate PR.
Modules with an owner and a version
A module wraps 10 to 30 resources behind a short input/output interface. Kief Morris, in Infrastructure as Code (O'Reilly, 2nd ed., 2021), recommends one module per capability: network, database, queue service. A module with 200 inputs turns into a review bottleneck and a version nobody dares to bump. An internal registry publishes the module with a semver tag; the consuming team pins the version and upgrades via PR, with infrastructure diffs going through the same review as code.
Pipeline: plan on the PR, apply on merge
The pull request runs terraform plan and posts the diff as a PR comment. Merging to main runs apply. Without that step, the plan lives on the machine of whoever applied it, and the rest of the team reviews on faith. A scheduled job runs plan and fails when it finds drift, which exposes changes made in the console or by another pipeline. Using -target outside emergencies hides pending changes in state; teams avoid it.
Import and legacy migration
A pre-existing resource enters state through terraform import. A moved block renames a resource address without destroy-and-recreate. Migration runs in slices: critical network first, database after, application layer last. Every migration apply becomes a PR with a reviewable diff. Drift that shows up during import reveals the size of the debt before any architecture change.
Enjoyed this content?
I build web products and AI solutions the right way — solid architecture, maintainable code, and real delivery.
Let's talk