Terraform lets you define your cloud infrastructure (servers, networks, databases, DNS, firewalls) as code, with versioning, review and reproducibility. Clicking through a cloud console in production often ends in mistakes; with Terraform you can recreate the same cluster in ten different regions in seconds.

Basic Structure

# main.tf
terraform {
  required_version = ">= 1.6"
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.45"
    }
  }
  backend "s3" {
    bucket = "my-tf-state"
    key    = "prod/terraform.tfstate"
    region = "eu-central-1"
  }
}

provider "hcloud" {
  token = var.hcloud_token
}

Essential Commands

terraform init       # download providers, set up backend
terraform fmt -recursive
terraform validate
terraform plan       # WHAT will change? — read this before applying
terraform apply      # apply (re-shows plan, asks for confirmation)
terraform destroy    # delete everything (be careful!)

terraform state list
terraform state show hcloud_server.web
terraform output

Variables

# variables.tf
variable "hcloud_token" {
  type      = string
  sensitive = true
}

variable "server_count" {
  type    = number
  default = 2
  validation {
    condition     = var.server_count > 0 && var.server_count <= 10
    error_message = "Must be between 1 and 10."
  }
}

variable "region" {
  type    = string
  default = "nbg1"
}

# terraform.tfvars
hcloud_token = "your-token"
server_count = 3

Hetzner Example: Web + DB

resource "hcloud_ssh_key" "admin" {
  name       = "admin"
  public_key = file("~/.ssh/id_ed25519.pub")
}

resource "hcloud_network" "internal" {
  name     = "internal"
  ip_range = "10.0.0.0/16"
}

resource "hcloud_network_subnet" "main" {
  network_id   = hcloud_network.internal.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = "10.0.1.0/24"
}

resource "hcloud_server" "web" {
  count       = var.server_count
  name        = "web-${count.index + 1}"
  image       = "ubuntu-24.04"
  server_type = "cx22"
  location    = var.region
  ssh_keys    = [hcloud_ssh_key.admin.id]

  network {
    network_id = hcloud_network.internal.id
    ip         = "10.0.1.${10 + count.index}"
  }

  user_data = file("cloud-init.yaml")
}

resource "hcloud_load_balancer" "web_lb" {
  name               = "web-lb"
  load_balancer_type = "lb11"
  location           = var.region
}

Modules

Factor repeated infrastructure into modules — write once, reuse across environments.

# modules/webapp/main.tf
variable "name" {}
variable "count" { default = 1 }

resource "hcloud_server" "this" {
  count       = var.count
  name        = "${var.name}-${count.index + 1}"
  image       = "ubuntu-24.04"
  server_type = "cx22"
}

output "ips" {
  value = hcloud_server.this[*].ipv4_address
}

# main.tf
module "frontend" {
  source = "./modules/webapp"
  name   = "frontend"
  count  = 3
}

module "backend" {
  source = "./modules/webapp"
  name   = "backend"
  count  = 2
}

State Management

Terraform stores the current state of your infrastructure in terraform.tfstate. On a team, remote state is mandatory — otherwise two concurrent applies will corrupt the state.

# S3 + DynamoDB lock
terraform {
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-central-1"
    dynamodb_table = "tf-state-locks"  # state lock
    encrypt        = true
  }
}

# Alternatives: Terraform Cloud (HashiCorp), GCS, Azure Blob

Workspaces

terraform workspace new staging
terraform workspace new production
terraform workspace select production
terraform workspace list

# Workspace-specific tfvars
terraform apply -var-file="prod.tfvars"

Drift Detection

# Did someone change infra manually?
terraform plan -detailed-exitcode
# exit 0: no changes
# exit 2: the plan includes changes
# Run this regularly in CI → alert

Importing Existing Resources

# Resource created by hand in the UI, bring it under Terraform:
terraform import hcloud_server.web 12345678

# Terraform 1.5+ — import blocks
import {
  to = hcloud_server.web
  id = "12345678"
}

Terraform + CI/CD

# .github/workflows/terraform.yml
name: Terraform
on:
  pull_request:
    paths: ['terraform/**']
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
        working-directory: terraform
      - run: terraform fmt -check -recursive
      - run: terraform validate
      - run: terraform plan -out=plan.bin
        env:
          TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}

Conclusion

Terraform is the written contract of modern cloud infrastructure. Infrastructure as code, PR review, versioning, reproducibility — all way beyond a single YAML file. It feels slow the first time; but cloning your infrastructure in five minutes on the second project becomes addictive.

Terraform IaC setup

Reach out to KEYDAL for Terraform infrastructure design and migration on AWS, Hetzner and Azure. Contact us

WhatsApp