What Terraform is and why teams adopt it
Terraform describes infrastructure in declarative files, compares that description with what exists, and works out the changes needed to close the gap. The value is not the syntax. It is the workflow: a change is written, reviewed as a pull request, planned against real state and then applied, which leaves a record of who changed what and why.
Teams adopt Terraform for three practical reasons. It covers many providers with one workflow, so a cloud account, a DNS zone and a monitoring SaaS can live in the same review process. It has the largest ecosystem of providers and community modules of any tool in its class. And the plan step makes the blast radius of a change visible before anything happens.
Our Terraform infrastructure as code service covers the full engagement. This page explains how we think about running it in production, and when we would steer you elsewhere.
How we structure Terraform in production
State is the foundation
State records which real objects belong to which resources in your code. We store it remotely, versioned and encrypted, with locking, and we limit write access to the pipeline and a small admin group. The detail of that setup is in our guide to Terraform remote state on S3 and Azure Blob.
Equally important is how many state files you have. One state for everything makes plans slow and means an unrelated mistake can touch production networking. We split by change frequency and ownership: network, identity, data platform, each cluster, each application stack. Stacks share values through outputs or data sources rather than by sharing a file.
Modules with opinions, not wrappers
A module that merely re-exposes every argument of a resource adds indirection and no value. Good modules encode a decision: a private subnet layout, an encrypted bucket with logging and public access blocked, a cluster with your agreed node and identity pattern. We version them, pin consumers to a release and document inputs and outputs. See reusable Terraform modules for AWS and Azure for the approach.
Environments
We prefer small root configurations per environment that call the same modules with different inputs. Workspaces share a backend and a code path, which makes it easy to apply the wrong one, so we use them sparingly. Where directory duplication becomes tedious, a wrapper such as Terragrunt can help, but it is an addition with its own costs, not a default.
Security and access
Terraform has broad permissions by design, so the pipeline identity is a high-value target. We use short-lived federated credentials (OIDC from your CI system) rather than stored access keys, and separate plan roles with read access from apply roles with write access. Pull requests from forks never receive apply credentials.
State can contain secrets in plain text, so treat state storage like a production database. Prefer referencing secrets from a secrets manager at runtime over passing them as variables, and mark sensitive outputs. In CI we run static analysis such as Checkov, Trivy or tfsec, covered in Terraform security scanning with Checkov, plus policy as code for rules specific to your organisation.
Cost and capacity
Terraform itself is a build-time tool, so its cost is mostly people time and the infrastructure it creates. Two habits help. First, tag everything through provider-level default tags so spend can be attributed. Second, add a cost estimate step to pull requests so reviewers see the monthly effect of a change next to the plan. We avoid quoting savings, because they depend entirely on what you are running.
Plan time grows with state size, which is another reason to keep stacks small. Parallelism and provider rate limits can also slow large applies, particularly on accounts with many resources.
Day-2 operations
Drift and reconciliation
Someone will change something in the console during an incident. Scheduled plans against each stack detect that, and we treat a non-empty plan as something to reconcile deliberately. Our notes on drift detection and remediation cover the routine.
Upgrades
Provider and core upgrades are routine work, not events. We pin versions, read changelogs, upgrade in lower environments first and keep a lock file committed. The process is detailed in Terraform provider version upgrades.
A minimal pipeline step
jobs:
plan:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive
- run: terraform init -input=false
- run: terraform validate
- run: terraform plan -input=false -out=tfplan
The apply job runs only after merge, from the saved plan or a fresh plan that is compared with the reviewed one, under a different role.
Common failure modes
- One huge state file. Slow plans, wide blast radius and lock contention. Fix by splitting stacks and moving resources with state commands under review.
- Unpinned versions. A provider release changes behaviour on a Monday morning. Pin and upgrade deliberately.
- Secrets in code or state. Move to a secrets manager and rotate anything already exposed.
- Stale locks and half-applied changes. Rehearse recovery before you need it.
- Hand edits in the console. Remove write access for humans, keep break-glass access audited.
- Modules that nobody owns. Assign owners, version them and write a changelog.
When Terraform is the wrong choice
Be wary of Terraform if your estate is entirely AWS and nobody on the team wants to operate state storage, since AWS CloudFormation removes that burden. If your platform engineers would rather use loops, conditionals and unit tests in TypeScript or Python, Pulumi may feel more natural. If the job is installing packages and managing files on servers, use Ansible for that layer.
On licensing: Terraform moved to the Business Source License in 2023, and the OpenTofu fork continues under an open source licence. Neither is wrong for everyone. We help you read the terms against your actual use and pick deliberately.
If you are starting from a hand-built estate, our article on moving from ClickOps to Terraform on AWS describes the path, and you can book a consultation to scope it.
Official documentation: developer.hashicorp.com/terraform