Introduzione a Puppet

Puppet è un sistema di gestione della configurazione. Viene utilizzato per portare gli host allo stato desiderato e mantenerlo.

Lavoro con Puppet da più di cinque anni. Questo testo è essenzialmente una compilazione tradotta e riorganizzata dei punti chiave dalla documentazione ufficiale, che permetterà ai principianti di comprendere rapidamente il funzionamento di Puppet.

Introduzione a Puppet

Informazioni di base

Il funzionamento di Puppet è basato su un'architettura client-server, anche se è supportata una modalità senza server con funzionalità limitate.

Utilizza un modello di lavoro pull: per impostazione predefinita, ogni trenta minuti i client si rivolgono al server per ottenere la configurazione e applicarla. Se hai lavorato con Ansible, utilizza un modello diverso, quello push: l'amministratore avvia il processo di applicazione della configurazione, i client non applicheranno nulla da soli.

Nell'interazione di rete, viene utilizzata la crittografia TLS bidirezionale: sia il server che il client dispongono delle proprie chiavi private e dei rispettivi certificati. Di solito, il server emette certificati per i client, ma è comunque possibile utilizzare una CA esterna.

Introduzione ai manifesti

Nella terminologia di Puppet si collegano al server Puppet i nodi (nodes). La configurazione per i nodi è scritta nei manifesti in un linguaggio di programmazione specifico — Puppet DSL. Puppet DSL è un linguaggio dichiarativo. In esso si descrive lo stato desiderato di un nodo sotto forma di dichiarazioni di risorse singole, ad esempio:

Il file esiste e ha un contenuto specifico.

  • Il pacchetto è installato.
  • Il servizio è attivo.
  • Le risorse possono essere interconnesse:

Ci sono dipendenze che influenzano l'ordine di applicazione delle risorse.

  • Ad esempio, «prima installa il pacchetto, poi modifica il file di configurazione, infine avvia il servizio».
    Ci sono notifiche — se una risorsa cambia, essa invia notifiche alle risorse iscritte.
  • Ad esempio, se il file di configurazione cambia, è possibile riavviare automaticamente il servizio.
    Inoltre, in Puppet DSL ci sono funzioni e variabili, così come operatori condizionali e selettori. Sono supportati anche diversi meccanismi di templating — EPP e ERB.

Puppet è scritto in Ruby, quindi molte costruzioni e termini provengono da lì. Ruby consente di estendere Puppet — 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, aggiungendo logica complessa, nuovi tipi di risorse e funzioni.

Durante l'esecuzione di Puppet, i manifesti per ogni specifico nodo sul server vengono compilati nella directory. Directory è un elenco di risorse e delle loro interrelazioni dopo il calcolo dei valori delle funzioni, delle variabili e l'espansione degli operatori condizionali.

Sintassi e stile del codice

Ecco le sezioni della documentazione ufficiale che possono aiutarti a comprendere la sintassi, nel caso in cui gli esempi forniti non siano 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,
  }
}

Le indentazioni e i ritorni a capo non sono una parte obbligatoria del manifesto, ma esiste un style guide. In sintesi:

  • Viene utilizzato un doppio spazio in indentazione, non si usano tabulazioni.
  • Le parentesi graffe sono separate da uno spazio, i due punti non sono separati da uno spazio.
  • Le virgole sono presenti dopo ogni parametro, incluso l'ultimo. Ogni parametro deve essere su una nuova riga. Ci sono eccezioni per i casi senza parametri e con un solo parametro: possono essere scritti su una sola riga e senza virgola (cioè, resource { 'title': } e resource { 'title': param => value }).
  • Le frecce dei parametri devono essere allineate allo stesso livello.
  • Le frecce della relazione delle risorse sono scritte prima di esse.

Posizionamento dei file sul puppet server

Per ulteriori spiegazioni, introdurrò il concetto di «directory radice». La directory radice è la directory in cui si trova la configurazione Puppet per una specifica nodo.

La directory radice varia a seconda della versione di Puppet e dell’utilizzo degli ambienti. Gli ambienti sono set di configurazione indipendenti, conservati in directory separate. Solitamente vengono utilizzati in combinazione con Git, in tal caso gli ambienti vengono creati da rami Git. Di conseguenza, ogni nodo si trova in un ambiente specifico. Questo è impostato sul nodo stesso, 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 è facoltativo: ad esempio, noi non li utilizziamo con il vecchio Puppet. Se gli ambienti sono utilizzati, di solito sono conservati in /etc/puppet/environments, la directory radice sarà la directory dell’ambiente. Se gli ambienti non sono utilizzati, 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 trasferita in /etc/puppetlabs/code. Di conseguenza, gli ambienti sono memorizzati in /etc/puppetlabs/code/environments, la directory radice è la directory dell'ambiente.

