Terraform per infrastruttura Hetzner e Digital Ocean: IaC pratico per sviluppatori PHP
Il 18 gennaio 2024 ho vissuto uno degli incidenti operativi più imbarazzanti della mia carriera. Stavo lavorando alla configurazione firewall di un VPS Hetzner di un cliente - un'azienda del settore e-commerce moda - durante un intervento serale pianificato a basso traffico. Volevo aggiungere una regola specifica per permettere accesso SSH da un nuovo IP di un sistemista del cliente; un'operazione banale di routine che avevo fatto centinaia di volte. Ma quella sera ero stanco, ho fatto clic su un menu sbagliato nel pannello Hetzner Cloud, e ho finito per applicare una regola firewall template di default che ha bloccato tutto il traffico in entrata verso il VPS - incluso il mio SSH attivo e l'intero traffico HTTP/HTTPS dell'e-commerce. Nei dieci minuti successivi, mentre cercavo disperatamente di capire cosa avessi fatto, il sito e-commerce del cliente era completamente offline a metà serata di un venerdì, con il loro canale Twitter pieno di clienti frustrati. Ho dovuto accedere tramite console VNC di emergenza, ricostruire la configurazione firewall da zero ricordando a memoria i parametri corretti, e riportare il sito online dopo 47 minuti di downtime totale. Il cliente è stato comprensivo, ma l'episodio mi ha costretto a una riflessione dolorosa: qualsiasi configurazione infrastrutturale gestita tramite click umani in un pannello web è esposta a errori umani che possono essere catastrofici.
Da quella serata di gennaio 2024, ho progressivamente migrato la gestione infrastrutturale di tutti i miei clienti verso un modello Infrastructure as Code (IaC) basato su Terraform. Oggi - aprile 2026 - gestisco l'infrastruttura di sei clienti PHP distribuiti fra Hetzner Cloud, Digital Ocean e AWS esclusivamente tramite Terraform, con configurazione versionata su Git e applicata tramite pipeline CI/CD automatizzate. Nessun cambiamento infrastrutturale avviene tramite click nel pannello del provider - ogni modifica passa da un pull request, viene revisionata da almeno un secondo paio d'occhi, viene applicata in modo atomico e reversibile. Il numero di incidenti operativi causati da errori umani nella configurazione infrastrutturale è sceso a zero nei 27 mesi successivi. Il tempo medio di replica di un ambiente esistente (es. clonare staging per creare un ambiente di test) è sceso da 3-4 ore di lavoro manuale a 15 minuti di esecuzione Terraform. Il costo consulenziale medio di introdurre Terraform su un'infrastruttura PMI esistente è circa 5.000-8.000 euro una tantum; il valore misurabile in termini di riduzione del rischio operativo e aumento della velocità di delivery è significativamente superiore.
Questo articolo descrive l'approccio pragmatico con cui introduco Terraform in contesti PMI italiane, basato sull'esperienza di questi 6+ progetti degli ultimi due anni. Il principio guida è uno: Terraform non serve a "fare DevOps cool" - serve a eliminare una classe specifica di errori operativi che in contesti PMI sono relativamente frequenti e potenzialmente devastanti. L'adozione va calibrata sul valore reale che produce per ciascun contesto, non sul desiderio di seguire una moda tecnologica.
Perché Infrastructure as Code vale la pena in contesti PMI, non solo enterprise
Il concetto di Infrastructure as Code nasce nell'ecosistema enterprise cloud-native dove la scala dell'infrastruttura (centinaia o migliaia di risorse cloud) rende impraticabile la gestione manuale. Molte PMI italiane concludono erroneamente che "IaC è una cosa per chi ha cluster Kubernetes e centinaia di servizi" - non applicabile al loro contesto. Questa conclusione è sbagliata per due motivi.
Primo motivo: IaC non serve solo per scala - serve per affidabilità e ripetibilità. Anche un'infrastruttura PMI con 3 VPS, 2 load balancer, 1 database managed e 1 bucket S3 beneficia enormemente dalla gestione come codice. La differenza non è quantitativa (numero di risorse) ma qualitativa (come si gestiscono le modifiche). Secondo motivo: IaC è il prerequisito naturale del disaster recovery serio. Se hai la tua intera infrastruttura descritta in codice Terraform versionato, puoi ricostruirla da zero in un'altra regione geografica in 30-60 minuti in caso di disastro. Se la tua infrastruttura vive solo "nel pannello del provider", ricostruirla richiede ricordare a memoria tutti i parametri, o ipoteticamente, il tempo della ricerca forense sull'account cliente - settimane.
La documentazione ufficiale HashiCorp su Terraform descrive in dettaglio i concetti fondamentali e i provider disponibili, ed è il riferimento canonico per iniziare. Terraform supporta oltre 3.000 provider - plugin specializzati per specifici sistemi - inclusi tutti i principali cloud (AWS, Azure, GCP) e i provider europei rilevanti (Hetzner, OVH, Digital Ocean). La multi-cloud capacity è uno dei valori strutturali di Terraform: lo stesso linguaggio gestisce risorse eterogenee con sintassi consistente.
Se gestisci un'infrastruttura PMI e non hai ancora introdotto IaC, nel mio profilo professionale trovi il dettaglio dei progetti di introduzione Terraform che ho guidato in contesti PMI italiane, sempre con approccio pragmatico e calibrato sul valore operativo reale per il cliente specifico.
Il provider Hetzner Cloud: risorse gestibili e pattern comuni
Il provider ufficiale Hetzner per Terraform è disponibile sul Terraform Registry e copre praticamente tutte le risorse gestibili via Hetzner Cloud Console: server, volume, floating IP, network privata, firewall, load balancer, certificati, SSH key, snapshot. La definizione di un'infrastruttura Hetzner semplice ma completa si scrive in poche decine di righe di Terraform HCL.
Esempio di un'infrastruttura base per un cliente PHP con VPS applicativo, VPS database, firewall, network privata:
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
backend "s3" {
bucket = "terraform-state-company"
key = "production/infra.tfstate"
region = "eu-central-1"
}
}
provider "hcloud" {
token = var.hcloud_token
}
resource "hcloud_network" "private" {
name = "app-private-network"
ip_range = "10.0.0.0/16"
}
resource "hcloud_network_subnet" "private" {
network_id = hcloud_network.private.id
type = "cloud"
network_zone = "eu-central"
ip_range = "10.0.1.0/24"
}
resource "hcloud_firewall" "app" {
name = "app-firewall"
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = var.allowed_ssh_ips
}
}
resource "hcloud_server" "app" {
name = "app-production"
server_type = "ccx33"
image = "debian-12"
location = "fsn1"
ssh_keys = [hcloud_ssh_key.deploy.id]
firewall_ids = [hcloud_firewall.app.id]
network {
network_id = hcloud_network.private.id
ip = "10.0.1.10"
}
user_data = file("cloud-init/app-server.yaml")
}
resource "hcloud_server" "database" {
name = "db-production"
server_type = "ccx23"
image = "debian-12"
location = "fsn1"
ssh_keys = [hcloud_ssh_key.deploy.id]
network {
network_id = hcloud_network.private.id
ip = "10.0.1.20"
}
user_data = file("cloud-init/db-server.yaml")
}Questa configurazione di 65 righe circa rappresenta l'infrastruttura completa in formato dichiarativo. Ogni modifica futura (aggiungere una nuova porta al firewall, cambiare il server type, aggiungere un nuovo server) avviene modificando il codice, committando in Git, eseguendo terraform plan per vedere cosa cambierà, eseguendo terraform apply per applicare effettivamente. Questo flusso è tracciabile, revisionabile, versionato - tutto quello che non era il click nel pannello.
Il pattern di bootstrap con cloud-init: dal VPS nudo al server applicativo pronto
Un VPS appena creato da Terraform è un server Debian nudo senza applicazione, senza utenti, senza pacchetti specifici. Per trasformarlo in un server applicativo funzionante serve un bootstrap - la configurazione iniziale del sistema operativo e l'installazione dei pacchetti necessari. Il pattern standard è cloud-init, un sistema multi-distro per provisioning iniziale supportato nativamente da Hetzner, Digital Ocean e altri provider.
Il file cloud-init/app-server.yaml referenziato nel Terraform sopra è qualcosa tipo:
#cloud-config
packages:
- curl
- git
- nginx
- php8.3-fpm
- php8.3-cli
- php8.3-mysql
- php8.3-mbstring
- php8.3-xml
- php8.3-curl
- php8.3-intl
users:
- name: deploy
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
write_files:
- path: /etc/nginx/sites-available/app
content: |
server {
listen 443 ssl http2;
server_name app.example.com;
root /var/www/app/public;
# ... configurazione completa nginx
}
runcmd:
- systemctl enable nginx
- systemctl enable php8.3-fpm
- ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/Cloud-init viene eseguito una volta al primo boot del server. Dopo il primo boot, il server è pronto per il deploy applicativo - che può avvenire tramite la pipeline CI/CD standard (es. GitHub Actions per deploy Laravel su VPS unmanaged come ho descritto in un articolo dedicato). La separazione fra provisioning infrastrutturale (Terraform), configurazione sistema (cloud-init), e deploy applicativo (CI/CD) produce un modello pulito in cui ogni strato ha responsabilità chiare.
State management: il pattern critico dei Terraform remote state
Il componente Terraform spesso sottovalutato dai principianti è la gestione dello state file - il file terraform.tfstate che Terraform usa per mappare le risorse dichiarate nel codice con quelle effettivamente esistenti nel provider. Questo file è critico perché contiene l'intero inventario dell'infrastruttura gestita e spesso include segreti (token, password generati). Gestirlo male significa disastri operativi o esposizione di credenziali.
Il pattern di default sbagliato è tenere lo state file locale sulla macchina di chi esegue Terraform. Funziona per un singolo operatore ma è fragile in team multipli (conflitti di state), non backup-friendly (se il laptop crasha perdi lo state), e non sicuro (lo state file contiene segreti).
Il pattern corretto è remote state su backend cifrato. Le opzioni principali sono tre. Prima opzione: AWS S3 con locking tramite DynamoDB. Il backend S3 è supportato nativamente, gestisce versioning automatico, cifratura at-rest tramite AWS KMS. DynamoDB fornisce lock distribuito per prevenire modifiche concorrenti che corromperebbero lo state. Seconda opzione: Terraform Cloud (hosted da HashiCorp) - gestione completa con UI web, ottimo per team distribuiti ma costo aggiuntivo. Terza opzione: backend consul o backend postgres self-hosted - valido per team tecnici che preferiscono hostare tutto internamente.
Per le mie PMI italiane, uso tipicamente S3 con DynamoDB locking su AWS con region EU (eu-central-1 Francoforte per conformità GDPR). Il costo operativo è di circa 2 euro al mese per l'intera gestione - irrisorio.
Moduli Terraform per pattern riutilizzabili
Man mano che il numero di clienti gestiti cresce, emergono pattern ricorrenti - configurazioni che si replicano con piccole variazioni (stack VPS+database+LB+firewall per applicazioni PHP tipiche). Il pattern Terraform per gestire riusabilità è il modulo: un package riutilizzabile di codice Terraform che espone variabili di input e genera risorse come output.
Ho costruito nel tempo una libreria privata di moduli per pattern tipici PMI italiane: modulo "Laravel stack base" (VPS applicativo + VPS database + network privata + firewall + DNS + Let's Encrypt), modulo "staging environment" (replica a scala ridotta del production), modulo "disaster recovery region" (infrastruttura dormant in region diversa pronta per attivazione in emergenza), modulo "blue-green deployment" (due set paralleli di VPS applicativi con load balancer che può swappare). L'uso di un modulo è:
module "production_stack" {
source = "git::https://private-repo.example.com/terraform-modules/laravel-stack.git?ref=v2.1.0"
project_name = "acme-ecommerce"
environment = "production"
app_server_type = "ccx33"
db_server_type = "ccx23"
domain = "ecommerce.acme.com"
ssh_key_content = file("~/.ssh/deploy.pub")
}Questa chiamata genera l'intera infrastruttura di un cliente con 10 righe di codice. Il beneficio è enorme quando si ha un parco di clienti: replicare il pattern standard su un nuovo cliente richiede solo scrivere il modulo-call specifico, non duplicare 200 righe di Terraform. I principi architetturali di riutilizzo dei moduli Terraform si integrano con i pattern di provisioning automatizzato via Ansible per VPS Linux PMI senza DevOps dedicato che ho descritto in un articolo dedicato, dove Terraform gestisce l'infrastruttura e Ansible completa la configurazione del sistema operativo.
GitOps workflow: pull request, plan automatico, apply controllato
Il pattern operativo completo per gestire Terraform in team si chiama GitOps: ogni modifica infrastrutturale avviene tramite pull request su Git, la pipeline CI esegue terraform plan e mostra il diff proposto, il team revisiona e approva, la pipeline CI/CD esegue terraform apply dopo l'approval. Questo flusso implementa disciplina operativa equivalente a quella del codice applicativo.
Il pattern di pipeline GitHub Actions che uso tipicamente:
name: Terraform
on:
pull_request:
paths: ['terraform/**']
push:
branches: [main]
paths: ['terraform/**']
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform -chdir=terraform init
- run: terraform -chdir=terraform plan -out=tfplan
- uses: actions/upload-artifact@v4
with:
name: tfplan
path: terraform/tfplan
apply:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform -chdir=terraform init
- run: terraform -chdir=terraform apply -auto-approveIl job plan gira su ogni pull request e genera un piano delle modifiche che verranno applicate, output disponibile nella UI GitHub per revisione. Il job apply gira solo sul merge su main e applica effettivamente le modifiche all'infrastruttura reale. Il requisito environment: production permette di configurare approvazione manuale obbligatoria prima dell'apply - un secondo paio d'occhi umano autorizza il deploy infrastrutturale dopo aver revisionato il piano.
Questo flusso è coerente con i principi di CI/CD sicuro per proteggere pipeline da attacchi di injection e supply chain che ho descritto in un articolo dedicato, dove la protezione della pipeline è critica quando la pipeline stessa ha potere di modificare infrastruttura di produzione.
Il risultato finale dell'adozione Terraform su tutti i miei clienti gestiti, misurato negli ultimi 24 mesi, è stato il seguente. Zero incidenti operativi attribuibili a errori umani nella configurazione infrastrutturale (contro 3-5 incidenti minori all'anno pre-Terraform). Tempo di creazione di un nuovo ambiente (staging, development, DR) sceso da mezza giornata a 15-30 minuti di esecuzione automatizzata. Audit trail completo di ogni modifica infrastrutturale tramite Git history. Onboarding di nuovi collaboratori tecnici: sanno subito lo stato dell'infrastruttura leggendo il codice, senza dover esplorare pannelli provider multipli. Costo di migrazione iniziale a Terraform per nuovo cliente: 5-8 giornate di lavoro. Costo operativo ricorrente: trascurabile (CI/CD già esistente, state storage 2€/mese).
Se gestisci un'infrastruttura PMI con multiple macchine, network, firewall, DNS distribuiti su uno o più cloud provider europei, e continui a gestire le modifiche tramite click nei pannelli web, ti trovi in una situazione operativa a rischio latente - il prossimo incidente da configurazione umana sbagliata è solo questione di tempo. L'introduzione di Terraform è un investimento di 5-8 giornate che produce beneficio strutturale permanente. Se vuoi confrontarti sul tuo caso specifico con una proposta di introduzione Terraform calibrata sulla dimensione della tua infrastruttura e sulla capacità operativa del tuo team, contattami per una consulenza preliminare: in una sessione di analisi guidata produciamo insieme una mappatura della tua attuale infrastruttura, un disegno di moduli Terraform specifici per il tuo caso, e una roadmap di migrazione incrementale senza interruzioni operative.