MET : Terraform, infrastructure as code (2/6)
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:
- You log into your hosting provider’s console
- You click “Create a server”
- You choose options (RAM, CPU, OS…)
- You note the IP somewhere (or not)
- 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) .tfvarsfile (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.