Nella directory radice deve esserci una sottodirectory manifests, nella quale si trovano uno o più manifesti con la descrizione dei nodi. Inoltre, deve esserci una sottodirectory modules, nella quale si trovano i moduli. Cosa sono i moduli, lo spiegherò più avanti. Inoltre, nel vecchio Puppet potrebbe anche esserci una sottodirectory files, nella quale si trovano vari file che copiamo sui nodi. Nel nuovo Puppet, tutti i file sono stati spostati nei moduli.

I file manifesto hanno estensione .pp.

Una coppia di esempi pratici

Descrizione del nodo e del suo risorsa

Sul 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':   # descrizione del 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), affinché altrimenti un numero con 0 all'inizio venga interpretato come scritto in base ottale, e tutto andrà male
    }
}

Relazioni delle risorse sul nodo

Sul nodo server2.testdomain deve essere avviato nginx, funzionante con una configurazione predefinita.

Decomponiamo il compito:

  • È necessario installare il pacchetto nginx.
  • È necessario copiare i file di configurazione dal server.
  • È necessario avviare il servizio nginx.
  • In caso di aggiornamento della configurazione, è necessario riavviare il servizio.

Scriviamo il manifesto:

node 'server2.testdomain' {   # configurazione del 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 sopra descritta.
  # Tali dipendenze sono transitive.
    -> file { '/etc/nginx':   # descriviamo il file /etc/nginx
        ensure  => directory,   # deve essere una directory
        source  => 'puppet:///modules/example/nginx-conf',   # il suo contenuto deve essere prelevato dal server puppet all'indirizzo specificato
        recurse => true,   # copiare i file in modo ricorsivo
        purge   => true,   # è necessario rimuovere i file superflui (quelli non presenti nella fonte)
        force   => true,   # rimuovere le directory superflue
    }
  # La freccia ondulata (~&) indica che la risorsa sottostante deve
  # iscriversi ai cambiamenti della risorsa sopra descritta.
  # La freccia ondulata include la freccia 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.
}

Per far funzionare tutto ciò, è necessaria una configurazione dei file sul server Puppet simile a questa:

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Tipi di risorse

L'elenco completo dei tipi di risorse supportati si trova nella documentazione, qui descriverò cinque tipi di base, che nella mia esperienza sono sufficienti per risolvere la maggior parte dei problemi.

file

Gestisce file, directory, collegamenti simbolici, il loro contenuto e i permessi di accesso.

Parametri:

  • nome della risorsa — percorso del file (opzionale)
  • path — percorso del file (se non specificato nel nome)
  • ensure — tipo di file:
    • absent — rimuovi file
    • present — deve essere un file di qualsiasi tipo (se il file non esiste, verrà creato un file normale)
    • file — file normale
    • directory — directory
    • link — collegamento simbolico
  • content — contenuto del file (adatto solo per file normali, non può essere utilizzato con source o target)
  • source — collegamento al percorso da cui copiare il contenuto del file (non può essere usato con content o target). Può essere specificato come URI con schema puppet: (in tal caso verranno utilizzati file dal server Puppet), ma anche con schema http: (spero sia chiaro cosa accadrà in questo caso), e anche con schema file: o come percorso assoluto senza schema (in tal caso verrà utilizzato un file dal filesystem locale sul nodo)
  • target — dove deve puntare il symlink (non può essere usato con content o source)
  • proprietario — utente a cui deve appartenere il file
  • gruppo — gruppo a cui deve appartenere il file
  • mode — permessi sul file (in forma di stringa)
  • ricorsivo — include l'elaborazione ricorsiva delle directory
  • purga — include la rimozione di file non descritti in Puppet
  • forza — include la rimozione di directory non descritte in Puppet

pacchetto

