Contesto e Motivazioni

n8n è un potente strumento open source per l’automazione dei workflow, che permette di collegare una vasta gamma di servizi tramite nodi configurabili. A differenza di alternative SaaS (Software as a Service) come Zapier o Make, n8n può essere installato in modalità self-hosted, garantendo il pieno controllo su dati, privacy ed estensibilità.

L’obiettivo di questo progetto è illustrare come realizzare un’istanza di n8n completamente automatizzata all’interno di un ambiente casalingo (homelab). Verranno utilizzati:

  • container LXC per l’isolamento dell’ambiente
  • OpenTofu (un fork open source di Terraform) per il provisioning dell’infrastruttura
  • Ansible per la configurazione idempotente del servizio

L’intero codice del progetto è disponibile nel repository pubblico:
👉 https://github.com/monte97/homelab-n8n


Stack Infrastrutturale

Le scelte architetturali

L’infrastruttura è stata concepita su tre livelli distinti per una chiara separazione delle responsabilità e per garantire un ambiente completamente riproducibile:

  1. Provisioning: L’infrastruttura che ospiterà il servizio è gestita tramite OpenTofu.
  2. Configurazione: L’impostazione del sistema operativo e l’installazione dei prerequisiti avvengono con Ansible.
  3. Deploy: L’applicazione n8n viene distribuita tramite docker-compose, anch’esso gestito da Ansible.

L’adozione di LXC (Linux Containers) come tecnologia di containerizzazione è una scelta ponderata. Motivata dalla sua leggerezza e dalla maggiore trasparenza rispetto a una macchina virtuale (VM) tradizionale, LXC opera direttamente sul kernel del sistema host, offrendo un’efficienza superiore.

Focus: Container LXC vs. Docker

I Linux Containers (LXC) e Docker rappresentano due approcci alla containerizzazione con filosofie diverse. Mentre Docker ha rivoluzionato il deployment delle applicazioni con il paradigma dell’application container, LXC si basa sul concetto di system container: un ambiente che simula un sistema operativo completo pur condividendo il kernel dell’host.

CaratteristicaLXC (System Container)Docker (Application Container)
ObiettivoEseguire un sistema operativo completo, isolato e autonomo.Eseguire una singola applicazione o un singolo processo.
StrutturaInclude un sistema di init (es. systemd), più servizi, utenti e processi.Generalmente ha un unico processo principale e un’architettura stateless.
PersistenzaProgettato per essere persistente e “stateful”.Progettato per essere immutabile e “stateless” (i dati persistenti sono gestiti tramite volumi esterni).
FilosofiaSi comporta come una VM leggera.Paradigma “cattle, not pets”.
Meccanismi di isolamentoUtilizza Namespace e CGroup.Utilizza Namespace e CGroup.

Topologia logica

L’istanza di n8n è isolata in un container LXC con una rete in modalità bridge. Questa configurazione assegna al container un indirizzo IP unico sulla rete locale, rendendolo accessibile dalla LAN senza esporlo direttamente su Internet, migliorando la sicurezza.

Tutte le configurazioni, dalla definizione del container all’impostazione dell’applicazione, sono versionate su Git e applicate in modo idempotente tramite Ansible. Questo assicura che l’intero servizio possa essere ricreato in modo affidabile e completo su qualsiasi nodo compatibile, con un intervento manuale minimo.


Provisioning con OpenTofu

Che cos’è OpenTofu?

OpenTofu è uno strumento open source per la gestione dell’infrastruttura attraverso codice dichiarativo. Nato come fork della versione open source di Terraform, OpenTofu mantiene la stessa sintassi e filosofia, offrendo continuità e trasparenza.

Tradizionalmente, il provisioning avviene tramite interfacce web, script manuali o procedure documentate, un approccio che può portare a:

  • Inconsistenza: ogni deployment può differire leggermente dal precedente.
  • Mancanza di tracciabilità: è difficile sapere chi ha fatto cosa e quando.
  • Scalabilità limitata: creare molti server richiede un tempo proporzionalmente maggiore.
  • Disaster recovery complesso: ricreare un ambiente da zero è spesso un processo lungo e manuale.

OpenTofu risolve questi problemi permettendo di descrivere lo stato desiderato dell’infrastruttura in file di configurazione, come nell’esempio seguente.

