Logo

Terraform: Infrastructure as Code

Terraform: Infrastructure as Code

Provision your infrastructure declaratively with Terraform. Multi-cloud (AWS, Azure, GCP, OVH, Hetzner, Scaleway), remote state, reusable modules. The IaC reference since 2014.

Terraform: Infrastructure as Code

Provision your infrastructure declaratively with Terraform. Multi-cloud (AWS, Azure, GCP, OVH, Hetzner, Scaleway), remote state, reusable modules. The IaC reference since 2014.

Introduction

Terraform lets you:

  • Describe infra in HCL (HashiCorp Configuration Language)
  • Provision servers, network, DNS, certificates on all clouds
  • Manage state (who owns what)
  • Plan changes before applying
  • Version infra in Git

Use cases: create 100 identical VPS, K8s cluster on AWS, manage Cloudflare DNS as code.

Note: since 2023, Terraform is under BSL license (commercial). Community fork OpenTofu keeps the open-source MPL license. This tutorial uses terraform but everything works the same with tofu.

Prerequisites

  • Linux VPS or workstation Linux / macOS / Windows
  • Cloud provider account (AWS, Hetzner, OVH...)
  • Root access (to install)

Step 1: Installation

Debian / Ubuntu

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.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 -y terraform
terraform version

Or OpenTofu

curl -fsSL https://get.opentofu.org/install-opentofu.sh | sudo sh -s -- --install-method deb
tofu version

Step 2: First project (Hetzner Cloud)

mkdir ~/infra-hetzner && cd ~/infra-hetzner

main.tf:

terraform {
  required_providers {
    hcloud = {
      source = "hetznercloud/hcloud"
      version = "~> 1.45"
    }
  }
}

provider "hcloud" {
  token = var.hcloud_token
}

variable "hcloud_token" {
  type      = string
  sensitive = true
}

resource "hcloud_server" "web1" {
  name        = "web1"
  image       = "debian-12"
  server_type = "cx22"
  location    = "fsn1"
  ssh_keys    = [hcloud_ssh_key.default.id]
}

resource "hcloud_ssh_key" "default" {
  name       = "my-key"
  public_key = file("~/.ssh/id_ed25519.pub")
}

output "server_ip" {
  value = hcloud_server.web1.ipv4_address
}

Step 3: Initialize

terraform init

Step 4: Plan

export TF_VAR_hcloud_token="your-hetzner-token"
terraform plan

Always read the plan carefully before applying.

Step 5: Apply

terraform apply

Type yes. Terraform creates the Hetzner server.

Outputs:
server_ip = "5.78.123.45"
ssh [email protected]

Step 6: State

State is in terraform.tfstate. Contains current infra state.

⚠️ Never commit it to Git:

echo "*.tfstate*" >> .gitignore
echo ".terraform/" >> .gitignore

For team work, use a remote backend.

Step 7: Remote backend (S3)

backend.tf:

terraform {
  backend "s3" {
    bucket = "myteam-terraform-state"
    key    = "infra-hetzner/terraform.tfstate"
    region = "eu-west-3"
    encrypt = true
  }
}
terraform init -migrate-state

State now on S3, shared across team. Use DynamoDB for locking (avoid concurrent applies).

Alternatives: Terraform Cloud (managed), Scalr, Spacelift, custom HTTP backend.

Step 8: Variables and environments

variables.tf:

variable "environment" {
  type    = string
  default = "dev"
}

variable "server_type" {
  type = string
  default = "cx22"
}

locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

Usage:

resource "hcloud_server" "web" {
  name        = "${var.environment}-web"
  server_type = var.server_type
  labels      = local.common_tags
}

Variables via .tfvars file:

prod.tfvars:

environment = "prod"
server_type = "ccx33"
terraform apply -var-file=prod.tfvars

Step 9: Modules

modules/
  webapp/
    main.tf
    variables.tf
    outputs.tf

modules/webapp/main.tf:

resource "hcloud_server" "web" {
  name        = var.name
  image       = "debian-12"
  server_type = var.server_type
  location    = var.location
  ssh_keys    = var.ssh_keys
}

Usage:

module "web1" {
  source      = "./modules/webapp"
  name        = "web1"
  server_type = "cx22"
  location    = "fsn1"
  ssh_keys    = [hcloud_ssh_key.default.id]
}

module "web2" {
  source      = "./modules/webapp"
  name        = "web2"
  server_type = "cx32"
  location    = "nbg1"
  ssh_keys    = [hcloud_ssh_key.default.id]
}

Community modules: https://registry.terraform.io

Step 10: Provisioning (cloud-init)

resource "hcloud_server" "web" {
  name        = "web"
  image       = "debian-12"
  server_type = "cx22"
  location    = "fsn1"
  
  user_data = <<-EOT
    #cloud-config
    package_update: true
    packages:
      - nginx
      - certbot
    runcmd:
      - systemctl enable nginx
      - echo "Hello from terraform" > /var/www/html/index.html
  EOT
}

For complex configs, delegate to Ansible after Terraform provisioning.

Step 11: Destroy

terraform destroy

⚠️ Destroys all infra managed by this state.

Destroy a single resource:

terraform destroy -target=hcloud_server.web1

Step 12: Pro workflow

git pull
terraform fmt -recursive
terraform validate
terraform plan -out=tfplan
terraform show tfplan
terraform apply tfplan
git add . && git commit -m "Add web1" && git push

In CI, use Atlantis or Terraform Cloud to automate plan + apply via PR.

Troubleshooting

"Error: Provider not found"

terraform init -upgrade

State lock

terraform force-unlock LOCK_ID

Check first that no one is applying.

"resource already exists"

You created the resource outside terraform. Import:

terraform import hcloud_server.web1 12345

Drift (resource modified manually)

terraform plan

Shows drift. apply realigns. Or adjust code to match reality.

"Provider produced inconsistent final plan"

Provider bug. Update:

terraform init -upgrade

Useful commands

terraform init
terraform plan
terraform apply
terraform destroy
terraform fmt -recursive
terraform validate
terraform show

terraform state list
terraform state show <resource>
terraform state rm <resource>
terraform state mv <from> <to>

terraform import <resource> <id>

terraform output
terraform output -json

terraform workspace list
terraform workspace new staging
terraform workspace select staging

Conclusion

Terraform gives you:

  • Declarative and versioned infrastructure
  • Multi-cloud
  • State management for team work
  • Reusable modules

Going further:

  • Use Atlantis or Terraform Cloud for CI/CD workflow
  • Combine with Ansible for post-provisioning config
  • For Kubernetes, check Pulumi or Crossplane (k8s-native)
  • To stay open-source, migrate to OpenTofu

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members