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 plan in CI on every pull request and apply only from the main branch.
  • Use prevent_destroy in a lifecycle block for critical resources.
  • Keep state remote and backed up.
  • Terraform
  • Infrastructure as Code
  • DevOps