Installa e rimuove pacchetti. Gestisce le 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 — installata qualsiasi versione
    • latest — installata l'ultima versione
    • absent — rimosso (apt-get remove)
    • ripulito — rimosso insieme ai file di configurazione (apt-get purge)
    • bloccato — versione del pacchetto bloccata (apt-mark hold)
    • qualsiasi altra stringa — installata la versione specificata
  • reinstalla_su_aggiornamento — se true, quindi, al ricevimento della notifica, il pacchetto sarà reinstallato. Utile per le distribuzioni basate su source, dove può essere necessaria la ricompilazione dei pacchetti quando si modificano i parametri di build. Per impostazione predefinita false.

service

Gestisce i servizi. È in grado di elaborare le notifiche — riavvia il servizio.

Parametri:

  • nome della risorsa — il servizio da gestire (opzionale)
  • name — il servizio da gestire (se non specificato nel nome)
  • ensure — lo stato desiderato del servizio:
    • correre — avviato
    • stopped — fermo
  • enable — gestisce la possibilità di avviare il servizio:
    • true — avviato automaticamente (systemctl enable)
    • mask — mascherato (systemctl mask)
    • false — disabilitato all'avvio automatico (systemctl disable)
  • restart — comando per riavviare il servizio
  • status — comando per controllare lo stato del servizio
  • hasrestart — indicare se lo script di init del servizio supporta il riavvio. Se false è specificato il parametro restart — viene utilizzato il valore di questo parametro. Se false e il parametro restart non è specificato — il servizio si ferma e viene avviato di nuovo per il riavvio (ma in systemd viene utilizzato il comando systemctl restart).
  • hasstatus — indicare se lo script di init del servizio supporta il comando status. Se false, quindi viene utilizzato il valore del parametro status. Per impostazione predefinita true.

exec

Avvia comandi esterni. Se non vengono specificati parametri crea, solo_se, a_meno_che o aggiorna_solo, il comando verrà eseguito ad ogni esecuzione di Pappetta. È in grado di gestire notifiche — avvia il comando.

Parametri:

  • nome della risorsa — comando da eseguire (opzionale)
  • comando — comando da eseguire (se non specificato nel nome)
  • path — percorsi in cui cercare il file eseguibile
  • solo_se — se il comando specificato in questo parametro termina con codice di uscita zero, il comando principale verrà eseguito
  • a_meno_che — se il comando specificato 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
  • aggiorna_solo — se true, il comando verrà avviato solo se questo exec riceve notifiche da altre risorse
  • cwd — directory da cui eseguire il comando
  • utente — utente da cui eseguire il comando
  • fornitore — con cosa eseguire il comando:
    • posix — crea semplicemente un processo figlio, è necessario specificare path
    • shell — il comando viene eseguito in shell /bin/sh, non è necessario specificare path, si possono usare globbing, pipe e altre funzionalità della shell. Di solito viene determinato automaticamente se ci sono vari caratteri speciali (|, ;, &&, || e così via).

cron

Gestisce i cron job.

