Terraform: infrastructure as code
Terraform (HashiCorp) lets you describe server infrastructure as .tf configuration files and manage it from the CLI. Instead of creating servers by hand in a web console, the entire stack lives in a Git repository.
Installing Terraform
wget https://releases.hashicorp.com/terraform/1.7.0/terraform_1.7.0_linux_amd64.zip
unzip terraform_1.7.0_linux_amd64.zip
mv terraform /usr/local/bin/
terraform --versionExample: Hetzner Cloud
# main.tf
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.44"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
# SSH key
resource "hcloud_ssh_key" "default" {
name = "my-key"
public_key = file("~/.ssh/id_rsa.pub")
}
# Server
resource "hcloud_server" "web" {
name = "web-01"
image = "ubuntu-24.04"
server_type = "cx22"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.default.id]
user_data = <<-EOF
#!/bin/bash
apt update && apt install -y nginx
systemctl enable nginx
EOF
}
# Firewall
resource "hcloud_firewall" "web_fw" {
name = "web-firewall"
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
}
resource "hcloud_firewall_attachment" "web" {
firewall_id = hcloud_firewall.web_fw.id
server_ids = [hcloud_server.web.id]
}
output "server_ip" {
value = hcloud_server.web.ipv4_address
}# variables.tf
variable "hcloud_token" {
sensitive = true
}# terraform.tfvars (in .gitignore!)
hcloud_token = "your-hetzner-api-token"Core commands
# Initialization (download providers)
terraform init
# Review the change plan
terraform plan
# Apply the changes
terraform apply
# Inspect the state
terraform show
terraform state list
# Destroy the infrastructure
terraform destroyMultiple servers with count
resource "hcloud_server" "app" {
count = 3
name = "app-${count.index + 1}"
image = "ubuntu-24.04"
server_type = "cx22"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.default.id]
}
output "app_ips" {
value = hcloud_server.app[*].ipv4_address
}terraform.tfvars and *.tfstate to the repository — they contain tokens and secrets. Add them to .gitignore.terraform.tfstate in Terraform Cloud, S3 or Hetzner Object Storage — that lets several people manage the same infrastructure.Real Terraform Errors Explained
Two errors are almost guaranteed to show up when a team works with Terraform, and neither is about HCL syntax — both are about state.
Error Acquiring the State Lock
Error: Error acquiring the state lock
Lock Info:
ID: 7a3f1c2e-...
Path: terraform.tfstate
Operation: OperationTypeApply
Who: ci-runner@build-42Someone is already running apply, or a previous process crashed without releasing the lock. Before unlocking, make sure there really is no parallel run in progress — otherwise you risk corrupting the state. Only then: terraform force-unlock 7a3f1c2e-....
Inconsistent Dependency Lock File
Error: Inconsistent dependency lock file
The following dependency selections recorded in the lock file are inconsistent with the current configuration.
To update the locked dependency selections to match a change in the configuration, run: terraform init -upgradeThe provider version in .terraform.lock.hcl does not match the one specified in the required_providers block — usually after manually bumping the version in code without regenerating the lock file. The fix is right there in the error text: terraform init -upgrade, then commit the updated lock file.
Terraform vs. Ansible
| Criterion | Terraform | Ansible |
|---|---|---|
| Model | Declarative, with state | Procedural, stateless by default |
| Primary job | Creating infrastructure (servers, networks, DNS) | Configuring the OS inside an already existing server |
| Idempotency | Guaranteed by comparing against state | Depends on the quality of the written modules and tasks |
When Terraform Is Not a Good Fit
- A one-off change to a single server — the full plan/apply cycle is overkill; calling the provider's API or CLI directly is faster and clearer.
- Configuring software inside the server — installing packages, config templates, systemd units. That calls for Ansible; Terraform only creates the hardware and kicks off cloud-init.
- You need complex imperative logic with loops and conditional error handling — declarative HCL is poorly suited for that.
- A single server, a two-person team, infrastructure that almost never changes — the overhead of remote state and HCL does not pay off.
- You need an instant infrastructure rollback without recalculating a plan —
terraform applyanddestroyare not transactional and take time to execute for each resource.
Verifying the Result
# The plan should be empty after a successful apply — a sign of idempotency
terraform plan -detailed-exitcode
# exit code 0 = no changes, 2 = there are differences, 1 = error
# Syntax and formatting are correct
terraform validate
terraform fmt -check -recursive
# Cross-check the state against reality at the provider
terraform state list
hcloud server listIf terraform plan -detailed-exitcode returns 2 right after an apply, there is drift somewhere: either someone changed a resource by hand through the web console, or the provider itself changed an attribute (for example, assigned a different IP). Run terraform plan on a schedule regularly so that kind of drift does not build up unnoticed.