Terraform is one of the most widely used tools for managing infrastructure as code. It works with almost every cloud provider and with many external services, from DNS to monitoring, using a declarative language called HCL. After the license change decided by HashiCorp in 2023, OpenTofu was also born, a compatible open source fork; the concepts described here apply to both.
Providers and resources
A provider is the plugin that knows how to talk to a given service: a cloud's provider, Cloudflare's, GitHub's. A resource is a single managed object, such as a virtual machine, a bucket or a DNS record.
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
resource "cloudflare_record" "www" {
zone_id = var.zone_id
name = "www"
type = "CNAME"
content = "example.pages.dev"
proxied = true
}
The first block pins the provider and its version; the second describes a record. There is no "create" command: you declare that the record must exist with those characteristics. Note that the exact attribute names can change between major versions of a provider, one more reason to pin its version.
The working cycle
The daily flow relies on a few commands:
terraform initdownloads the providers and prepares the working folder.terraform plancompares the code with reality and shows what would be created, changed or destroyed.terraform applycarries out the changes, after showing the plan again and asking for confirmation.terraform destroydeletes all the resources managed by that configuration.
The most important step is the second. The plan uses clear symbols: + for creations, ~ for changes, - for destructions and -/+ for resources that will be destroyed and recreated. This last case always deserves attention: some attributes cannot be changed in place, and modifying them means deleting the resource. On a database, that means losing the data.
State: Terraform's memory
Terraform needs to know which real resources correspond to which code blocks. This mapping is saved in the state file, usually terraform.tfstate. Without state, Terraform doesn't know what it created and would try to recreate everything.
State has three characteristics worth knowing well:
- it contains sensitive data: generated passwords, keys, internal addresses, often in plain text;
- it is shared: if two people work on the same infrastructure they must use the same state;
- it must be protected from concurrent writes: two simultaneous
applyruns can corrupt it.
For these reasons state should not be left on someone's computer nor stored in the Git repository. You use a remote backend, such as an object storage bucket with versioning and encryption enabled or a dedicated service, with a locking mechanism that prevents simultaneous runs.
Variables, outputs and environments
Variables make the code reusable: machine size, region, environment name. Outputs expose useful values after applying, such as a server's IP address.
To manage several environments, the most readable solution is usually to keep separate configurations for staging and production, each with its own state, calling the same modules with different values. This way a mistake in staging cannot touch production, and whoever reads the code immediately understands what they are working on.
Modules
A module is a folder of reusable Terraform code, with input variables and outputs. If every application needs a machine, a DNS record and a firewall rule, you write a module that creates them together and call it with different parameters.
Modules should be designed with balance. A module that is too generic, with dozens of options, becomes as hard as the code it was meant to simplify. A good module represents a clear concept in your domain, such as "a standard web service", and hides repetitive details.
Common mistakes
- Changing resources managed by Terraform by hand: on the next plan Terraform will try to bring them back to the declared state, undoing the change.
- Not pinning versions of providers and modules: an automatic update can change behavior without warning.
- Applying without reading the plan, especially when destructions appear.
- Putting secrets in the code: they should be passed through variables from a secure store, remembering that they will end up in the state anyway.
- One huge configuration for everything: slow plans, broad risk and conflicts between people. It is better to split by area or by service.
Protecting critical resources
For resources that must never disappear, such as production databases or buckets holding data, Terraform offers the prevent_destroy setting in the lifecycle block: any plan that would destroy them is rejected with an error. Together with the deletion protection offered by many providers, it is a cheap safety belt against the most expensive mistakes.
In short
- Terraform describes resources from many providers declaratively; OpenTofu is a compatible open source alternative.
- Always read the plan before applying it, paying particular attention to resources destroyed and recreated.
- State contains sensitive data: keep it in an encrypted, versioned remote backend with locking.
- Separate environments into distinct configurations and use modules that represent clear concepts.
- Pin versions, don't change managed resources by hand and protect critical ones from destruction.