Parametri:

  • nome della risorsa — semplicemente un identificatore
  • ensure — stato del cron job:
    • present — creare, se non esiste
    • absent — eliminare, se esiste
  • comando — quale comando eseguire
  • environment — in quale ambiente eseguire il comando (lista delle variabili d'ambiente e i loro valori separati da =)
  • utente — da quale utente eseguire il comando
  • minuto, ora, giorno della settimana, mese, giorno del mese — quando eseguire cron. Se uno di questi attributi non è specificato, il suo valore nella crontab sarà *.

In Puppet 6.0 cron come se fosse rimosso dalla scatola in puppetserver, quindi non c'è documentazione sul sito generale. Ma è disponibile nella scatola in puppet-agent, quindi non è necessario installarlo separatamente. La documentazione su di esso può essere vista nella documentazione della quinta versione di Puppet, oppure su GitHub.

Riguardo alle risorse in generale

Requisiti per la unicità delle risorse

L'errore più comune che incontriamo è Dichiarazione duplicata. Questo errore si verifica quando nella directory entrano due o più risorse dello stesso tipo con lo stesso nome.

Quindi ripeterò ancora: nei manifesti per una singola unità non dovrebbero esserci risorse dello stesso tipo con lo stesso nome (title)!

A volte è necessario installare pacchetti con lo stesso nome, ma con diversi gestori di pacchetti. In tal caso, bisogna utilizzare il parametro name, per evitare errori:

package { 'ruby-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'gem',
}
package { 'python-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'pip',
}

Negli altri tipi di risorse ci sono parametri simili che aiutano a evitare duplicazioni, — name a service, comando a exec, e così via.

Metaparametri

Alcuni parametri speciali sono presenti per ogni tipo di risorsa, indipendentemente dalla sua entità.

Elenco completo dei metaparametri nella documentazione di Puppet.

Elenco breve:

  • require — in questo parametro viene specificato da quali risorse dipende questa risorsa.
  • before — in questo parametro viene indicato quali risorse dipendono da questa risorsa.
  • subscribe — in questo parametro si specifica da quali risorse la presente risorsa riceve notifiche.
  • notify — in questo parametro si indica quali risorse ricevono notifiche da questa risorsa.

Tutti i metaparamentri elencati accettano o un singolo riferimento a una risorsa, o un array di riferimenti racchiusi tra parentesi quadre.

Riferimenti alle risorse

Un riferimento a una risorsa è semplicemente un'annotazione della risorsa stessa. Viene usato principalmente per indicare dipendenze. Un riferimento a una risorsa inesistente genererà un errore di compilazione.

La sintassi per il riferimento è la seguente: tipo di risorsa con la lettera maiuscola (se il nome del tipo contiene due punti, ogni parte del nome tra i due punti deve essere scritta con la lettera maiuscola), seguito tra parentesi quadre dal nome della risorsa (il case del nome non deve essere cambiato!). Non devono esserci spazi, le parentesi quadre devono essere scritte immediatamente dopo il nome del tipo.

Esempio:

file { '/file1': ensure => present }
file { '/file2':
  ensure => directory,
  before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']

Dipendenze e notifiche

La documentazione è qui.

Come già detto in precedenza, le semplici dipendenze tra risorse sono transitiva. Attenzione a impostare le dipendenze — è possibile 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 al momento dell'aggiornamento dipendono dal tipo di risorsa — exec esegue un comando, service riavvia il servizio, pacchetto reinstalla il pacchetto. Se non è definita un'azione di aggiornamento per la risorsa, non accade nulla.
  • In un singolo ciclo di Puppet, una risorsa può essere aggiornata al massimo una volta. Questo è possibile poiché le notifiche includono le dipendenze e il grafo delle dipendenze non contiene cicli.
  • Se Puppet cambia lo stato di una risorsa, la risorsa invia notifiche a tutte le risorse che vi sono iscritte.
  • Se la risorsa viene aggiornata, invia notifiche a tutte le risorse che vi sono iscritte.

Gestione dei parametri non specificati

In generale, se un parametro di una risorsa non ha un valore predefinito e questo parametro non è specificato nel manifesto, Puppet non cambierà questa proprietà per la corrispondente risorsa nel nodo. Ad esempio, se per una risorsa di tipo file non è specificato il parametro proprietario, Puppet non cambierà il proprietario del file corrispondente.

Introduzione a classi, variabili e definiti

Supponiamo di avere più nodi che condividono una parte della configurazione, ma presentano anche delle differenze, altrimenti potremmo descrivere tutto in un unico blocco. node {}. Certo, si potrebbe semplicemente copiare le parti identiche della configurazione, ma in generale si tratta di una cattiva soluzione: la configurazione si espande e, quando si modifica la parte comune, è necessario correggere la stessa cosa in molti luoghi. È facile commettere errori e, in ogni caso, il principio DRY (don’t repeat yourself) non è stato ideato a caso.

Per risolvere questo problema, esiste una costrutto chiamata classe.

Classi

Classe è un blocco di codice Puppet nominato. Le classi servono per il riutilizzo del codice.

Prima di tutto, bisogna descrivere la classe. La sola descrizione non aggiunge risorse da nessuna parte. La classe è descritta nei manifesti:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

Dopo di che, la classe può essere utilizzata:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Esempio dal compito precedente: estraiamo l'installazione e configurazione di nginx in una classe:

classe nginx_example {
    pacchetto { 'nginx':
        assicurati => installato,
    }
    -> file { '/etc/nginx':
        assicurati => directory,
        origine => 'puppet:///modules/example/nginx-conf',
        ricorsivo => true,
        purga => true,
        forza => true,
    }
    ~> servizio { 'nginx':
        assicurati => in esecuzione,
        abilita => true,
    }
}

node 'server2.testdomain' {
    includi nginx_example
}

Variabili

La classe dell'esempio precedente non è affatto flessibile, poiché porta sempre la stessa configurazione di nginx. Rendiamo il percorso della configurazione una variabile, in modo che questa classe possa essere utilizzata per installare nginx con qualsiasi configurazione.

Questo può essere fatto utilizzando variabili.

Attenzione: le variabili in Puppet sono immutabili!

Inoltre, una variabile può essere utilizzata solo dopo essere stata dichiarata, altrimenti il suo valore 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 hanno quindi ambito: una variabile con lo stesso nome può essere definita in spazi dei nomi diversi. Quando si risolve il valore di una variabile, questa viene cercata nello spazio dei nomi corrente, poi nel genitore, e così via.

Esempi di spazi dei nomi:

  • globale — qui si trovano variabili al di fuori della descrizione della classe o del 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::var

Stabiliamo che il percorso per la configurazione di nginx si trovi nella variabile $nginx_conf_source. Allora la classe apparirà come segue:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # qui usiamo una 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 è scadente in quanto c'è una sorta di "conoscenza segreta" che all'interno della classe viene utilizzata una variabile con un certo nome. È molto più corretto rendere questa conoscenza comune — le classi possono avere parametri.

Parametri della classe — sono variabili nello spazio dei nomi della classe, definite nell'intestazione della classe e possono essere utilizzate come normali variabili nel corpo della classe. I valori dei parametri vengono specificati quando si utilizza la classe nel manifesto.

Un parametro può avere un valore predefinito. Se un parametro non ha un valore predefinito e non viene fornito un valore durante l'uso, ciò causerà un errore di compilazione.

Parametrizziamo la classe dell'esempio precedente e aggiungiamo due parametri: il primo, obbligatorio, è il percorso della 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 molti tipi di dati. I tipi di dati vengono generalmente utilizzati per la convalida dei valori dei parametri passati a classi e definizioni. Se il parametro fornito non corrisponde al tipo specificato, si verificherà un errore di compilazione.

Il tipo viene scritto immediatamente 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 altri tipi di risorse, non possono esserci due istanze della stessa classe su un nodo.

Se si prova ad aggiungere la classe allo stesso nodo due volte usando class { 'classname':} (indipendentemente dai parametri, che siano diversi o uguali), si verificherà un errore di compilazione. Tuttavia, nel caso di utilizzo della classe nello stile di una risorsa, è possibile specificare tutti i suoi parametri direttamente nel manifesto.

Tuttavia, se si utilizza include, si può aggiungere la classe quante volte si desidera. Il punto è che include è una funzione idempotente che controlla se la classe è già presente nel catalogo. Se la classe non è presente, viene aggiunta; se è già presente, non succede nulla. Ma nel caso di utilizzo di include non è possibile specificare i parametri della classe al momento della dichiarazione della classe — tutti i parametri obbligatori devono essere forniti in una sorgente dati esterna — Hiera o ENC. Di questo ne parleremo nel prossimo articolo.

Definizioni

Come detto nel blocco precedente, lo stesso tipo 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 con parametri diversi su un nodo. In altre parole, c'è bisogno di un tipo di risorsa personalizzato.

Ad esempio, per installare un modulo PHP, noi in Avito facciamo quanto segue:

  1. Installiamo il pacchetto con questo modulo.
  2. Creiamo un file di configurazione per questo modulo.
  3. Creiamo un symlink per la configurazione di php-fpm.
  4. Creiamo un symlink per la configurazione di php cli.

In questi casi viene utilizzata una costruzione come define (type definito, tipo di risorsa definita). Il define è simile a una classe, ma ci sono delle differenze: in primo luogo, ogni define è un tipo di risorsa, non una risorsa; in secondo luogo, ogni define ha un parametro implicito $title, dove finisce il nome della risorsa al momento della dichiarazione. Così come nel caso delle classi, un define deve prima essere descritto, dopodiché può essere utilizzato.

Ecco un esempio semplificato con un modulo per PHP:

definisci php74::modulo (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installato',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = '/etc/php/7.4/mods-available',
) {
  pacchetto { $php_package_name:
    assicurati          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # i trigger dei pacchetti php debian creano automaticamente symlink e riavviano il servizio php-fpm - non ci serve, poiché gestiamo sia i symlink che il servizio tramite Puppet
  }
  -> file { "${php_module_path}/${php_module_name}.ini":
    assicurati  => $ensure,
    contenuto => $data,
  }
  file { "/etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    assicurati  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
  file { "/etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    assicurati  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
}

nodo server3.testdomain {
  php74::modulo { 'sqlite3': }
  php74::modulo { 'amqp': php_package_name => 'php-amqp' }
  php74::modulo { 'msgpack': priority => '10' }
}

È più semplice catturare un errore di dichiarazione duplicata nel definire. Ciò accade se nel definire c'è una risorsa con un nome costante, e su un certo nodo ci sono due o più istanze di quel definire.

Proteggersi da questo è semplice: tutte le risorse all'interno del definire devono avere un nome che dipende da $title. Come alternativa, si può utilizzare un'aggiunta idempotente delle risorse; nel caso più semplice, è sufficiente estrarre le risorse comuni a tutte le istanze in una classe separata e includere questa classe nella definizione — funzione include è idempotente.

Ci sono anche altri modi per raggiungere l'idempotenza durante l'aggiunta delle risorse, come l'uso di funzioni definito e assicurati_di_risorse, ma ne parlerò nella prossima serie.

Dipendenze e notifiche per classi e definizioni

Le classi e le definizioni aggiungono le seguenti regole per la gestione delle dipendenze e delle notifiche:

  • una dipendenza da una classe/è definita aggiunge dipendenze da 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

La documentazione è qui.

in etcdhelper, che non modificherà il servizio kube-dns.

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'è nulla di complicato. Come valori puoi usare valori normali (stringhe, numeri e così via), espressioni regolari e anche 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, è facile tenerla in un unico manifesto. Ma più grande è la configurazione che descriviamo, più classi e nodi compaiono nel manifesto, ed esso cresce, rendendo il lavoro con esso scomodo.

Inoltre, c'è il problema del riutilizzo del codice: quando tutto il codice è in un unico manifesto, diventa difficile condividerlo con altri. Per risolvere questi due problemi, in Puppet esiste un'entità chiamata moduli.

Moduli — è un insieme di classi, definiti e altre entità Puppet, estratte 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, che conterrà solo ciò che serve per lavorare con nginx, oppure può esserci un modulo per PHP, e così via.

I moduli sono versionati e supportano anche le dipendenze tra moduli. Esiste un repository aperto di moduli — Puppet Forge.

Sul server Puppet, i moduli si trovano nella sottodirectory modules della directory radice. All'interno di ogni modulo, c'è uno schema di directory standard: manifests, files, templates, lib e così via.

Struttura dei file in un modulo

Nella radice del modulo possono esserci le seguenti directory con nomi significativi:

  • manifests — contiene i manifesti
  • files — contiene i file
  • templates — contiene i template
  • lib — contiene codice Ruby

Questa non è una lista completa di directory e file, ma è sufficiente per questo articolo.

Nomi delle risorse e nomi dei file nel modulo

La documentazione è qui.

Le risorse (classi, definizioni) in un modulo non possono essere chiamate a piacere. Inoltre, esiste una corrispondenza diretta tra il nome della risorsa e il nome del file in cui Puppet cercherà la descrizione di questa risorsa. Se vengono violate 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 in un modulo devono trovarsi nel namespace del modulo. Se il modulo si chiama foo, tutte le risorse al suo interno devono essere chiamate foo::, oppure semplicemente foo.
  • La risorsa con il nome del modulo deve trovarsi nel file init.pp.
  • Per le altre risorse, lo schema di denominazione dei file è il seguente:
    • il prefisso con il nome del modulo viene scartato
    • tutti i doppi due punti, se presenti, vengono sostituiti da barre oblique
    • si aggiunge l'estensione .pp

Lo dimostrerò con un esempio. Supponiamo di scrivere un modulo nginx. Esso contiene le seguenti risorse:

  • classe nginx definita nel manifesto init.pp;
  • classe nginx::service definita nel manifesto service.pp;
  • define nginx::server definita nel manifesto server.pp;
  • define nginx::server::location definita nel manifesto server/location.pp.

Modelli

Sicuramente sapete già cosa sono i modelli, non entrerò nei dettagli qui. Ma per ogni evenienza lascerò un link a Wikipedia.

Come usare i modelli: il valore del modello può essere rivelato tramite la funzione template, che passa il percorso del modello. Per le risorse di tipo file utilizzato insieme al parametro content. Ad esempio, così:

file { '/tmp/example': content => template('modulename/templatename.erb')

Il percorso del tipo / implica il file /modules//templates/.

Inoltre, c'è la funzione inline_template — a cui si passa il testo del modello, non il nome del file.

All'interno dei modelli è possibile utilizzare tutte le variabili Puppet nell'attuale ambito.

Puppet supporta modelli in formato ERB ed EPP:

In breve su ERB

Costrutti di controllo:

  • <%= ВЫРАЖЕНИЕ %> — inserire il valore dell'espressione
  • <% ВЫРАЖЕНИЕ %> — calcolare il valore dell'espressione (senza inserirlo). Qui vanno i normali operatori condizionali (if), cicli (each).
  • <%# КОММЕНТАРИЙ %>

Le espressioni in ERB sono scritte in Ruby (in effetti, ERB è Embedded Ruby).

Per accedere alle variabili del manifesto, è necessario aggiungere @ al nome della variabile. Per rimuovere la nuova riga che appare dopo il costrutto di controllo, è necessario usare il tag di chiusura -%>.

Esempio di utilizzo del modello

Immagina di scrivere un modulo per gestire ZooKeeper. La classe responsabile 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 attualmente nel nodo. Ad esempio, a seconda della versione di Debian installata, è necessario installare una certa versione del pacchetto. È possibile monitorare tutto ciò manualmente, riscrivendo i manifesti in caso di modifiche nel nodo. Ma questo è un approccio poco serio, l'automazione è di gran lunga migliore.

Per ottenere informazioni sui nodi, in Puppet esiste un meccanismo chiamato fatti. Fatti — queste 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 i loro indirizzi, e molto, molto altro. I fatti sono disponibili nei manifesti e nei modelli come normali variabili.

Esempio di utilizzo dei fatti:

notify { "Esecuzione del sistema operativo ${facts['os']['name']} versione ${facts['os']['release']['full']}" : }
# La risorsa di tipo notify semplicemente scrive un messaggio nel log

Se vogliamo essere formali, un fatto ha un nome (stringa) e un valore (varie tipologie disponibili: stringhe, array, dizionari). Ci sono un insieme di fatti integrati. È anche possibile scrivere i propri. I raccoglitori di fatti sono descritti come funzioni in Ruby, o come eseguibili. Inoltre, i fatti possono essere presentati come file di testo con dati sugli nodi.

Durante il funzionamento, l'agente Puppet copia inizialmente dal server Puppet sul nodo tutti i raccoglitori di fatti disponibili, quindi li avvia e invia i fatti raccolti al server; solo dopo il server inizia la compilazione del catalogo.

Fatti come file eseguibili

Fatti di questo tipo sono collocati nei moduli nella directory facts.d. Ovviamente, i file devono essere eseguibili. Quando vengono eseguiti, devono stampare sull'output standard informazioni in formato YAML o nel formato "chiave=valore".

Non dimenticate che i fatti si estendono a tutti i nodi gestiti dal server Puppet su cui viene distribuito il vostro modulo. Pertanto, nel vostro script, assicuratevi di controllare che nel sistema siano presenti tutti i programmi e file necessari per il corretto funzionamento del vostro fatto.

#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'

Fatti in Ruby

Fatti di questo tipo sono 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
end

Fatti testuali

Fatti di questo tipo sono 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: anothervalue

Accesso ai fatti

È possibile accedere ai fatti in due modi:

  • attraverso il dizionario $facts: $facts['fqdn'];
  • utilizzando il nome del fatto come nome della variabile: $fqdn.

È meglio usare il dizionario $facts, e ancor meglio specificare lo spazio dei nomi globale ($::facts).

Ecco la sezione pertinente della documentazione.

Variabili incorporate

Oltre ai fatti, ci sono anche alcune variabili, disponibili nello spazio dei nomi globale.

  • fatti attendibili — variabili estratte dal certificato del cliente (poiché il certificato è solitamente emesso sul server Puppet, l'agente non può semplicemente cambiare il suo certificato; quindi le variabili e le "affidabili" sono: 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 dal puppet-agent, e non dal facter: nome del certificato, versione dell'agente, versione di Puppet.
  • variabili del master — variabili del Puppet Master (sic!). Qui ci sono più o meno le stesse informazioni che in fatti del server, più sono disponibili i valori dei parametri di configurazione.
  • variabili del compilatore — variabili del compilatore, che variano in ogni ambito di visibilità: nome del modulo corrente e nome del modulo in cui è stata fatta la chiamata all'oggetto corrente. Possono essere utilizzate, ad esempio, per verificare che le vostre classi private non siano utilizzate direttamente da altri moduli.

Supplemento 1: come eseguire e fare debug di tutto questo?

Nell'articolo ci sono stati molti esempi di codice Puppet, ma non è stato affatto spiegato come eseguire questo codice. Bene, mi scuso.

Per utilizzare Puppet è sufficiente un agente, ma nella maggior parte dei casi sarà necessario anche un server.

Agente

A partire dalla quinta versione, i pacchetti puppet-agent da repository ufficiale di Puppetlabs contengono tutte le dipendenze (ruby e gem corrispondenti), quindi non ci sono difficoltà nell'installazione (sto parlando di distribuzioni basate su Debian — non utilizziamo distribuzioni basate su RPM).

Nel caso più semplice, per applicare una configurazione puppet è sufficiente eseguire l'agente in modalità senza server: a condizione che il codice puppet sia copiato sulla nodi, lanci puppet apply:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds

È meglio, ovviamente, avviare un server e far girare gli agenti sulle nodi in modalità demone — in questo modo ogni mezz'ora applicheranno la configurazione scaricata dal server.

È possibile simulare un modello di lavoro push — accedere alla nodi di interesse e avviare sudo puppet agent -t. La chiave -t (--test) include in realtà diverse opzioni, che possono essere attivate anche singolarmente. Tra queste opzioni ci sono le seguenti:

  • non operare in modalità demone (di default l'agente viene avviato in modalità demone);
  • terminare l'esecuzione dopo l'applicazione del catalogo (di default l'agente continuerà a funzionare e applicherà la configurazione ogni trenta minuti);
  • scrivere un log dettagliato delle operazioni;
  • mostrare le modifiche nei file.

L'agente ha una modalità di funzionamento senza modifiche — può essere utilizzato nel caso in cui non si sia certi di aver scritto una configurazione corretta e si voglia verificare cosa cambierà l'agente durante l'esecuzione. Questa modalità si attiva con il parametro --noop nella riga di comando: sudo puppet agent -t --noop.

Inoltre, è possibile attivare il log di debug — in esso Puppet scrive tutte le azioni che esegue: sulle risorse che al momento sta elaborando, sui parametri di queste risorse e sui programmi che avvia. Ovviamente, questo è un parametro. --debug.

Server

Non tratterò in questo articolo la configurazione completa del puppetserver e il deploy del codice su di esso; dirò solo che viene installata una versione del server perfettamente funzionante, che non richiede ulteriori configurazioni per operare con un piccolo numero di nodi (diciamo fino a cento). Un numero maggiore di nodi richiederà già una messa a punto: per impostazione predefinita, puppetserver avvia non più di quattro worker, e per una maggiore prestazione è necessario aumentare il loro numero e non dimenticare di innalzare i limiti di memoria, altrimenti gran parte del tempo il server sarà occupato con la garbage collection.

Per il deploy del codice — se hai bisogno di qualcosa di veloce e semplice, dai un'occhiata a (r10k)[https://github.com/puppetlabs/r10k], dovrebbe essere sufficiente per piccole installazioni.

Supplemento 2: raccomandazioni per scrivere codice

  1. Estrai tutta la logica in classi e definizioni.
  2. Tieni classi e definizioni in moduli, non nei manifesti con la descrizione dei nodi.
  3. Utilizza i fatti.
  4. Evita le condizioni basate sui nomi host.
  5. Non esitare ad aggiungere parametri per classi e definizioni: è meglio che avere logica implicita nascosta nel corpo della classe/definizione.

E perché raccomando di farlo? Lo spiegherò nel prossimo articolo.

Conclusione

Concludiamo con l'introduzione. Nello prossimo articolo parlerò di Hiera, ENC e PuppetDB.

Solo gli utenti registrati possono partecipare al sondaggio. Accedi, per favore.

In realtà, ci sono molti più argomenti — posso scrivere articoli su questi temi, votate su cosa vi piacerebbe leggere:

  • 59,1%Costrutti avanzati di Puppet — argomenti complessi: cicli, mappatura e altre espressioni lambda, collezionatori di risorse, risorse esportabili e interazione tra host tramite Puppet, tag, fornitori, tipi di dati astratti.
  • 31,8%«Sono un amministratore di sistema, ma da mamma» o come abbiamo integrato diversi server Puppet di versioni diverse in Avito, e in generale una parte sull'amministrazione del server Puppet.
  • 81,8%Come scriviamo codice Puppet: strumenti, documentazione, testing, CI/CD.

Hanno votato 22 utenti. 9 utenti si sono astenuti.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster