Gestire cento server con shell script imperativi non scala: ogni script dà per scontato uno stato di partenza che, dopo qualche mese di modifiche a mano, sui cento nodi non è più lo stesso. Puppet — scritto in Ruby da Luke Kanies e mantenuto da Reductive Labs — ribalta il problema cambiando ciò che si scrive. Invece della sequenza di comandi per arrivare a uno stato, si descrive lo stato in cui la macchina deve trovarsi. La serie 0.24, attiva nel 2009 sotto licenza GPLv2, è la base stabile sotto la maggior parte delle installazioni di oggi.
Contesto
La configurazione automatica di parchi macchine non è un problema nuovo. Lo precede CFEngine, che Mark Burgess scrive a partire dal 1993: la versione 3, uscita nel 2008, rifonda il modello sulla promise theory, l’idea che ogni componente dichiari ciò che garantisce invece di ricevere ordini. Puppet nasce nel 2005 nello stesso solco, con una scelta diversa sul linguaggio: un domain-specific language (DSL) dedicato, che chi amministra sistemi legge senza dover essere uno sviluppatore Ruby a tempo pieno.
Intanto il campo si affolla. A gennaio 2009 Opscode, la società di Adam Jacob, pubblica la prima release di Chef, anch’esso in Ruby ma con le recipe scritte come codice Ruby vero e proprio, più vicino allo stile imperativo. C’è poi bcfg2, sviluppato all’Argonne National Laboratory, meno diffuso. Per ora Puppet resta avanti: la community più ampia e il maggior numero di installazioni documentate.
Il modello dichiarativo
L’unità di base è la risorsa: un pacchetto, un file, un servizio, un utente. Ogni risorsa ha un tipo, un titolo e una serie di attributi che ne descrivono lo stato voluto. Un frammento di manifest:
package { 'nginx':
ensure => installed,
}
file { '/etc/nginx/nginx.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('nginx/nginx.conf.erb'),
require => Package['nginx'],
notify => Service['nginx'],
}
service { 'nginx':
ensure => running,
enable => true,
require => Package['nginx'],
}
Il manifest non dice come installare nginx: non c’è apt-get, non c’è yum. Dice che il pacchetto deve risultare presente, e Puppet sceglie il provider giusto a seconda del sistema — apt su Debian, yum su Red Hat, pkgadd su Solaris. Lo stesso manifest porta allo stato voluto su distribuzioni diverse, perché tradurlo nei comandi nativi spetta ai provider del resource abstraction layer.
Le relazioni require e notify non sono un ordine di lettura: dichiarano dipendenze. Puppet le risolve costruendo un grafo, non eseguendo le risorse nell’ordine in cui compaiono nel file.
Catalog e idempotenza
Quando un nodo chiede la propria configurazione, il Puppet master compila i manifest in un catalog: un documento per quel singolo nodo, dove le risorse astratte sono risolte e il grafo delle dipendenze è ordinato topologicamente. L’agent riceve il catalog e lo applica una risorsa alla volta.
A rendere praticabile il modello è l’idempotenza. Prima di toccare una risorsa, l’agent ne guarda lo stato corrente: se /etc/nginx/nginx.conf ha già il contenuto, il proprietario e i permessi richiesti, l’agent non fa niente. Applicare lo stesso catalog due volte, o cento, lascia il sistema nello stesso stato. Qui sta la differenza fra una risorsa dichiarativa e uno script: lo script echo "..." >> file aggiunge una riga a ogni esecuzione, la risorsa file converge a un unico stato.
L’idempotenza porta con sé una conseguenza operativa netta: l’agent può girare di continuo. La configurazione di default lo fa partire ogni 30 minuti. Se qualcuno mette mano a un file gestito, alla passata dopo Puppet lo riporta allo stato dichiarato e lo annota nel report. La deriva della configurazione — i nodi nati identici che col tempo divergono — si corregge a ogni ciclo invece di accumularsi.
Architettura agent/master
Nel 2009 l’installazione tipica è client/server:
- il master è il server centrale che compila i catalog. Gira come applicazione Ruby servita da WEBrick per i test o, in produzione, da Apache con il modulo Passenger;
- l’agent è il demone su ogni nodo gestito. Contatta il master via HTTPS con autenticazione a certificati, scarica il catalog, lo applica, rispedisce un report;
- Facter è lo strumento di introspezione. Raccoglie i fact del nodo — sistema operativo, versione del kernel, indirizzi delle interfacce, RAM, architettura — e li espone come variabili nei manifest. È così che lo stesso manifest ramifica su
$operatingsystemo$memorytotalsenza che l’amministratore distingua i nodi uno per uno.
I certificati sono il punto delicato dell’architettura. Il master fa da certificate authority: un agent nuovo genera una richiesta di firma, l’amministratore la approva sul master, e solo da quel momento il nodo riceve i catalog. È il meccanismo che impedisce a una macchina qualunque sulla rete di farsi configurare come un nodo legittimo, ma è anche il primo intoppo concreto quando si automatizza il provisioning di istanze nuove — su EC2, per dire, dove le macchine nascono e muoiono in continuazione e la firma a mano diventa un collo di bottiglia.
Manifest come codice
La conseguenza meno vistosa, e forse la più duratura, è che i manifest sono file di testo. Stanno in un repository Subversion o Git, hanno una storia di commit, si confrontano con un diff, passano per una revisione prima di andare in produzione. La configurazione di un’infrastruttura smette di vivere nella testa di chi l’ha montata e nei file sparsi sui singoli host, e diventa un artefatto versionato come un sorgente qualsiasi.
I moduli organizzano questi file: un modulo Apache, uno MySQL, uno Postfix racchiudono i manifest, i template e gli eventuali file statici di un componente, e si scambiano via mailing list o repository pubblici. Nel 2009 ci sono già decine di moduli della community per i servizi più comuni, anche se manca ancora un catalogo centrale e installarne uno vuol dire copiare directory.
Limiti
Il modello dichiarativo non è gratis. L’ordine delle risorse va dichiarato a mano con require e notify: dimenticarne uno produce un catalog che a volte funziona e a volte no, secondo l’ordine in cui il grafo viene percorso. Il debug di una dipendenza mancante è meno immediato della lettura di uno script lineare.
Il master è un punto di centralizzazione: compila il catalog per ogni nodo a ogni passata, e su parchi grandi diventa lui stesso un componente da dimensionare e da mettere in alta disponibilità. La gestione dei certificati aggiunge attrito proprio dove servirebbe scorrevolezza, cioè nel provisioning automatico.
C’è infine lo stato che Puppet riporta, che è solo quello modellato dalle risorse. Ciò che non sta in un manifest non è gestito, e su un sistema vero resta sempre una zona grigia di configurazione fatta a mano che nessuna risorsa rivendica — e che alla passata dopo Puppet, semplicemente, ignora.
- https://reductivelabs.com/trac/puppet/wiki/DocumentationStart
- https://reductivelabs.com/trac/puppet/wiki/TypeReference
- https://reductivelabs.com/trac/puppet/wiki/Facter
- https://www.cfengine.org/
- https://www.opscode.com/chef/
- https://www.noze.it/insights/puppet-0-24-iac/
Immagine di copertina: Diagramma di un catalog Puppet: nodi che rappresentano risorse (pacchetti, file, servizi) collegati da frecce di dipendenza in una… — diagramma di MarcelButucea, CC BY-SA 3.0 — https://commons.wikimedia.org/wiki/File:Expanded_relationships.svg