Skip to main content

Terraform: infrastructure as code for VPS and cloud

Cloud & DevOps · 19.04.2026 · 5 min read
Illustration for “Terraform: infrastructure as code for VPS and cloud”

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 --version

Example: 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 destroy

Multiple 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
}
Security: Never commit terraform.tfvars and *.tfstate to the repository — they contain tokens and secrets. Add them to .gitignore.
Remote State: For team work, keep 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-42

Someone 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 -upgrade

The 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

CriterionTerraformAnsible
ModelDeclarative, with stateProcedural, stateless by default
Primary jobCreating infrastructure (servers, networks, DNS)Configuring the OS inside an already existing server
IdempotencyGuaranteed by comparing against stateDepends 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 apply and destroy are 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 list

If 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.

Was this article helpful?
← Back to Knowledge Base Ask Support