Terraform lets you describe infrastructure — servers, DNS records, firewalls, buckets, Kubernetes resources — in configuration files, then creates and updates it through providers’ APIs. Instead of clicking through dashboards, you review a plan, apply it, and keep the files in Git. (OpenTofu is an open-source fork with the same language — see OpenTofu.)
Core concepts
- Provider — a plugin that talks to an API (Cloudflare, AWS, Kubernetes, GitHub, many hosting platforms).
- Resource — something Terraform manages (a DNS record, a bucket, a VM).
- Data source — read-only information from a provider.
- State — Terraform’s record of what it created, mapping configuration to real objects.
- Plan / Apply — preview changes, then make them.
Install
sudo apt install -y gnupg software-properties-common curl
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
terraform -version
A practical first project: DNS records
Managing DNS as code is a great first use — every VPS needs records, and changes become reviewable. Example with Cloudflare:
# main.tf
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
variable "cloudflare_api_token" {
type = string
sensitive = true
}
variable "zone_id" {
type = string
}
variable "web_ip" {
type = string
default = "203.0.113.10"
}
resource "cloudflare_record" "root" {
zone_id = var.zone_id
name = "@"
type = "A"
content = var.web_ip
proxied = false
}
resource "cloudflare_record" "www" {
zone_id = var.zone_id
name = "www"
type = "CNAME"
content = "example.com"
}
output "web_ip" {
value = var.web_ip
}
Provider schemas change between major versions (attribute names like content vs value) — check the provider’s documentation for the version you pin.
Provide variables in terraform.tfvars (keep secrets out of Git) or environment variables:
export TF_VAR_cloudflare_api_token="…"
export TF_VAR_zone_id="…"
The workflow
terraform init # download providers, set up state
terraform fmt # format files
terraform validate # check syntax
terraform plan # show what will change
terraform apply # make the changes (asks for confirmation)
terraform destroy # remove everything this config manages
Always read the plan. Lines with + create, ~ update in place, -/+ replace (destroy then create), - destroy. A surprising -/+ on a database server is your warning before data loss.
State
Terraform stores state in terraform.tfstate. It can contain secrets and must not be lost or edited by hand.
- Never commit state to Git.
- For teams, use a remote backend (S3-compatible storage with locking, Terraform Cloud, or a Postgres backend) so everyone shares one state and concurrent runs are locked. See Terraform state.
terraform {
backend "s3" {
bucket = "tf-state"
key = "dns/terraform.tfstate"
region = "us-east-1"
}
}
S3-compatible storage such as MinIO works with additional endpoint settings.
Variables, outputs and expressions
variable "sites" {
type = map(string)
default = {
blog = "203.0.113.10"
shop = "203.0.113.11"
}
}
resource "cloudflare_record" "sites" {
for_each = var.sites
zone_id = var.zone_id
name = each.key
type = "A"
content = each.value
}
for_each creates one resource per map entry — add a site by adding a line.
Modules
Group reusable configuration into modules (a directory with its own variables and outputs), then call it several times with different inputs. Public modules are on the Terraform Registry. See Terraform modules.
Importing existing resources
Already created records by hand? Bring them under management instead of recreating them, with an import block (Terraform 1.5+) or terraform import. Then plan should show no changes.
Terraform and configuration management
Terraform creates and wires infrastructure; tools like Ansible configure what runs inside servers. Many teams use both: Terraform for DNS, firewalls and cloud resources; Ansible for packages, users and services. See infrastructure as code.
Tips
- Pin provider versions.
- Run
planin CI on every pull request andapplyonly from the main branch. - Use
prevent_destroyin alifecycleblock for critical resources. - Keep state remote and backed up.