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

Related guides: What is DNS, settings · Domain names & WHOIS lookup · Hosting types guide · Nginx configuration · Plesk panel guide

# 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
Warning
terraform.tfstate can contain secrets as plain text. Never commit it to Git, and keep your backend encrypted.

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

Modern Web Hosting and Server Infrastructure

A performant web hosting service rests on three infrastructure decisions: NVMe SSD disks (4-6× IOPS over SATA SSD), LiteSpeed Web Server or Nginx + LSCache (9× request capacity over Apache) and CloudLinux + Imunify360 isolation. The hosting provider's control panel (cPanel, Plesk, DirectAdmin), daily backup policy, data center location and support response time make a big difference too. Turkish locations give low latency to local visitors, while Hetzner Frankfurt or OVH Roubaix suit global traffic. As your site grows, transitioning from shared hosting to VPS to dedicated server scales CPU/RAM/disk to your needs.

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