Puppet è un sistema di gestione della configurazione. Viene utilizzato per riportare gli host nello stato desiderato e mantenerlo.
Lavoro con Puppet da oltre cinque anni. Questo testo è fondamentalmente una compilazione tradotta e riorganizzata dei punti chiave dalla documentazione ufficiale, che permetterà ai principianti di comprendere rapidamente l'essenza di Puppet.

Informazioni di base
Il funzionamento di Puppet è di tipo client-server, sebbene sia supportata anche un'opzione di funzionamento senza server con funzionalità limitate.
Utilizza un modello di lavoro pull: di default, ogni mezz'ora i client si rivolgono al server per ottenere la configurazione e applicarla. Se hai lavorato con Ansible, lì si utilizza un modello diverso, push: l'amministratore avvia il processo di applicazione della configurazione, i client non applicano nulla da soli.
Nelle interazioni di rete viene utilizzata la crittografia TLS bidirezionale: sia il server che il client hanno le proprie chiavi private e i relativi certificati. In genere, il server emette certificati per i client, ma in linea di principio è possibile anche l'uso di un CA esterno.
Introduzione ai manifesti
Nella terminologia di Puppet si collegano i nodi (nodes). La configurazione per i nodi è scritta in manifesti in un linguaggio di programmazione apposito, il Puppet DSL. Il Puppet DSL è un linguaggio dichiarativo. Con esso si descrive lo stato desiderato di un nodo sotto forma di dichiarazione di risorse individuali, ad esempio:
Un file esiste e ha un contenuto specifico.
- Un pacchetto è installato.
- Un servizio è avviato.
- Le risorse possono essere correlate:
Ci sono dipendenze che influenzano l'ordine di applicazione delle risorse.
- Ad esempio, "prima installa il pacchetto, poi modifica il file di configurazione, dopodiché avvia il servizio".
Ci sono notifiche: se una risorsa cambia, invia notifiche alle risorse che vi sono iscritte. - Ad esempio, se un file di configurazione cambia, è possibile riavviare automaticamente il servizio.
Inoltre, nel Puppet DSL ci sono funzioni e variabili, così come operatori condizionali e selettori. Sono supportati diversi meccanismi di templating: EPP ed ERB.
Puppet è scritto in Ruby, quindi molte costruzioni e termini provengono da lì. Ruby consente di estendere Puppet: è possibile aggiungere logica complessa, nuovi tipi di risorse e funzioni.
Puppet è scritto in Ruby, quindi molte costruzioni e termini provengono da lì. Ruby consente di estendere Puppet: è possibile aggiungere logica complessa, nuovi tipi di risorse e funzioni.
Durante l'esecuzione di Puppet, i manifesti per ogni singolo nodo sul server vengono compilati nella directory. Directory è un elenco di risorse e delle loro interazioni dopo aver calcolato il valore delle funzioni, delle variabili e aver espanso gli operatori condizionali.
Sintassi e stile del codice
Ecco le sezioni della documentazione ufficiale che ti aiuteranno a comprendere la sintassi, nel caso gli esempi forniti non fossero sufficienti:
Ecco un esempio di come appare un manifesto:
# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
# Конфигурация по сути является перечислением ресурсов и их параметров.
#
# У каждого ресурса есть тип и название.
#
# Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
#
# Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
# Про разные типы ресурсов написано ниже.
#
# После типа в фигурных скобках пишется название ресурса, потом двоеточие,
# дальше идёт опциональное перечисление параметров ресурса и их значений.
# Значения параметров указываются через т.н. hash rocket (=>).
resource { 'title':
param1 => value1,
param2 => value2,
param3 => value3,
}
}Gli spazi e i ritorni a capo non sono una parte obbligatoria del manifesto, tuttavia esiste una . Riepilogo:
- Si utilizzano due spazi per l'indentazione, le tabulazioni non sono utilizzate.
- Le parentesi graffe sono separate da uno spazio, mentre i due punti non devono avere uno spazio.
- Le virgole devono seguire ogni parametro, incluso l'ultimo. Ogni parametro deve essere su una linea separata. Fanno eccezione i casi senza parametri e con uno solo: possono essere scritti su una sola riga e senza la virgola (ossia,
resource { 'title': }eresource { 'title': param => value }). - Le frecce dei parametri devono essere allineate.
- Le frecce delle interazioni delle risorse sono scritte prima di esse.
Posizione dei file sul puppetserver
Per ulteriori spiegazioni, introdurrò il concetto di "directory radice". La directory radice è quella in cui si trova la configurazione Puppet per un nodo specifico.
La directory radice varia a seconda della versione di Puppet e dell'uso degli ambienti. Gli ambienti sono insiemi di configurazione indipendenti, che sono memorizzati in directory separate. Di solito vengono utilizzati in combinazione con git, nel qual caso gli ambienti sono creati da branch git. Di conseguenza, ogni nodo si trova in uno o nell'altro ambiente. Questo viene configurato direttamente sul nodo o nell'ENC, di cui parlerò nel prossimo articolo.
- Nella terza versione ("vecchio Puppet"), la directory di base era
/etc/puppet. L'uso degli ambienti è opzionale: noi, ad esempio, non li utilizziamo con il vecchio Puppet. Se gli ambienti vengono utilizzati, solitamente sono memorizzati in/etc/puppet/environments, la directory radice sarà la directory dell'ambiente. Se non vengono utilizzati ambienti, la directory radice sarà quella di base. - A partire dalla quarta versione ("nuovo Puppet"), l'uso degli ambienti è diventato obbligatorio e la directory di base è stata spostata in
/etc/puppetlabs/code. Di conseguenza, gli ambienti vengono memorizzati in/etc/puppetlabs/code/environments, la directory radice è la directory dell'ambiente.
Nella directory radice deve esserci una sottodirectory manifests, in cui si trovano uno o più manifesti con la descrizione dei nodi. Inoltre, deve esserci una sottodirectory modules, in cui si trovano i moduli. Cosa sono i moduli, lo spiegherò più avanti. Inoltre, nella vecchia Puppet potrebbe esserci anche una sottodirectory files, in cui si trovano vari file che copiamo sui nodi. Nella nuova Puppet, invece, tutti i file sono stati spostati in moduli.
I file dei manifesti hanno estensione .pp.
Esempi di utilizzo
Descrizione del nodo e della risorsa su di esso
Nel nodo server1.testdomain deve essere creato un file /etc/issue con il contenuto Debian GNU/Linux n l. Il file deve appartenere all'utente e al gruppo root, i permessi devono essere 644.
Scriviamo il manifesto:
node 'server1.testdomain' { # blocco di configurazione relativo al nodo server1.testdomain
file { 'etc/issue': # descriviamo il file /etc/issue
ensure => present, # questo file deve esistere
content => 'Debian GNU/Linux n l', # deve avere questo contenuto
owner => root, # utente proprietario
group => root, # gruppo proprietario
mode => '0644', # permessi sul file. Sono dati come stringa (tra virgolette) perché altrimenti un numero con 0 all'inizio sarebbe interpretato come scritto in ottale, e tutto andrebbe male
}
}Interrelazioni delle risorse sul nodo
Nel nodo server2.testdomain deve avere nginx in esecuzione, che utilizza una configurazione preparata in anticipo.
Decomponiamo il compito:
- È necessario che il pacchetto
nginx. - È necessario copiare i file di configurazione dal server.
- È necessario che il servizio
nginx. - In caso di aggiornamento della configurazione, è necessario riavviare il servizio.
Scriviamo il manifesto:
node 'server2.testdomain' { # blocco di configurazione relativo al nodo server2.testdomain
package { 'nginx': # descriviamo il pacchetto nginx
ensure => installed, # deve essere installato
}
# La freccia diretta (->) indica che la risorsa sottostante deve
# essere creata dopo la risorsa descritta sopra.
# Queste dipendenze sono transitivi.
-> file { '/etc/nginx': # descriviamo il file /etc/nginx
ensure => directory, # deve essere una directory
source => 'puppet:///modules/example/nginx-conf', # il contenuto deve essere prelevato dal puppet server all'indirizzo specificato
recurse => true, # copiare i file in modo ricorsivo
purge => true, # devono essere eliminati i file in eccesso (quelli non presenti nella fonte)
force => true, # eliminare le directory in eccesso
}
# La freccia ondulata (~>) indica che la risorsa sottostante deve
# iscriversi alle modifiche della risorsa precedentemente descritta.
# La freccia ondulata include la diretta (->).
~> service { 'nginx': # descriviamo il servizio nginx
ensure => running, # deve essere in esecuzione
enable => true, # deve essere avviato automaticamente all'avvio del sistema
}
# Quando una risorsa di tipo service riceve una notifica,
# il servizio corrispondente viene riavviato.
}Perché questo funzioni, è necessaria circa questa disposizione dei file sul puppet server:
/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│ └── site.pp
└── modules/
└── example/
└── files/
└── nginx-conf/
├── nginx.conf
├── mime.types
└── conf.d/
└── some.confTipi di risorse
L'elenco completo dei tipi di risorse supportati si trova , qui descriverò cinque tipi di base, che nella mia pratica sono sufficienti per risolvere la maggior parte dei compiti.
file
Gestisce file, directory, symlink, il loro contenuto, i permessi.
Parametri:
- nome della risorsa — percorso del file (opzionale)
- path — percorso del file (se non specificato nel nome)
- ensure — tipo di file:
absent— rimuovere il filepresent— deve essere un file di qualsiasi tipo (se non c'è il file, verrà creato un file normale)file— file normaledirectory— directorylink— symlink
- content — contenuto del file (valido solo per file normali, non può essere usato insieme a source o target)
- source — link al percorso da cui copiare il contenuto del file (non può essere usato insieme a content o target). Può essere specificato sia come URI con schema
puppet:(in tal caso saranno utilizzati file dal puppet server), sia con schemahttp:(spero sia chiaro cosa accadrà in questo caso), e anche con schemafile:o come percorso assoluto senza schema (in tal caso sarà utilizzato il file dal filesystem locale sul nodo) - target — dove deve puntare il symlink (non può essere usato insieme a content o source)
- proprietario — utente a cui deve appartenere il file
- gruppo — gruppo a cui deve appartenere il file
- modalità — diritti sul file (in forma di stringa)
- ricorsivo — include l'elaborazione ricorsiva delle directory
- elimina — include la rimozione dei file che non sono descritti in Puppet
- forza — include la rimozione delle directory che non sono descritte in Puppet
pacchetto
Installa e rimuove pacchetti. È in grado di gestire notifiche — reinstalla il pacchetto se è specificato il parametro reinstalla_su_aggiornamento.
Parametri:
- nome della risorsa — nome del pacchetto (opzionale)
- name — nome del pacchetto (se non specificato nel nome)
- fornitore — gestore di pacchetti da utilizzare
- ensure — stato desiderato del pacchetto:
present,installato— qualsiasi versione installatalatest— installata l'ultima versioneabsent— rimosso (apt-get remove)eliminato— rimosso insieme ai file di configurazione (apt-get purge)bloccato— versione del pacchetto bloccata (apt-mark hold)qualsiasi altro valore— installata la versione specificata
- reinstalla_su_aggiornamento — se
true, allora, al ricevimento della notifica, il pacchetto sarà reinstallato. Utile per le distribuzioni basate su source, dove la ricompilazione dei pacchetti può essere necessaria quando si modificano i parametri di compilazione. Per impostazione predefinitafalse.
service
Gestisce i servizi. È in grado di gestire notifiche — riavvia il servizio.
Parametri:
- nome della risorsa — servizio da gestire (opzionale)
- name — servizio da gestire (se non specificato nel nome)
- ensure — stato desiderato del servizio:
in esecuzione— avviatofermo— fermo
- abilita — gestisce la possibilità di avvio del servizio:
true— avviato automaticamente (systemctl enable)maschera— mascherato (systemctl mask)false— disabilitato l'avvio automatico (systemctl disable)
- riavvia — comando per riavviare il servizio
- stato — comando per controllare lo stato del servizio
- ha_riavvio — specificare se lo script di init del servizio supporta il riavvio. Se
falsee il parametro è specificato riavvia — viene utilizzato il valore di questo parametro. Sefalsee il parametro riavvia non è specificato — il servizio viene fermato e riavviato per il riavvio (ma in systemd viene utilizzato il comandosystemctl restart). - ha_stato — specificare se lo script di init del servizio supporta il comando
statoSefalse, viene utilizzato il valore del parametro stato. Per impostazione predefinitatrue.
exec
Esegue comandi esterni. Se non vengono specificati parametri crea, solo_se, a_meno_che o solo_se_aggiornato, il comando verrà eseguito ad ogni esecuzione di Puppet. È in grado di gestire notifiche — esegue il comando.
Parametri:
- nome della risorsa — comando da eseguire (opzionale)
- command — comando da eseguire (se non specificato nel titolo)
- path — percorsi in cui cercare il file eseguibile
- solo_se — se il comando indicato in questo parametro termina con codice di uscita zero, il comando principale verrà eseguito
- a_meno_che — se il comando indicato in questo parametro termina con codice di uscita diverso da zero, il comando principale verrà eseguito
- crea — se il file specificato in questo parametro non esiste, il comando principale verrà eseguito
- solo_se_aggiornato — se
true, quindi il comando verrà avviato solo se questo exec riceve una notifica da altre risorse - cwd — directory da cui eseguire il comando
- user — utente da cui eseguire il comando
- fornitore — con cosa eseguire il comando:
- posix — crea semplicemente un processo figlio, è necessario specificarlo path
- shell — il comando viene eseguito in una shell
/bin/sh, può non essere specificato path, è possibile utilizzare globbing, pipe e altre funzionalità della shell. Di solito è determinato automaticamente se ci sono caratteri speciali (|,;,&&,||e così via).
cron
Gestisce i cron job.
Parametri:
- nome della risorsa — semplicemente un identificatore
- ensure — stato del cron job:
present— crea, se non esisteabsent— elimina, se esiste
- command — quale comando eseguire
- ambiente — in quale ambiente eseguire il comando (elenco delle variabili d'ambiente e dei loro valori separati da
=) - user — da quale utente eseguire il comando
- minuto, ora, giorno della settimana, mese, giorno del mese — quando eseguire il cron. Se uno di questi attributi non è specificato, il suo valore nel crontab sarà
*.
In Puppet 6.0 cron è come se in puppetserver, quindi non c'è documentazione sul sito generale. Ma è in puppet-agent, quindi non è necessario installarlo separatamente. La documentazione su di esso può essere vista , oppure .
Riguardo alle risorse in generale
Requisiti di unicità delle risorse
Il problema più comune che incontriamo è Dichiarazione duplicata. Questo errore si verifica quando nella directory finiscono due o più risorse dello stesso tipo con lo stesso nome.
Quindi lo ripeterò: nei manifesti per un nodo non devono esserci risorse dello stesso tipo con lo stesso nome (title)!
A volte è necessario installare pacchetti con lo stesso nome, ma con gestori di pacchetti diversi. In tal caso è necessario utilizzare il parametro name, per evitare errori:
pacchetto { 'ruby-mysql':
assicurati => installato,
nome => 'mysql',
fornitore => 'gem',
}
pacchetto { 'python-mysql':
assicurati => installato,
nome => 'mysql',
fornitore => 'pip',
}Negli altri tipi di risorse ci sono parametri simili che aiutano a evitare la duplicazione, — name a service, command a exec, e così via.
Metaparametri
Alcuni parametri speciali sono presenti per ogni tipo di risorsa, indipendentemente dalla sua entità.
Elenco completo dei metaparametri .
Elenco breve:
- require — in questo parametro si specifica da quali risorse dipende questa risorsa.
- before — in questo parametro si specifica quali risorse dipendono da questa risorsa.
- subscribe — in questo parametro si specifica da quali risorse riceve notifiche questa risorsa.
- notify — in questo parametro si specifica quali risorse ricevono notifiche da questa risorsa.
Tutti i metaparametri elencati accettano o un singolo riferimento a una risorsa, o un array di riferimenti tra parentesi quadre.
Riferimenti alle risorse
Un riferimento a una risorsa è semplicemente un accenno a quella risorsa. Vengono utilizzati principalmente per specificare le dipendenze. Un riferimento a una risorsa inesistente genererà un errore di compilazione.
La sintassi di un riferimento è la seguente: tipo di risorsa con la lettera maiuscola (se nel nome del tipo ci sono doppi punti, si scrive con la maiuscola ogni parte del nome tra i due punti), dopo di che tra parentesi quadre c'è il nome della risorsa (il caso del nome non cambia!). Non ci devono essere spazi, le parentesi quadre seguono immediatamente il nome del tipo.
Esempio:
file { '\/file1': assicurati => presente }
file { '\/file2':
assicurati => directory,
before => File['\/file1'],
}
file { '\/file3': assicurati => assente }
File['\/file1'] -> File['\/file3']Dipendenze e notifiche
Come già detto in precedenza, le dipendenze semplici tra risorse sono transitivo. Inoltre, fai attenzione nell'impostare le dipendenze — si possono creare dipendenze cicliche, il che genererà un errore di compilazione.
A differenza delle dipendenze, le notifiche non sono transitive. Per le notifiche si applicano le seguenti regole:
- Se una risorsa riceve una notifica, viene aggiornata. Le azioni durante l'aggiornamento dipendono dal tipo di risorsa — exec esegue un comando, service riavvia un servizio, pacchetto reinstalla un pacchetto. Se per la risorsa non è definita un'azione durante l'aggiornamento, non succede nulla.
- Durante un singolo passaggio, Puppet aggiorna una risorsa al massimo una volta. Questo è possibile poiché le notifiche includono dipendenze e il grafo delle dipendenze non contiene cicli.
- Se Puppet modifica lo stato di una risorsa, la risorsa invia notifiche a tutte le risorse che vi sono sottoscritte.
- Se una risorsa viene aggiornata, invia notifiche a tutte le risorse sottoscritte.
Gestione dei parametri non specificati
In genere, se un parametro di risorsa non ha un valore predefinito e non è specificato nel manifesto, Puppet non modificherà questa proprietà della risorsa corrispondente sul nodo. Ad esempio, se una risorsa di tipo file non ha specificato il parametro proprietario, Puppet non cambierà il proprietario del file corrispondente.
Introduzione a classi, variabili e definizioni
Supponiamo di avere diversi nodi con una parte di configurazione identica, ma anche differenze: altrimenti potremmo descrivere tutto in un unico blocco node {}. Certamente, si può semplicemente copiare le parti identiche di configurazione, ma in generale questo è una cattiva soluzione: la configurazione cresce e quando si modifica la parte comune della configurazione è necessario modificare lo stesso in più posti. È facile fare errori e l'intero principio DRY (don’t repeat yourself) non è stato inventato per caso.
Per risolvere tale problema, esiste una costruzione chiamata classe.
Classi
è un blocco di codice Puppet nominato. Le classi sono necessarie per il riutilizzo del codice.
Innanzitutto, è necessario descrivere la classe. La descrizione da sola non aggiunge risorse. La classe viene descritta nei manifesti:
# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
...
}Dopo di ciò, la classe può essere utilizzata:
# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальшеEsempio dal compito precedente: trasferiamo l'installazione e la configurazione di nginx in una classe:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => 'puppet:///modules/example/nginx-conf',
recurse => true,
purge => true,
force => true,
}
~> service { 'nginx':
ensure => running,
enable => true,
}
}
node 'server2.testdomain' {
include nginx_example
}Variabili
La classe dell'esempio precedente non è affatto flessibile, poiché porta sempre la stessa configurazione di nginx. Facciamo in modo che il percorso della configurazione diventi una variabile, in modo da poter utilizzare questa classe per installare nginx con qualsiasi configurazione.
È possibile farlo .
Attenzione: le variabili in Puppet sono immutabili!
Inoltre, è possibile accedere a una variabile solo dopo che è stata dichiarata, altrimenti il valore della variabile sarà undef.
Esempio di utilizzo delle variabili:
# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"In Puppet ci sono spazi dei nomi, e le variabili, di conseguenza, hanno visibilità: una variabile con lo stesso nome può essere definita in spazi dei nomi diversi. Durante la risoluzione del valore della variabile, questa viene cercata nello spazio dei nomi corrente, poi in quello contenitore, e così via.
Esempi di spazi dei nomi:
- globale — qui vanno a finire le variabili al di fuori della descrizione di una classe o di un nodo;
- spazio dei nomi del nodo nella descrizione del nodo;
- spazio dei nomi della classe nella descrizione della classe.
Per evitare ambiguità nell'accesso alla variabile, è possibile specificare lo spazio dei nomi nel nome della variabile:
# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::varAccordiamo che il percorso alla configurazione di nginx sia nella variabile $nginx_conf_source. Quindi la classe apparirà come segue:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => $nginx_conf_source, # qui usiamo la variabile invece di una stringa fissa
recure => true,
purge => true,
force => true,
}
~> service { 'nginx':
ensure => running,
enable => true,
}
}
node 'server2.testdomain' {
$nginx_conf_source = 'puppet:///modules/example/nginx-conf'
include nginx_example
}Tuttavia, l'esempio fornito è problematico perché esiste una certa "conoscenza segreta" riguardo al fatto che all'interno della classe venga utilizzata una variabile con un certo nome. È molto più corretto rendere questa conoscenza condivisa: le classi possono avere parametri.
Parametri della classe sono variabili nello spazio dei nomi della classe, vengono definiti nell'intestazione della classe e possono essere utilizzati come normali variabili nel corpo della classe. I valori dei parametri vengono specificati quando si utilizza la classe nel manifesto.
È possibile assegnare un valore predefinito a un parametro. Se un parametro non ha un valore predefinito e non viene fornito un valore durante l'utilizzo, si verificherà un errore di compilazione.
Parametrizziamo la classe dell'esempio sopra e aggiungiamo due parametri: il primo, obbligatorio, è il percorso alla configurazione, e il secondo, facoltativo, è il nome del pacchetto con nginx (in Debian, ad esempio, ci sono pacchetti nginx, nginx-light, nginx-full).
# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
$conf_source,
$package_name = 'nginx-light', # параметр со значением по умолчанию
) {
package { $package_name:
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => $conf_source,
recurse => true,
purge => true,
force => true,
}
~> service { 'nginx':
ensure => running,
enable => true,
}
}
node 'server2.testdomain' {
# если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
# *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
class { 'nginx_example':
conf_source => 'puppet:///modules/example/nginx-conf', # задаём параметры класса точно так же, как параметры для других ресурсов
}
}In Puppet le variabili sono tipizzate. Ci sono . I tipi di dati sono solitamente utilizzati per la validazione dei valori dei parametri passati a classi e definizioni. Se il parametro passato non corrisponde al tipo specificato, si verificherà un errore di compilazione.
Il tipo è scritto direttamente prima del nome del parametro:
class example (
String $param1,
Integer $param2,
Array $param3,
Hash $param4,
Hash[String, String] $param5,
) {
...
}Classi: include classname vs class{‘classname’:}
Ogni classe è una risorsa di tipo class. Come per qualsiasi altro tipo di risorsa, non possono esistere due istanze della stessa classe su un nodo.
Se provi ad aggiungere una classe allo stesso nodo due volte usando class { 'classname':} (indipendentemente dai parametri diversi o identici), ci sarà un errore di compilazione. Tuttavia, nel caso di utilizzo di una classe in stile risorsa, puoi immediatamente specificare tutti i suoi parametri nel manifesto.
Tuttavia, se utilizzi include, è possibile aggiungere la classe quante volte si desidera. Il punto è che include è una funzione idempotente che verifica se la classe è stata aggiunta alla directory. Se la classe non è presente nella directory, la aggiunge; se è già presente, non fa nulla. Ma nel caso di utilizzo di include non puoi specificare i parametri della classe durante la dichiarazione della classe: tutti i parametri obbligatori devono essere forniti da una fonte di dati esterna: Hiera o ENC. Ne parleremo nel prossimo articolo.
Definizioni
Come detto nel blocco precedente, la stessa classe non può essere presente su un nodo più di una volta. Tuttavia, in alcuni casi è necessario avere la possibilità di applicare lo stesso blocco di codice a un nodo con parametri diversi. In altre parole, c'è bisogno di un proprio tipo di risorsa.
Ad esempio, per installare un modulo PHP, noi di Avito facciamo quanto segue:
- Installiamo il pacchetto con questo modulo.
- Creiamo un file di configurazione per questo modulo.
- Creiamo un symlink per la configurazione di php-fpm.
- Creiamo un symlink per la configurazione di php cli.
In questi casi si utilizza una struttura come (define, defined type, defined resource type). Défini è simile a una classe, ma ci sono differenze: da un lato, ogni definizione è un tipo di risorsa e non una risorsa; dall'altro, ogni definizione ha un parametro implicito $title, dove finisce il nome della risorsa quando viene dichiarata. Proprio come nel caso delle classi, la definizione deve essere descritta prima di poterla utilizzare.
Esempio semplificato con un modulo per PHP:
define php74::module (
$php_module_name = $title,
$php_package_name = "php7.4-${title}",
$version = 'installed',
$priority = '20',
$data = "extension=${title}.son",
$php_module_path = 'etc/php/7.4/mods-available',
) {
package { $php_package_name:
ensure => $version,
install_options => ['-o', 'DPkg::NoTriggers=true'], # i trigger dei pacchetti php di Debian creano automaticamente dei symlink e riavviano il servizio php-fpm - non ne abbiamo bisogno, poiché gestiamo sia i symlink che il servizio con Puppet
}
-> file { "${php_module_path}/${php_module_name}.ini":
ensure => $ensure,
content => $data,
}
file { "etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
ensure => link,
target => "${php_module_path}/${php_module_name}.ini",
}
file { "etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
ensure => link,
target => "${php_module_path}/${php_module_name}.ini",
}
}
node server3.testdomain {
php74::module { 'sqlite3': }
php74::module { 'amqp': php_package_name => 'php-amqp' }
php74::module { 'msgpack': priority => '10' }
}Nella definizione, è più facile individuare l'errore di dichiarazione duplicata. Questo accade se nella definizione esiste una risorsa con un nome costante e su un nodo ci sono due o più istanze di quella definizione.
Proteggersi da questo è semplice: tutte le risorse all'interno della definizione devono avere un nome che dipende da $title. Come alternativa, si può optare per un'aggiunta idempotente delle risorse; nel caso più semplice è sufficiente estrarre le risorse comuni a tutte le istanze della definizione in una classe separata e includere questa classe nella definizione - la funzione include è idempotente.
Ci sono anche altri modi per raggiungere l'idempotenza nell'aggiunta di risorse, in particolare utilizzando le funzioni defined e ensure_resources, ma ne parlerò nella prossima serie.
Dipendenze e notifiche per classi e definizioni
Le classi e le definizioni aggiungono le seguenti regole al trattamento delle dipendenze e delle notifiche:
- una dipendenza da una classe/definizione aggiunge dipendenze a tutte le risorse della classe/definizione;
- una dipendenza da una classe/definizione aggiunge dipendenze a tutte le risorse della classe/definizione;
- una notifica da una classe/definizione notifica tutte le risorse della classe/definizione;
- l'iscrizione a una classe/definizione iscrive a tutte le risorse della classe/definizione.
Operatori condizionali e selettori
if
Qui è tutto semplice:
if ESPRESSIONE1 {
...
} elsif ESPRESSIONE2 {
...
} else {
...
}a_meno_che
unless è l'opposto di if: il blocco di codice verrà eseguito se l'espressione è falsa.
unless ESPRESSIONE {
...
}case
Anche qui non c'è niente di complicato. Come valori si possono utilizzare valori normali (stringhe, numeri e così via), espressioni regolari, così come tipi di dati.
case ESPRESSIONE {
VALORE1: { ... }
VALORE2, VALORE3: { ... }
default: { ... }
}Selettori
Un selettore è una costruzione linguistica simile a case, solo che invece di eseguire un blocco di codice restituisce un valore.
$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }Moduli
Quando la configurazione è piccola, può essere facilmente mantenuta in un unico manifesto. Ma più configurazioni descriviamo, più classi e nodi ci sono nel manifesto, esso cresce, rendendo il lavoro scomodo.
Inoltre, c'è un problema di riutilizzo del codice: quando tutto il codice è in un unico manifesto, è difficile condividerlo con altri. Per risolvere questi due problemi, in Puppet esiste un'entità chiamata moduli.
Moduli — sono insiemi di classi, definizioni e altre entità Puppet, collocate in una directory separata. In altre parole, un modulo è un pezzo indipendente di logica Puppet. Ad esempio, può esserci un modulo per lavorare con nginx, e in esso ci sarà solo ciò che è necessario per lavorare con nginx, e può esserci un modulo per lavorare con PHP, e così via.
I moduli sono versionati e supportano anche le dipendenze tra moduli. Esiste un repository pubblico di moduli — .
Sul puppet-server, i moduli si trovano nella sottodirectory modules della directory radice. All'interno di ogni modulo, c'è una schematica standard delle directory: manifests, files, templates, lib e così via.
Struttura dei file nel modulo
Nella root del modulo possono esserci le seguenti directory con nomi significativi:
manifests— contiene i manifestifiles— contiene i filetemplates— contiene i templatelib— contiene il codice Ruby
Questa non è una lista completa di directory e file, ma per questo articolo basta così.
Nomi delle risorse e nomi dei file nel modulo
Le risorse (classi, definizioni) in un modulo non possono essere chiamate a piacimento. Inoltre, c'è una corrispondenza diretta tra il nome della risorsa e il nome del file in cui Puppet cercherà la descrizione di quella risorsa. Se si violano le regole di denominazione, Puppet non troverà semplicemente la descrizione delle risorse, e si avrà un errore di compilazione.
Le regole sono semplici:
- Tutte le risorse nel modulo devono essere nel namespace del modulo. Se il modulo si chiama
foo, allora tutte le risorse in esso devono essere chiamatefoo::, oppure semplicementefoo. - Una risorsa con il nome del modulo deve essere nel file
init.pp. - Per le altre risorse, lo schema di denominazione dei file è il seguente:
- il prefisso con il nome del modulo viene omesso
- tutti i doppio punti, se presenti, vengono sostituiti da barre
- viene aggiunta una estensione
.pp
Mostrerò un esempio. Supponiamo che io stia scrivendo un modulo nginx. Esso contiene le seguenti risorse:
- classe
nginxdescritta nel manifestoinit.pp; - classe
nginx::servicedescritta nel manifestoservice.pp; - definire
nginx::serverdescritta nel manifestoserver.pp; - definire
nginx::server::locationdescritta nel manifestoserver/location.pp.
Modelli
Sicuramente sapete già cosa sono i modelli, non entrerò nei dettagli. Ma per ogni evenienza lascerò .
Come utilizzare i modelli: il valore del modello può essere espanso utilizzando la funzione template, alla quale viene passato il percorso del modello. Per le risorse di tipo file usiamo insieme al parametro content. Ad esempio, così:
file { '/tmp/example': content => template('modulename/templatename.erb')Un percorso del tipo / implica il file /modules//templates/.
Inoltre, c'è una funzione inline_template che riceve come input il testo del modello, e non il nome del file.
All'interno dei modelli è possibile utilizzare tutte le variabili Puppet nell'attuale ambito di visibilità.
Puppet supporta i modelli in formato ERB ed EPP:
In breve su ERB
Costruzioni di controllo:
<%= ВЫРАЖЕНИЕ %>— inserire il valore dell'espressione<% ВЫРАЖЕНИЕ %>— calcolare il valore dell'espressione (senza inserirlo). Qui vanno gli operatori condizionali normali (if), i cicli (each).<%# КОММЕНТАРИЙ %>
Le espressioni in ERB sono scritte in Ruby (infatti, ERB sta per Embedded Ruby).
Per accedere alle variabili dal manifesto è necessario aggiungere @ al nome della variabile. Per rimuovere il ritorno a capo che appare dopo la costruzione di controllo, è necessario utilizzare il tag di chiusura -%>.
Esempio di utilizzo di un modello
Supponiamo che io stia scrivendo un modulo per la gestione di ZooKeeper. La classe che si occupa della creazione della configurazione appare più o meno così:
class zookeeper::configure (
Array[String] $nodes,
Integer $port_client,
Integer $port_quorum,
Integer $port_leader,
Hash[String, Any] $properties,
String $datadir,
) {
file { '/etc/zookeeper/conf/zoo.cfg':
ensure => present,
content => template('zookeeper/zoo.cfg.erb'),
}
}E il corrispondente modello zoo.cfg.erb — è così:
0 -%>
server.=::;
dataDir=
=Fatti e variabili incorporate
Spesso, una parte specifica della configurazione dipende da ciò che sta accadendo al nodo in quel momento. Ad esempio, a seconda della versione di Debian installata, sarà necessario installare una versione specifica del pacchetto. Si può monitorare tutto ciò manualmente, riscrivendo i manifesti in caso di cambiamenti nei nodi. Ma questo è un approccio poco serio, l'automazione è di gran lunga migliore.
Per ottenere informazioni sui nodi, in Puppet esiste un meccanismo chiamato fatti. I fatti sono informazioni sul nodo, disponibili nei manifesti come normali variabili nello spazio dei nomi globale. Ad esempio, il nome dell'host, la versione del sistema operativo, l'architettura della CPU, l'elenco degli utenti, l'elenco delle interfacce di rete e dei loro indirizzi, e molto, molto altro. I fatti sono disponibili nei manifesti e nei modelli come normali variabili.
Esempio di lavoro con i fatti:
notify { "In esecuzione OS ${facts['os']['name']} versione ${facts['os']['release']['full']}": }
# la risorsa di tipo notify semplicemente scrive un messaggio nel logFormalmente parlando, un fatto ha un nome (stringa) e un valore (sono disponibili vari tipi: stringhe, array, dizionari). Ci sono . È anche possibile scrivere i propri. I raccoglitori di fatti sono descritti , oppure come . Inoltre, i fatti possono essere presentati in forma di su nodi.
Durante l'esecuzione, l'agente Puppet prima copia dal server Puppet sul nodo tutti i raccoglitori di fatti disponibili, dopo di che li esegue e invia al server i fatti raccolti; solo dopo, il server inizia la compilazione del catalogo.
Fatti come file eseguibili
Questi fatti vengono collocati nei moduli nella directory facts.d. Ovviamente, i file devono essere eseguibili. Durante l'esecuzione, devono stampare sull'output standard informazioni o in formato YAML, o in formato "chiave=valore".
Non dimenticate che i fatti si applicano a tutti i nodi che sono sotto il controllo del server Puppet, su cui viene distribuito il vostro modulo. Pertanto, nello script assicuratevi di controllare che nel sistema siano presenti tutti i programmi e i file necessari per il corretto funzionamento del vostro fatto.
#!/bin/sh
echo "testfact=success"#!/bin/sh
echo '{"testyamlfact":"success"}'Fatti in Ruby
Questi fatti vengono collocati nei moduli nella directory lib/facter.
# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
confine do
Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
end
confine do
File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
end
# в блоке setcode происходит собственно вычисление значения факта
setcode do
hash = {}
if (out = Facter::Core::Execution.execute('ladvdc -b'))
out.split.each do |l|
line = l.split('=')
next if line.length != 2
name, value = line
hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
end
end
hash # значение последнего выражения в блоке setcode является значением факта
end
endFatti di testo
Questi fatti vengono collocati sui nodi nella directory /etc/facter/facts.d nella vecchia versione di Puppet o /etc/puppetlabs/facts.d nella nuova versione di Puppet.
examplefact=examplevalue---
examplefact2: examplevalue2
anotherfact: anothervalueAccesso ai fatti
Puoi accedere ai fatti in due modi:
- tramite un dizionario
$facts:$facts['fqdn']; - utilizzando il nome del fatto come nome della variabile:
$fqdn.
È meglio utilizzare un dizionario $facts, e ancor meglio specificare lo spazio dei nomi globale ($::facts).
Variabili incorporate
Oltre ai fatti, ci sono anche , disponibili nello spazio dei nomi globale.
- fatti fidati — variabili che vengono prese dal certificato del cliente (poiché il certificato è solitamente emesso sul server Puppet, l'agente non può semplicemente cambiare il suo certificato, quindi le variabili sono "fidate": nome del certificato, nome host e dominio, estensioni del certificato.
- fatti del server — variabili relative alle informazioni sul server: versione, nome, indirizzo IP del server, ambiente.
- fatti dell'agente — variabili aggiunte direttamente dall'agente Puppet, non da Facter: nome del certificato, versione dell'agente, versione di Puppet.
- variabili del master — variabili del Puppet Master (sic!). Sono simili a quelle in fatti del server, più i valori dei parametri di configurazione sono disponibili.
- variabili del compilatore — variabili del compilatore, che variano in ogni ambito: nome del modulo corrente e nome del modulo in cui è stata effettuata la chiamata all'oggetto corrente. Possono essere utilizzate, ad esempio, per verificare che le tue classi private non siano utilizzate direttamente da altri moduli.
Supplemento 1: come eseguire e fare il debug di tutto questo?
Nell'articolo sono stati presentati molti esempi di codice Puppet, ma non si è parlato affatto di come eseguire questo codice. Beh, lo correggo.
Per far funzionare Puppet è sufficiente un agente, ma nella maggior parte dei casi sarà necessario anche un server.
Agente
Almeno dalla quinta versione, i pacchetti puppet-agent da contengono tutte le dipendenze (ruby e relative gemme), quindi non ci sono difficoltà con l'installazione (parlo di distribuzioni basate su Debian — non utilizziamo distribuzioni basate su RPM).
Nel caso più semplice, per applicare la configurazione di Puppet basta eseguire l'agente in modalità senza server: a condizione che il codice Puppet sia stato copiato sulla nod. puppet apply:
atikhonov@atikhonov ~\/puppet-test $ cat helloworld.pp \nnode default {\n notify { 'Hello world!': }\n}\natikhonov@atikhonov ~\/puppet-test $ puppet apply helloworld.pp \nNotice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds\nNotice: Hello world!\nNotice: \/Stage[main]\/Main\/Node[default]\/Notify[Hello world!]\/message: defined 'message' as 'Hello world!'\nNotice: Applied catalog in 0.01 secondsMeglio, ovviamente, sollevare il server e far girare gli agenti sui nodi in modalità demone: in questo modo, ogni mezz'ora applicheranno la configurazione scaricata dal server.
Si può simulare un modello di lavoro push: collegarsi al nodo di interesse e avviarlo sudo puppet agent -t. L'opzione -t (--test) in realtà attiva diverse opzioni, che possono essere abilitate singolarmente. Tra queste opzioni ci sono:
- non lavorare in modalità demone (per impostazione predefinita, l'agente viene avviato in modalità demone);
- terminare dopo aver applicato il catalogo (di default, l'agente continuerà a funzionare e applicherà la configurazione ogni mezz'ora);
- scrivere un log dettagliato delle operazioni;
- mostrare le modifiche nei file.
L'agente ha una modalità di lavoro senza modifiche: può essere utilizzato quando non si è certi di aver scritto una configurazione corretta e si desidera verificare quali cambiamenti apporterà l'agente durante l'operazione. Questa modalità si attiva con il parametro --noop nella riga di comando: sudo puppet agent -t --noop.
Inoltre, si può attivare il log di debug: in esso puppet registra tutte le azioni che compie: la risorsa che sta attualmente elaborando, i parametri di quella risorsa e quali programmi vengono eseguiti. Ovviamente, questo è il parametro --debug.
Server
Non tratterò in questo articolo la configurazione completa del puppet server e il deploy del codice su di esso, dirò solo che viene installata di default una versione funzionante del server, che non richiede configurazioni aggiuntive per funzionare con un numero ridotto di nodi (diciamo fino a cento). Un numero maggiore di nodi richiederà già un tuning: per impostazione predefinita, puppetserver avvia al massimo quattro worker, per una maggiore produttività è necessario aumentare il loro numero e non dimenticare di aumentare i limiti di memoria, altrimenti il server passerà la maggior parte del tempo a fare garbage collect.
Il deploy del codice — se si ha bisogno di qualcosa di veloce e semplice, guardate (su r10k)[], per piccole installazioni dovrebbe andar bene.
Appendice 2: raccomandazioni per la scrittura del codice
- Concentrate tutta la logica in classi e definizioni.
- Mantieni le classi e le definizioni nei moduli, non nei manifesti con la descrizione dei nodi.
- Usa i fatti.
- Non fare if basati sui nomi host.
- Non esitare ad aggiungere parametri per classi e definizioni: è meglio che avere logica implicita nascosta nel corpo della classe/definizione.
E perché raccomando di fare così, lo spiegherò nel prossimo articolo.
Conclusione
Terminiamo qui con l'introduzione. Nel prossimo articolo parlerò di Hiera, ENC e PuppetDB.
Solo gli utenti registrati possono partecipare al sondaggio. , per favore.
In realtà ci sarebbe molto più materiale, posso scrivere articoli sui seguenti temi, votate su cosa vi piacerebbe leggere:
- 59,1%Costrutti Puppet avanzati — alcune cose di livello successivo: cicli, mapping e altre espressioni lambda, collezioni di risorse, risorse esportabili e interazione tra host tramite Puppet, tag, provider, tipi di dati astratti.
- 31,8%«Io sono il sysadmin di mamma» o come abbiamo integrato diversi server Puppet di versioni differenti in Avito, e in generale una parte sull'amministrazione del server Puppet.
- 81,8%Come scriviamo il codice Puppet: insieme di strumenti, documentazione, test, CI/CD.
22 utenti hanno votato. 9 utenti si sono astenuti.
Fonte: habr.com