# Esempio: si definisce COSA si vuole, non COME crearlo
resource "lxc_container" "n8n_prod" {
  name     = "n8n-production"
  image    = "ubuntu/22.04"
  memory   = "2048MB"
  cpu      = 2
  
  network {
    name = "lxc-bridge"
    ip   = "10.0.0.100"
  }
}

Analisi del codice (file main.tf)

Di seguito sono analizzate le parti salienti del file main.tf per comprendere come OpenTofu traduce le intenzioni in infrastruttura concreta.

Configurazione del provider

terraform {
  required_providers {
    proxmox = {
      source = "telmate/proxmox"
      version = "~> 2.9"
    }
  }
  required_version = ">= 1.0"
}

Questa sezione definisce i requisiti fondamentali del progetto. Si specifica l’uso del provider Proxmox (version = "~> 2.9") e una versione minima di OpenTofu/Terraform (>= 1.0). Questa pratica garantisce riproducibilità e stabilità nell’ambiente di deployment.

Autenticazione sicura

provider "proxmox" {
  pm_api_url = var.proxmox_api_url
  pm_api_token_id = var.proxmox_api_token_id
  pm_api_token_secret = var.proxmox_api_token_secret
  pm_tls_insecure = var.proxmox_tls_insecure
}

Il provider utilizza variabili anziché credenziali hard-coded. Questo approccio, una best practice in ambito Infrastructure as Code (IaC), mantiene i segreti separati dal codice e supporta diversi ambienti (dev, staging, prod) con la stessa configurazione base, ma credenziali differenti.

Definizione dichiarativa della risorsa

Il cuore della configurazione è la definizione del container LXC:

resource "proxmox_lxc" "n8n_container" {
  target_node = var.proxmox_node
  hostname = var.vm_name
  description = "n8n Workflow Automation Container"
  
  # Configurazione risorse computazionali
  cores = var.vm_cores
  memory = var.vm_memory
  swap = var.vm_swap
}

Questa sezione dimostra la natura dichiarativa di OpenTofu. Non si sta scrivendo uno script che dice “crea un container, poi assegna memoria…”, ma si sta definendo lo stato finale desiderato. OpenTofu orchestra le chiamate API necessarie per raggiungere tale stato.

Gestione dello storage

# Root filesystem
rootfs {
  storage = var.vm_storage
  size = var.vm_disk_size
}

# Storage dedicato per i dati applicativi
mountpoint {
  key = "0"
  slot = 0
  storage = var.vm_storage
  mp = "/opt/n8n"
  size = var.vm_data_disk_size
}

La configurazione dello storage mostra un pattern avanzato: la separazione tra filesystem di sistema e dati applicativi. Questo facilita backup selettivi, migrazione dei dati e ridimensionamento indipendente dello storage.

Networking deterministico

network {
  name = "eth0"
  bridge = var.vm_network_bridge
  ip = var.vm_ip_address
  gw = var.vm_gateway
  type = "veth"
}

La configurazione di rete elimina l’assegnazione casuale degli indirizzi IP. Ogni risorsa ha un indirizzo prevedibile, essenziale per l’automazione, il monitoraggio e l’integrazione con altri sistemi.

Provisioning post-creazione

provisioner "remote-exec" {
  inline = [
    "apt-get update",
    "apt-get install -y curl wget gnupg python3",
    "systemctl enable ssh"
  ]
  connection {
    type = "ssh"
    host = split("/", var.vm_ip_address)[0]
    private_key = file(var.ssh_private_key)
  }
}

I provisioner permettono di eseguire configurazioni post-creazione. In questo caso, il sistema viene preparato con i pacchetti base e viene abilitato SSH. Questo crea un ponte fondamentale tra la creazione dell’infrastruttura e la sua configurazione applicativa.

Lifecycle management

lifecycle {
  ignore_changes = [
    ostemplate,  # Evita ricreazione del container per cambi template
  ]
}

Le regole di lifecycle prevengono ricreazioni indesiderate. Ad esempio, se il template del container viene aggiornato su Proxmox, OpenTofu non tenterà di ricreare il container esistente, preservando i dati e le configurazioni.


Automazione con Ansible

Che cos’è Ansible?

