MET : Terraform, infrastructure as code (2/6)

| Digital

Creating servers manually is a thing of the past. With Terraform, your infrastructure becomes code: versioned, reproducible, and deployable in seconds.

In the first article of this series, we presented our vision of modern cloud infrastructure. Today, we dive into the first tool in our stack: Terraform.

The problem: artisanal infrastructure

Imagine the classic scenario:

  1. You log into your hosting provider’s console
  2. You click “Create a server”
  3. You choose options (RAM, CPU, OS…)
  4. You note the IP somewhere (or not)
  5. You repeat for each server

Six months later:

  • “What was the config of that server again?”
  • “Who created this private network?”
  • “Can we recreate the same infra for client X?”

The answer is often: “Uh… we’ll have to dig around”.

The solution: Infrastructure as Code

Terraform is an open-source tool created by HashiCorp that allows you to define your infrastructure in configuration files. Instead of clicking through an interface, you describe what you want, and Terraform takes care of creating it.

“Describe the desired state, Terraform takes care of getting there.”

The advantages are numerous:

  • Versioning: Your infrastructure is in Git, with complete history
  • Reproducibility: Recreate the same infra with one command
  • Living documentation: The code IS the documentation
  • Code review: Infrastructure changes go through merge requests
  • Rollback: Return to a previous version if needed

Our infrastructure in Terraform

Here’s how we define our servers. Everything starts with the provider configuration (the hosting provider):

# Terraform configuration
terraform {
  required_version = ">= 1.6"

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

# Connection to the hosting provider's API
provider "hcloud" {
  token = var.hcloud_token  # Token stored in environment variable
}

This block tells Terraform:

  • Which version of Terraform to use
  • Which provider (here Hetzner Cloud) with which version
  • How to authenticate (via a secure API token)

Server definition

Next, we declare our servers. Here’s the definition of our primary server:

# Primary server - Kubernetes Control Plane
resource "hcloud_server" "control_plane" {
  name        = "k8s-master"
  server_type = "cpxxx"        # Our model
  image       = "ubuntu-22.04"
  location    = "nbg1"         # Nuremberg datacenter (Germany)
  ssh_keys    = data.hcloud_ssh_keys.all.ssh_keys[*].id

  labels = {
    role        = "k8s-control-plane"
    environment = "production"
    managed_by  = "terraform"
  }

  lifecycle {
    prevent_destroy = true  # Protection against accidental deletion
  }
}

Let’s break down this code:

  • resource: Declares a resource to create
  • hcloud_server: Resource type (Hetzner server)
  • server_type: Server size
  • location: The datacenter (Nuremberg, Germany = GDPR ✓)
  • labels: Metadata for organization and filtering
  • prevent_destroy: Prevents accidental deletion

The private network

For our servers to communicate securely, we create a private network:

# Private network for inter-node communication
resource "hcloud_network" "k8s_network" {
  name     = "k8s-private-network"
  ip_range = "10.0.0.0/16"

  labels = {
    environment = "production"
  }
}

# Subnet for servers
resource "hcloud_network_subnet" "k8s_subnet" {
  network_id   = hcloud_network.k8s_network.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = "10.0.1.0/24"
}

# Attach server to private network
resource "hcloud_server_network" "control_plane_network" {
  server_id  = hcloud_server.control_plane.id
  network_id = hcloud_network.k8s_network.id
  ip         = "10.0.1.10"
}

This private network enables:

  • Secure communication between Kubernetes nodes
  • Internal traffic isolation (not exposed to the Internet)
  • Optimal performance (minimal latency)

Variables: flexibility and security

Sensitive information (API tokens, passwords) is never hardcoded. We use variables:

# variables.tf
variable "hcloud_token" {
  description = "Hosting provider API token"
  type        = string
  sensitive   = true  # Masked in logs
}

variable "ssh_keys" {
  description = "Authorized SSH keys"
  type        = list(string)
  default     = []
}

These variables are then defined via:

  • Environment variables (TF_VAR_hcloud_token)
  • .tfvars file (not committed to Git)
  • Secrets manager (Vault, etc.)

The Terraform workflow in practice

Using Terraform daily comes down to three commands:

1. Initialization

# Download providers and initialize the project
terraform init

2. Planning

# Show what will be created/modified/deleted
terraform plan

This command is crucial: it shows exactly what Terraform will do, without executing anything. Example output:

Terraform will perform the following actions:

  # hcloud_server.control_plane will be created
  + resource "hcloud_server" "control_plane" {
      + name        = "k8s-master"
      + server_type = "cpx52"
      + location    = "nbg1"
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.

3. Application

# Apply changes (with confirmation)
terraform apply

Terraform then creates the resources and stores their state in a terraform.tfstate file.

Our best practices

🔐 Security

  • Never secrets in code: Use variables and secrets managers
  • Remote state: Store tfstate in an encrypted S3 bucket, not locally
  • Code review: All infrastructure changes go through a merge request

🛡️ Protection

  • prevent_destroy: On critical resources (servers, databases)
  • ignore_changes: For manually managed attributes

📁 Organization

  • main.tf: Main resources
  • variables.tf: Variable declarations
  • outputs.tf: Exported values (IPs, IDs…)
  • terraform.tfvars: Variable values (not committed)

Integration into our workflow

To simplify usage, we created Make commands:

# Plan changes
make terraform-plan

# Apply changes
make terraform-apply

# Import existing servers
make terraform-import-servers ODIN_ID=12345 ATHENA_ID=67890

This allows the entire team to use Terraform without knowing the exact commands.

The result

With Terraform, our infrastructure is now:

📝 Documented The code describes exactly what exists
🔄 Reproducible Complete recreation in 10 minutes
📊 Versioned Complete history of changes
👥 Collaborative Code review before each modification
🛡️ Secure Externalized secrets, anti-deletion protection

What’s next?

Terraform creates the servers, but they’re “empty”. In the next article, we’ll see how Ansible takes over to:

  • Configure the operating system
  • Install Kubernetes (K3s)
  • Deploy base services

All in an automated and idempotent manner.

🚀 Need help adopting Infrastructure as Code?

We help companies implement Terraform, whether migrating existing infrastructure or starting from scratch.

Let’s talk about your project →