Ansible è una piattaforma di automazione “agentless” per la configurazione, il deployment e l’orchestrazione dei sistemi. Sviluppata da Red Hat, è uno degli strumenti più diffusi in ambito DevOps per la gestione di infrastrutture complesse.

Dopo aver effettuato il provisioning dell’infrastruttura, il passo successivo è la configurazione, un processo che tradizionalmente richiedeva:

  • login manuale sui server per installare software.
  • script bash personalizzati, spesso difficili da gestire.
  • procedure manuali da seguire passo dopo passo.
  • mancanza di idempotenza: ripetere la stessa operazione poteva produrre risultati diversi.

Ansible risolve questi problemi grazie a due caratteristiche fondamentali:

  • Dichiarativo: invece di scrivere script che descrivono come fare qualcosa, si dichiara quale stato finale si vuole raggiungere. Ansible determina le azioni necessarie per ottenerlo.
  • Agentless: non richiede l’installazione di alcun software aggiuntivo sui sistemi target, utilizzando protocolli standard come SSH (per Linux) e WinRM (per Windows).

La sua forza sta nella capacità di unificare la gestione di tutti i livelli con un unico linguaggio e una metodologia coerente.

# Esempio: si dichiara LO STATO DESIDERATO
- name: Ensure Docker is installed and running
  package:
    name: docker.io
    state: present
  
- name: Ensure Docker service is enabled
  service:
    name: docker
    state: started
    enabled: yes

Analisi del Playbook di configurazione

Di seguito sono analizzate le sezioni più significative del playbook Ansible per comprendere come trasformare un container vuoto in un’applicazione pronta per la produzione.

Struttura e variabili centralizzate

vars:
  n8n_data_dir: "/opt/n8n_data"
  n8n_port: 5678
  n8n_domain: "n8n.K8S2.homelab"
  n8n_timezone: "Europe/Rome"
  n8n_docker_image: "docker.n8n.io/n8nio/n8n"

La centralizzazione delle variabili è una best practice fondamentale. Tutte le configurazioni specifiche dell’ambiente sono definite in un unico punto, rendendo il playbook adattabile a diversi contesti (development, staging, production) semplicemente modificando questi valori.

Gestione condizionale del sistema operativo

- name: Install required system packages
  ansible.builtin.package:
    name:
      - ca-certificates
      - curl
      - gnupg
      - python3-pip
    state: present
  when: ansible_os_family == "Debian"

Ansible utilizza i facts automatici per rilevare le caratteristiche del sistema target. La condizione when: ansible_os_family == "Debian" rende il playbook compatibile con diverse distribuzioni Linux, adattando automaticamente i comandi.

Installazione di Docker

- name: Add Docker GPG key
  ansible.builtin.apt_key:
    url: "https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg"
    state: present

- name: Add Docker APT repository
  ansible.builtin.apt_repository:
    repo: "deb [arch=amd64] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
    state: present

Invece di installare Docker dai repository di default, spesso obsoleti, il playbook configura i repository ufficiali. L’uso delle variabili {{ ansible_distribution }} e {{ ansible_distribution_release }} garantisce che venga utilizzato il repository corretto per la specifica versione del sistema operativo.

Gestione dei privilegi

- name: Add user to docker group
  ansible.builtin.user:
    name: "{{ ansible_user }}"
    groups: docker
    append: yes

- name: Install compatible Docker Python packages
  ansible.builtin.pip:
    name:
      - docker==6.1.3
      - docker-compose==1.29.2
    state: present

La gestione dei privilegi segue il principio del minimo privilegio. L’utente viene aggiunto al gruppo docker per evitare l’uso di sudo per ogni comando. L’installazione di versioni specifiche dei pacchetti Python (docker==6.1.3) garantisce compatibilità e riproducibilità.

Cleanup idempotente

- name: Remove old n8n containers and data (cleanup)
  ansible.builtin.shell: |
    docker compose down || true
    docker container rm -f n8n || true
    docker volume rm n8n_data || true
  args:
    chdir: "{{ n8n_data_dir }}"
  ignore_errors: yes

Questa sezione implementa un cleanup robusto prima della configurazione. L’uso di || true e ignore_errors: yes rende l’operazione idempotente: può essere eseguita più volte senza generare errori, anche se gli elementi da rimuovere non esistono.

Generazione dinamica della configurazione

- name: Create Docker Compose file for n8n
  ansible.builtin.copy:
    content: |
      version: '3.8'
      services:
        n8n:
          image: {{ n8n_docker_image }}:{{ n8n_docker_tag }}
          environment:
            - N8N_HOST={{ n8n_domain }}
            - WEBHOOK_URL=http://{{ n8n_domain }}:{{ n8n_port }}
            - GENERIC_TIMEZONE={{ n8n_timezone }}
          volumes:
            - n8n_data:/home/node/.n8n
    dest: "{{ n8n_data_dir }}/docker-compose.yml"
  notify: Restart n8n container

Il playbook genera dinamicamente il file Docker Compose utilizzando le variabili definite in precedenza. Questo elimina la necessità di mantenere template separati e assicura che ogni deployment sia configurato correttamente per il proprio ambiente.

Handler per reazioni automatiche

handlers:
  - name: Restart n8n container
    ansible.builtin.command:
      cmd: docker compose restart
      chdir: "{{ n8n_data_dir }}"
    listen: "Restart n8n container"

Gli handler sono un sistema di reazioni automatiche: quando il task di creazione del Docker Compose file viene modificato (notify: Restart n8n container), Ansible esegue automaticamente il riavvio del container. Questo garantisce che le modifiche di configurazione vengano applicate immediatamente.

Deployment idempotente

- name: Start n8n container with Docker Compose
  ansible.builtin.command:
    cmd: docker compose up -d
    chdir: "{{ n8n_data_dir }}"
  register: docker_compose_result
  changed_when: "'Creating' in docker_compose_result.stdout or 'Starting' in docker_compose_result.stdout"

Il task finale implementa un deployment intelligente: Ansible considera il task “modificato” solo se effettivamente vengono creati o avviati nuovi container. Questo distingue tra esecuzioni che cambiano lo stato del sistema e quelle che lo trovano già nello stato desiderato.


n8n: deploy e configurazione

Il deployment di n8n rappresenta l’ultimo livello dello stack, dove la semplicità operativa incontra la potenza dell’automazione. L’utilizzo di Docker Compose per orchestrare il container applicativo mantiene la coerenza con l’approccio dichiarativo dell’intera infrastruttura.

Filosofia di deployment

La configurazione di n8n segue i principi di “production-readiness” e operabilità, focalizzandosi su:

  • Configurazione essenziale tramite variabili d’ambiente.
  • Persistenza dei dati gestita da volumi.
  • Affidabilità con riavvio automatico.
  • Semplicità operativa, evitando l’over-engineering.
version: '3.8'
services:
    n8n:
        image: {{ n8n_docker_image }}:{{ n8n_docker_tag }}
        container_name: n8n
        restart: unless-stopped
        ports:
        - "{{ n8n_port }}:5678"
        environment:
        - N8N_SECURE_COOKIE=false
        - N8N_HOST={{ n8n_domain }}
        - N8N_PORT=5678
        - N8N_PROTOCOL=http
        - WEBHOOK_URL=http://{{ n8n_domain }}:{{ n8n_port }}
        - GENERIC_TIMEZONE={{ n8n_timezone }}
        - TZ={{ n8n_timezone }}
        - N8N_LOG_LEVEL=info
        - N8N_DIAGNOSTICS_ENABLED=false
        
        volumes:
        - n8n_data:/home/node/.n8n
        
volumes:
    n8n_data:
        external: false

Le scelte di configurazione

  • Configurazione minimale: l’approccio “minimal viable configuration” include solo le variabili d’ambiente strettamente necessarie, riducendo la complessità operativa.
  • Named Volumes: l’uso di un volume Docker (n8n_data) garantisce la persistenza dei dati critici (database SQLite interno, workflow, credenziali e configurazioni), semplificando backup e migrazione.
  • Restart Policy: la policy unless-stopped assicura il riavvio automatico del container in caso di crash o riavvio del sistema, garantendo alta disponibilità.
  • Variabili template: l’uso di variabili Ansible ({{ n8n_domain }}, {{ n8n_port }}) permette di personalizzare la configurazione per diversi ambienti usando lo stesso template di base.

📚 Risorse Utili