Introduzione al burattino

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

Lavoro con Puppet ormai da più di cinque anni. Questo testo è essenzialmente una raccolta tradotta e riordinata dei punti chiave della documentazione ufficiale, che consentirà ai principianti di comprendere rapidamente l'essenza di Puppet.

Introduzione al burattino

Informazioni di base

Il sistema operativo di Puppet è client-server, sebbene supporti anche il funzionamento serverless con funzionalità limitate.

Viene utilizzato un modello operativo pull: per impostazione predefinita, una volta ogni mezz'ora, i client contattano il server per una configurazione e la applicano. Se hai lavorato con Ansible, utilizzano un modello push diverso: l'amministratore avvia il processo di applicazione della configurazione, i client stessi non applicheranno nulla.

Durante la comunicazione di rete viene utilizzata la crittografia TLS bidirezionale: il server e il client hanno le proprie chiavi private e i relativi certificati. Normalmente il server emette certificati per i client, ma in linea di principio è possibile utilizzare una CA esterna.

Introduzione ai manifesti

Nella terminologia dei burattini al server dei pupazzi Collegare nodi (nodi). Viene scritta la configurazione per i nodi nei manifesti in un linguaggio di programmazione speciale: Puppet DSL.

Puppet DSL è un linguaggio dichiarativo. Descrive lo stato desiderato del nodo sotto forma di dichiarazioni di singole risorse, ad esempio:

  • Il file esiste e ha un contenuto specifico.
  • Il pacchetto è installato.
  • Il servizio è iniziato.

Le risorse possono essere interconnesse:

  • Esistono dipendenze che influenzano l'ordine in cui vengono utilizzate le risorse.
    Ad esempio, "installa prima il pacchetto, quindi modifica il file di configurazione, quindi avvia il servizio".
  • Ci sono notifiche: se una risorsa è cambiata, invia notifiche alle risorse ad essa sottoscritte.
    Ad esempio, se il file di configurazione cambia, puoi riavviare automaticamente il servizio.

Inoltre, Puppet DSL dispone di funzioni e variabili, nonché di istruzioni condizionali e selettori. Sono supportati anche vari meccanismi di modelli: EPP ed ERB.

Puppet è scritto in Ruby, quindi molti dei costrutti e dei termini sono presi da lì. Ruby ti consente di espandere Puppet: aggiungere logica complessa, nuovi tipi di risorse e funzioni.

Mentre Puppet è in esecuzione, i manifest per ogni nodo specifico sul server vengono compilati in una directory. elenco è un elenco di risorse e le loro relazioni dopo il calcolo del valore di funzioni, variabili ed espansione delle istruzioni condizionali.

Sintassi e stile del codice

Ecco le sezioni della documentazione ufficiale che ti aiuteranno a capire la sintassi se gli esempi forniti non bastano:

Ecco un esempio di come appare il manifest:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

Il rientro e le interruzioni di riga non sono una parte obbligatoria del manifest, ma sono consigliate guida di stile. Riepilogo:

  • Rientri di due spazi, le tabulazioni non vengono utilizzate.
  • Le parentesi graffe sono separate da uno spazio; i due punti non sono separati da uno spazio.
  • Virgole dopo ogni parametro, compreso l'ultimo. Ogni parametro si trova su una riga separata. Fa eccezione il caso senza parametri e con un parametro: si può scrivere su una riga e senza virgola (es. resource { 'title': } и resource { 'title': param => value }).
  • Le frecce sui parametri dovrebbero essere allo stesso livello.
  • Le frecce delle relazioni tra le risorse sono scritte davanti a loro.

Posizione dei file su pappetserver

Per ulteriori spiegazioni, introdurrò il concetto di “directory root”. La directory root è la directory che contiene la configurazione Puppet per un nodo specifico.

La directory root varia a seconda della versione di Puppet e degli ambienti utilizzati. Gli ambienti sono insiemi di configurazioni indipendenti archiviati in directory separate. Solitamente utilizzato in combinazione con git, nel qual caso gli ambienti vengono creati dai rami git. Di conseguenza, ciascun nodo si trova in un ambiente o nell'altro. Questo può essere configurato sul nodo stesso, oppure in ENC, di cui parlerò nel prossimo articolo.

  • Nella terza versione ("old Puppet") la directory di base era /etc/puppet. L'uso degli ambienti è facoltativo: ad esempio, non li usiamo con il vecchio Puppet. Se vengono utilizzati ambienti, solitamente vengono archiviati in /etc/puppet/environments, la directory root sarà la directory dell'ambiente. Se gli ambienti non vengono utilizzati, la directory root sarà la directory di base.
  • A partire dalla quarta versione (“new Puppet”), l'uso degli ambienti è diventato obbligatorio e la directory di base è stata spostata in /etc/puppetlabs/code. Di conseguenza, gli ambienti vengono archiviati /etc/puppetlabs/code/environments, la directory root è la directory dell'ambiente.

Deve essere presente una sottodirectory nella directory root manifests, che contiene uno o più manifest che descrivono i nodi. Inoltre, dovrebbe esserci una sottodirectory modules, che contiene i moduli. Ti dirò quali sono i moduli un po 'più tardi. Inoltre, il vecchio Puppet potrebbe avere anche una sottodirectory files, che contiene vari file che copiamo sui nodi. Nel nuovo Puppet, tutti i file sono inseriti in moduli.

I file manifest hanno l'estensione .pp.

Un paio di esempi di combattimento

Descrizione del nodo e della risorsa su di esso

Sul nodo server1.testdomain è necessario creare un file /etc/issue con contenuto Debian GNU/Linux n l. Il file deve essere di proprietà di un utente e di un gruppo root, i diritti di accesso devono essere 644.

Scriviamo un manifesto:

node 'server1.testdomain' {   # блок конфигурации, относящийся к ноде server1.testdomain
    file { '/etc/issue':   # описываем файл /etc/issue
        ensure  => present,   # этот файл должен существовать
        content => 'Debian GNU/Linux n l',   # у него должно быть такое содержимое
        owner   => root,   # пользователь-владелец
        group   => root,   # группа-владелец
        mode    => '0644',   # права на файл. Они заданы в виде строки (в кавычках), потому что иначе число с 0 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Relazioni tra le risorse su un nodo

Sul nodo server2.testdomain nginx deve essere in esecuzione e funzionante con una configurazione precedentemente preparata.

Scomponiamo il problema:

  • Il pacchetto deve essere installato nginx.
  • È necessario che i file di configurazione vengano copiati dal server.
  • Il servizio deve essere attivo nginx.
  • Se la configurazione viene aggiornata, il servizio deve essere riavviato.

Scriviamo un manifesto:

node 'server2.testdomain' {   # блок конфигурации, относящийся к ноде server2.testdomain
    package { 'nginx':   # описываем пакет nginx
        ensure => installed,   # он должен быть установлен
    }
  # Прямая стрелка (->) говорит о том, что ресурс ниже должен
  # создаваться после ресурса, описанного выше.
  # Такие зависимости транзитивны.
    -> file { '/etc/nginx':   # описываем файл /etc/nginx
        ensure  => directory,   # это должна быть директория
        source  => 'puppet:///modules/example/nginx-conf',   # её содержимое нужно брать с паппет-сервера по указанному адресу
        recurse => true,   # копировать файлы рекурсивно
        purge   => true,   # нужно удалять лишние файлы (те, которых нет в источнике)
        force   => true,   # удалять лишние директории
    }
  # Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
  # подписаться на изменения ресурса, описанного выше.
  # Волнистая стрелка включает в себя прямую (->).
    ~> service { 'nginx':   # описываем сервис nginx
        ensure => running,   # он должен быть запущен
        enable => true,   # его нужно запускать автоматически при старте системы
    }
  # Когда ресурс типа service получает уведомление,
  # соответствующий сервис перезапускается.
}

Affinché funzioni, è necessario approssimativamente il seguente percorso di file sul server dei pupazzi:

/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

Un elenco completo dei tipi di risorse supportati è disponibile qui nella documentazione, qui descriverò cinque tipi base, che nella mia pratica sono sufficienti per risolvere la maggior parte dei problemi.

filetto

Gestisce file, directory, collegamenti simbolici, i loro contenuti e diritti di accesso.

opzioni:

  • nome della risorsa — percorso del file (facoltativo)
  • sentiero — percorso del file (se non è specificato nel nome)
  • garantire - tipo di file:
    • absent - eliminare un file
    • present — deve essere presente un file di qualsiasi tipo (se non è presente alcun file, verrà creato un file normale)
    • file - fascicolo normale
    • directory - elenco
    • link - collegamento simbolico
  • contenuto — contenuto del file (adatto solo per file normali, non può essere utilizzato insieme a source o bersaglio)
  • source — un collegamento al percorso da cui si desidera copiare il contenuto del file (non può essere utilizzato insieme a contenuto o bersaglio). Può essere specificato come URI con uno schema puppet: (quindi verranno utilizzati i file del server pupazzo) e con lo schema http: (Spero che sia chiaro cosa accadrà in questo caso), e anche con il diagramma file: o come percorso assoluto senza schema (verrà utilizzato il file del FS locale sul nodo)
  • bersaglio — dove dovrebbe puntare il collegamento simbolico (non può essere utilizzato insieme a contenuto o source)
  • proprietario - l'utente che dovrebbe possedere il file
  • gruppo — il gruppo a cui dovrebbe appartenere il file
  • modo — permessi dei file (come stringa)
  • ricorso - abilita l'elaborazione ricorsiva delle directory
  • purga - consente l'eliminazione di file che non sono descritti in Puppet
  • forza - consente l'eliminazione delle directory che non sono descritte in Puppet

pacchetto

Installa e rimuove i pacchetti. In grado di gestire le notifiche: reinstalla il pacchetto se il parametro è specificato reinstall_on_refresh.

opzioni:

  • nome della risorsa — nome del pacchetto (facoltativo)
  • Nome — nome del pacchetto (se non specificato nel nome)
  • fornitore — gestore di pacchetti da utilizzare
  • garantire — stato desiderato del pacco:
    • present, installed - qualsiasi versione installata
    • latest -ultima versione installata
    • absent - cancellato (apt-get remove)
    • purged — eliminato insieme ai file di configurazione (apt-get purge)
    • held - la versione del pacchetto è bloccata (apt-mark hold)
    • любая другая строка — è installata la versione specificata
  • reinstall_on_refresh - se un true, quindi al ricevimento della notifica il pacchetto verrà reinstallato. Utile per distribuzioni basate su codice sorgente, dove potrebbe essere necessaria la ricostruzione dei pacchetti quando si modificano i parametri di compilazione. Predefinito false.

servizio

Gestisce i servizi. In grado di elaborare le notifiche: riavvia il servizio.

opzioni:

  • nome della risorsa — servizio da gestire (facoltativo)
  • Nome — il servizio da gestire (se non specificato nel nome)
  • garantire — stato del servizio desiderato:
    • running - lanciato
    • stopped - fermato
  • enable — controlla la possibilità di avviare il servizio:
    • true — l'esecuzione automatica è abilitata (systemctl enable)
    • mask - mascherato (systemctl mask)
    • false — l'esecuzione automatica è disabilitata (systemctl disable)
  • nuovo inizio - comando per riavviare il servizio
  • status — comando per verificare lo stato del servizio
  • hariavviato — indica se lo script di inizializzazione del servizio supporta il riavvio. Se false e il parametro è specificato nuovo inizio — viene utilizzato il valore di questo parametro. Se false e parametro nuovo inizio non specificato: il servizio viene interrotto e avviato per il riavvio (ma systemd utilizza il comando systemctl restart).
  • hasstatus — indica se il servizio initscript supporta il comando status. Se false, viene utilizzato il valore del parametro status. Predefinito true.

exec

Esegue comandi esterni. Se non si specificano i parametri crea, solo se, salvo che o aggiornasolo, il comando verrà eseguito ogni volta che viene eseguito Puppet. In grado di elaborare le notifiche: esegue un comando.

opzioni:

  • nome della risorsa — comando da eseguire (facoltativo)
  • command — il comando da eseguire (se non è specificato nel nome)
  • sentiero — percorsi in cui cercare il file eseguibile
  • solo se — se il comando specificato in questo parametro viene completato con un codice di ritorno pari a zero, verrà eseguito il comando principale
  • salvo che — se il comando specificato in questo parametro viene completato con un codice di ritorno diverso da zero, verrà eseguito il comando principale
  • crea — se il file specificato in questo parametro non esiste, verrà eseguito il comando principale
  • aggiornasolo - se un true, il comando verrà eseguito solo quando questo exec riceve una notifica da altre risorse
  • cwd — directory da cui eseguire il comando
  • Utente — l'utente da cui eseguire il comando
  • fornitore - come eseguire il comando:
    • posix — viene semplicemente creato un processo figlio, assicurati di specificarlo sentiero
    • conchiglia - il comando viene lanciato nella shell /bin/sh, potrebbe non essere specificato sentiero, puoi utilizzare globbing, pipe e altre funzionalità della shell. Solitamente rilevato automaticamente se sono presenti caratteri speciali (|, ;, &&, || eccetera).

cron

Controlla i cronjob.

opzioni:

  • nome della risorsa - solo una sorta di identificatore
  • garantire — stato del lavoro della corona:
    • present - crea se non esiste
    • absent - cancella se esiste
  • command - quale comando eseguire
  • ambiente — in quale ambiente eseguire il comando (elenco delle variabili di ambiente e dei loro valori tramite =)
  • Utente — da quale utente eseguire il comando
  • minuto, ora, giorno feriale, mese, mese giorno - quando eseguire cron. Se uno qualsiasi di questi attributi non è specificato, lo sarà il suo valore nel crontab *.

In Marionetta 6.0 cron come se rimosso dalla scatola in pupazzoserver, quindi non c'è documentazione sul sito generale. Ma lui è nella scatola in pupazzo-agente, quindi non è necessario installarlo separatamente. Puoi vedere la documentazione per questo nella documentazione per la quinta versione di PuppetO su GitHub.

Sulle risorse in generale

Requisiti per l'unicità delle risorse

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

Pertanto, scriverò ancora: i manifest per lo stesso nodo non devono contenere risorse dello stesso tipo con lo stesso titolo!

A volte è necessario installare pacchetti con lo stesso nome, ma con gestori di pacchetti diversi. In questo caso è necessario utilizzare il parametro nameper evitare l'errore:

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

Altri tipi di risorse hanno opzioni simili per evitare duplicazioni − name у servizio, command у exec, e così via.

Metaparametri

Ogni tipo di risorsa ha alcuni parametri speciali, indipendentemente dalla sua natura.

Elenco completo dei meta parametri nella documentazione di Puppet.

Breve elenco:

  • richiedere — questo parametro indica da quali risorse dipende questa risorsa.
  • prima - Questo parametro specifica quali risorse dipendono da questa risorsa.
  • sottoscrivi — questo parametro specifica da quali risorse questa risorsa riceve notifiche.
  • notificare — Questo parametro specifica quali risorse ricevono notifiche da questa risorsa.

Tutti i metaparametri elencati accettano un singolo collegamento a risorsa o una serie di collegamenti tra parentesi quadre.

Collegamenti alle risorse

Un collegamento a una risorsa è semplicemente una menzione della risorsa. Sono utilizzati principalmente per indicare le dipendenze. Fare riferimento a una risorsa inesistente causerà un errore di compilazione.

La sintassi del collegamento è la seguente: tipo di risorsa con una lettera maiuscola (se il nome del tipo contiene doppi due punti, allora ogni parte del nome tra i due punti è in maiuscolo), quindi il nome della risorsa tra parentesi quadre (nel caso del nome non cambia!). Non dovrebbero esserci spazi; le parentesi quadre vengono 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

Documentazione qui.

Come affermato in precedenza, le dipendenze semplici tra risorse sono transitive. A proposito, fai attenzione quando aggiungi le dipendenze: puoi creare dipendenze cicliche, che causeranno un errore di compilazione.

A differenza delle dipendenze, le notifiche non sono transitive. Per le notifiche valgono le seguenti regole:

  • Se la risorsa riceve una notifica, viene aggiornata. Le azioni di aggiornamento dipendono dal tipo di risorsa − exec esegue il comando, servizio riavvia il servizio, pacchetto reinstalla il pacchetto. Se per la risorsa non è definita un'azione di aggiornamento, non accade nulla.
  • Durante un'esecuzione di Puppet, la risorsa viene aggiornata non più di una volta. Ciò è possibile perché le notifiche includono le dipendenze e il grafico delle dipendenze non contiene cicli.
  • Se Puppet modifica lo stato di una risorsa, la risorsa invia notifiche a tutte le risorse ad essa sottoscritte.
  • Se una risorsa viene aggiornata, invia notifiche a tutte le risorse ad essa sottoscritte.

Gestire parametri non specificati

Di norma, se alcuni parametri della risorsa non hanno un valore predefinito e questo parametro non è specificato nel manifest, Puppet non modificherà questa proprietà per la risorsa corrispondente sul nodo. Ad esempio, se una risorsa di tipo filetto parametro non specificato owner, Puppet non cambierà il proprietario del file corrispondente.

Introduzione a classi, variabili e definizioni

Supponiamo di avere diversi nodi che hanno la stessa parte della configurazione, ma ci sono anche delle differenze, altrimenti potremmo descriverli tutti in un blocco node {}. Naturalmente, puoi semplicemente copiare parti identiche della configurazione, ma in generale questa è una cattiva soluzione: la configurazione cresce e se modifichi la parte generale della configurazione, dovrai modificare la stessa cosa in molti punti. Allo stesso tempo, è facile commettere un errore e, in generale, il principio DRY (non ripeterti) è stato inventato per un motivo.

Per risolvere questo problema esiste un design come classe.

Classi

classe è un blocco denominato di codice poppet. Le classi sono necessarie per riutilizzare il codice.

Innanzitutto è necessario descrivere la classe. La descrizione stessa non aggiunge alcuna risorsa da nessuna parte. La classe è descritta nei manifest:

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

Successivamente la classe può essere utilizzata:

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

Un esempio dall'attività precedente: spostiamo 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',
        recure => 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 perché porta sempre la stessa configurazione nginx. Creiamo il percorso della variabile di configurazione, quindi questa classe può essere utilizzata per installare nginx con qualsiasi configurazione.

Si può fare utilizzando variabili.

Attenzione: le variabili in Puppet sono immutabili!

Inoltre, è possibile accedere a una variabile solo dopo che è stata dichiarata, altrimenti il ​​valore della variabile verrà modificato undef.

Esempio di lavoro con le variabili:

# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"

Il burattino ha spazi dei nomi, e le variabili, di conseguenza, hanno zona di visibilità: Una variabile con lo stesso nome può essere definita in spazi dei nomi diversi. Quando si risolve il valore di una variabile, la variabile viene cercata nello spazio dei nomi corrente, quindi nello spazio dei nomi che la racchiude e così via.

Esempi di spazio dei nomi:

  • globale: le variabili esterne alla classe o alla descrizione del nodo vanno lì;
  • spazio dei nomi del nodo nella descrizione del nodo;
  • spazio dei nomi della classe nella descrizione della classe.

Per evitare ambiguità quando si accede a una variabile, è possibile specificare lo spazio dei nomi nel nome della variabile:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Siamo d'accordo che il percorso verso la configurazione nginx risiede nella variabile $nginx_conf_source. Quindi la classe sarà simile a questa:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # здесь используем переменную вместо фиксированной строки
        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 è negativo perché esiste una “conoscenza segreta” che da qualche parte all’interno della classe viene utilizzata una variabile con questo o quel nome. È molto più corretto rendere questa conoscenza generale: le classi possono avere parametri.

Parametri di classe sono variabili nello spazio dei nomi della classe, sono specificate nell'intestazione della classe e possono essere utilizzate come variabili regolari nel corpo della classe. I valori dei parametri vengono specificati quando si utilizza la classe nel manifest.

Il parametro può essere impostato su un valore predefinito. Se un parametro non ha un valore predefinito e il valore non viene impostato quando utilizzato, si verificherà un errore di compilazione.

Parametrizziamo la classe dell'esempio sopra 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 i 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 vengono digitate. Mangiare molti tipi di dati. I tipi di dati vengono generalmente utilizzati per convalidare i 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 viene scritto immediatamente prima del nome del parametro:

class example (
  String $param1,
  Integer $param2,
  Array $param3,
  Hash $param4,
  Hash[String, String] $param5,
) {
  ...
}

Classi: include nomeclasse vs classe{'nomeclasse':}

Ogni classe è una risorsa di tipo classe. Come con qualsiasi altro tipo di risorsa, non possono esserci due istanze della stessa classe sullo stesso nodo.

Se provi ad aggiungere una classe allo stesso nodo due volte utilizzando class { 'classname':} (nessuna differenza, con parametri diversi o identici), si verificherà un errore di compilazione. Ma se usi una classe nello stile risorsa, puoi immediatamente impostare in modo esplicito tutti i suoi parametri nel manifest.

Tuttavia, se usi include, la classe può essere aggiunta quante volte si desidera. Il fatto è che include è una funzione idempotente che controlla se una classe è stata aggiunta alla directory. Se la classe non è nella directory, la aggiunge e se esiste già, non fa nulla. Ma in caso di utilizzo include Non è possibile impostare i parametri della classe durante la dichiarazione della classe: tutti i parametri richiesti devono essere impostati in un'origine dati esterna: Hiera o ENC. Ne parleremo nel prossimo articolo.

Definisce

Come detto nel blocco precedente, la stessa classe non può essere presente su un nodo più di una volta. Tuttavia, in alcuni casi è necessario poter utilizzare lo stesso blocco di codice con parametri diversi sullo stesso nodo. In altre parole, è necessario un tipo di risorsa proprio.

Ad esempio, per installare il modulo PHP, in Avito eseguiamo le seguenti operazioni:

  1. Installa il pacchetto con questo modulo.
  2. Creiamo un file di configurazione per questo modulo.
  3. Creiamo un collegamento simbolico alla configurazione per php-fpm.
  4. Creiamo un collegamento simbolico alla configurazione per php cli.

In questi casi, un design come definire (definire, tipo definito, tipo di risorsa definito). Una 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, ciascuna definizione ha un parametro implicito $title, dove va il nome della risorsa quando viene dichiarato. Proprio come nel caso delle classi, la definizione deve prima essere descritta, dopodiché può essere utilizzata.

Un 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'],  # триггеры дебиановских php-пакетов сами создают симлинки и перезапускают сервис php-fpm - нам это не нужно, так как и симлинками, и сервисом мы управляем с помощью 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' }
}

Il modo più semplice per rilevare l'errore di dichiarazione duplicata è in Define. Ciò accade se una definizione ha una risorsa con un nome costante e ci sono due o più istanze di questa definizione su qualche nodo.

È facile proteggersi da questo: tutte le risorse all'interno della definizione devono avere un nome a seconda $title. Un'alternativa è l'aggiunta idempotente di risorse; nel caso più semplice, è sufficiente spostare le risorse comuni a tutte le istanze della definizione in una classe separata e includere questa classe nella funzione definizione include idempotente.

Esistono altri modi per ottenere l'idempotenza quando si aggiungono risorse, ovvero utilizzando le funzioni defined и ensure_resources, ma ve ne parlerò nella prossima puntata.

Dipendenze e notifiche per classi e definizioni

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

  • la dipendenza da una classe/definizione aggiunge dipendenze su tutte le risorse della classe/define;
  • una dipendenza class/define aggiunge dipendenze a tutte le risorse class/define;
  • notifica class/define notifica tutte le risorse della classe/define;
  • L'abbonamento class/define sottoscrive tutte le risorse della classe/define.

Istruzioni condizionali e selettori

Documentazione qui.

if

Qui è tutto semplice:

if ВЫРАЖЕНИЕ1 {
  ...
} elsif ВЫРАЖЕНИЕ2 {
  ...
} else {
  ...
}

salvo che

less è un if al contrario: il blocco di codice verrà eseguito se l'espressione è falsa.

unless ВЫРАЖЕНИЕ {
  ...
}

Custodie

Anche qui non c'è niente di complicato. Puoi utilizzare valori regolari (stringhe, numeri, ecc.), espressioni regolari e tipi di dati come valori.

case ВЫРАЖЕНИЕ {
  ЗНАЧЕНИЕ1: { ... }
  ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
  default: { ... }
}

Selettori

Un selettore è un costrutto linguistico simile a case, ma 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 manifest. Ma più configurazioni descriviamo, più classi e nodi ci sono nel manifest, cresce e diventa scomodo lavorarci.

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

Moduli - si tratta di insiemi di classi, definizioni e altre entità Puppet collocate in una directory separata. In altre parole, un modulo è un pezzo indipendente della logica Puppet. Ad esempio, potrebbe esserci un modulo per lavorare con nginx e conterrà cosa e solo ciò che è necessario per lavorare con nginx, oppure potrebbe esserci un modulo per lavorare con PHP e così via.

I moduli hanno una versione e sono supportate anche le dipendenze reciproche dei moduli. Esiste un repository aperto di moduli - Fucina dei burattini.

Sul server pupazzo, i moduli si trovano nella sottodirectory module della directory root. All'interno di ogni modulo c'è uno schema di directory standard: manifesti, file, modelli, lib e così via.

Struttura dei file in un modulo

La radice del modulo può contenere le seguenti directory con nomi descrittivi:

  • manifests - contiene manifesti
  • files - contiene file
  • templates - contiene modelli
  • lib - contiene il codice Ruby

Questo non è un elenco completo di directory e file, ma per ora è sufficiente per questo articolo.

Nomi delle risorse e nomi dei file nel modulo

Documentazione qui.

Le risorse (classi, definizioni) in un modulo non possono essere nominate come preferisci. Inoltre esiste una corrispondenza diretta tra il nome di una risorsa e il nome del file in cui Puppet cercherà la descrizione di quella risorsa. Se violi le regole di denominazione, Puppet semplicemente non troverà la descrizione della risorsa e riceverai un errore di compilazione.

Le regole sono semplici:

  • Tutte le risorse in un modulo devono trovarsi nello spazio dei nomi del modulo. Se il modulo viene chiamato foo, allora tutte le risorse in esso contenute dovrebbero essere nominate foo::<anything>o solo foo.
  • La 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 scartato
    • tutti i doppi due punti, se presenti, vengono sostituiti con barre
    • viene aggiunta l'estensione .pp

Lo dimostrerò con un esempio. Diciamo che sto scrivendo un modulo nginx. Contiene le seguenti risorse:

  • classe nginx descritto nel manifesto init.pp;
  • classe nginx::service descritto nel manifesto service.pp;
  • definire nginx::server descritto nel manifesto server.pp;
  • definire nginx::server::location descritto nel manifesto server/location.pp.

Modelli

Sicuramente tu stesso sai cosa sono i modelli; non li descriverò in dettaglio qui. Ma lo lascerò per ogni evenienza collegamento a Wikipedia.

Come utilizzare i modelli: Il significato di un modello può essere ampliato utilizzando una funzione template, a cui viene passato il percorso del modello. Per risorse di tipo filetto utilizzato insieme al parametro content. Ad esempio, in questo modo:

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

Visualizza percorso <modulename>/<filename> implica file <rootdir>/modules/<modulename>/templates/<filename>.

Inoltre, c'è una funzione inline_template — riceve come input il testo del modello, non il nome del file.

All'interno dei modelli, puoi utilizzare tutte le variabili Puppet nell'ambito corrente.

Puppet supporta modelli in formato ERB ed EPP:

Brevemente sull'ERB

Strutture di controllo:

  • <%= ВЫРАЖЕНИЕ %> — inserire il valore dell'espressione
  • <% ВЫРАЖЕНИЕ %> — calcolare il valore di un'espressione (senza inserirla). Le istruzioni condizionali (if) e i cicli (ciascuno) di solito vanno qui.
  • <%# КОММЕНТАРИЙ %>

Le espressioni in ERB sono scritte in Ruby (ERB è in realtà Embedded Ruby).

Per accedere alle variabili dal manifest, è necessario aggiungere @ al nome della variabile. Per rimuovere un'interruzione di riga visualizzata dopo un costrutto di controllo, è necessario utilizzare un tag di chiusura -%>.

Esempio di utilizzo del modello

Diciamo che sto scrivendo un modulo per controllare ZooKeeper. La classe responsabile della creazione della configurazione assomiglia a questa:

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 modello corrispondente zoo.cfg.erb - COSÌ:

<% if @nodes.length > 0 -%>
<% @nodes.each do |node, id| -%>
server.<%= id %>=<%= node %>:<%= @port_leader %>:<%= @port_quorum %>;<%= @port_client %>
<% end -%>
<% end -%>

dataDir=<%= @datadir %>

<% @properties.each do |k, v| -%>
<%= k %>=<%= v %>
<% end -%>

Fatti e variabili integrate

Spesso la parte specifica della configurazione dipende da ciò che sta accadendo attualmente sul nodo. Ad esempio, a seconda della versione Debian, è necessario installare l'una o l'altra versione del pacchetto. Puoi monitorare tutto questo manualmente, riscrivendo i manifest se i nodi cambiano. Ma questo non è un approccio serio; l’automazione è molto meglio.

Per ottenere informazioni sui nodi, Puppet dispone di un meccanismo chiamato fatti. Fatti - si tratta di informazioni sul nodo, disponibili nei manifest sotto forma di variabili ordinarie nello spazio dei nomi globale. Ad esempio, nome host, versione del sistema operativo, architettura del processore, elenco di utenti, elenco di interfacce di rete e relativi indirizzi e molto, molto altro. I fatti sono disponibili nei manifest e nei modelli come variabili regolari.

Un esempio di come lavorare con i fatti:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог

Formalmente un fatto ha un nome (stringa) e un valore (ne sono disponibili vari tipi: stringhe, array, dizionari). Mangiare insieme di fatti incorporati. Puoi anche scrivere il tuo. Vengono descritti i collezionisti di fatti come le funzioni in Rubyo come file eseguibili. I fatti possono anche essere presentati sotto forma di modulo file di testo con dati sui nodi.

Durante il funzionamento, l'agente fantoccio copia prima tutti i fact collector disponibili dal pappetserver al nodo, dopodiché li avvia e invia i fatti raccolti al server; Successivamente, il server inizia a compilare il catalogo.

Fatti sotto forma di file eseguibili

Tali fatti vengono inseriti nei moduli della directory facts.d. Naturalmente i file devono essere eseguibili. Quando vengono eseguiti, devono restituire le informazioni all'output standard in formato YAML o chiave=valore.

Non dimenticare che i fatti si applicano a tutti i nodi controllati dal server poppet su cui è distribuito il tuo modulo. Pertanto, nello script, abbi cura di controllare che nel sistema siano presenti tutti i programmi e i file necessari affinché il tuo funzionamento funzioni.

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

Fatti rubini

Tali fatti vengono inseriti nei moduli della 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 del testo

Tali fatti vengono posizionati sui nodi nella directory /etc/facter/facts.d nel vecchio burattino o /etc/puppetlabs/facts.d nel nuovo Burattino.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

Arrivare ai fatti

Esistono due modi per avvicinarsi ai fatti:

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

È meglio usare un dizionario $facts, o meglio ancora, indicare il namespace globale ($::facts).

Ecco la sezione pertinente della documentazione.

Variabili integrate

Oltre ai fatti, c'è anche alcune variabili, disponibile nello spazio dei nomi globale.

  • fatti attendibili — variabili prese dal certificato del client (poiché il certificato viene solitamente emesso su un server poppet, l'agente non può semplicemente prendere e modificare il suo certificato, quindi le variabili sono "attendibili"): il nome del certificato, il nome del host e dominio, estensioni dal 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 pupazzo e non dal facter — nome del certificato, versione dell'agente, versione pupazzo.
  • variabili principali - Variabili Pappetmaster (sic!). È più o meno lo stesso di fatti del server, oltre ai valori dei parametri di configurazione sono disponibili.
  • variabili del compilatore — variabili del compilatore che differiscono in ciascun ambito: il nome del modulo corrente e il nome del modulo in cui è stato effettuato l'accesso all'oggetto corrente. Possono essere utilizzati, ad esempio, per verificare che le tue lezioni private non vengano utilizzate direttamente da altri moduli.

Aggiunta 1: come eseguire ed eseguire il debug di tutto questo?

L'articolo conteneva molti esempi di codice pupazzo, ma non spiegava affatto come eseguire questo codice. Beh, mi correggo.

Un agente è sufficiente per eseguire Puppet, ma nella maggior parte dei casi avrai bisogno anche di un server.

agente

Almeno dalla versione XNUMX, i pacchetti pupazzo-agente da repository ufficiale di Puppetlabs contenga tutte le dipendenze (Ruby e i gem corrispondenti), quindi non ci sono difficoltà di installazione (sto parlando di distribuzioni basate su Debian - non usiamo distribuzioni basate su RPM).

Nel caso più semplice, per utilizzare la configurazione pupazzo, è sufficiente lanciare l'agente in modalità serverless: a condizione che il codice pupazzo venga copiato sul nodo, lanciare 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, configurare il server ed eseguire gli agenti sui nodi in modalità demone, quindi una volta ogni mezz'ora applicheranno la configurazione scaricata dal server.

Puoi imitare il modello di lavoro push: vai al nodo che ti interessa e inizia sudo puppet agent -t. Chiave -t (--test) include in realtà diverse opzioni che possono essere abilitate individualmente. Queste opzioni includono quanto segue:

  • non eseguire in modalità demone (per impostazione predefinita l'agente si avvia in modalità demone);
  • spegnimento dopo l'applicazione del catalogo (per impostazione predefinita, l'agente continuerà a funzionare e ad applicare la configurazione una volta ogni mezz'ora);
  • scrivere un registro di lavoro dettagliato;
  • mostrare le modifiche nei file.

L'agente ha una modalità operativa senza modifiche: puoi utilizzarla quando non sei sicuro di aver scritto la configurazione corretta e desideri verificare cosa cambierà esattamente l'agente durante il funzionamento. Questa modalità è abilitata dal parametro --noop sulla riga di comando: sudo puppet agent -t --noop.

Inoltre, puoi abilitare il registro di debug del lavoro: in esso il pupazzo scrive tutte le azioni che esegue: sulla risorsa che sta attualmente elaborando, sui parametri di questa risorsa, su quali programmi avvia. Naturalmente questo è un parametro --debug.

Server

Non prenderò in considerazione la configurazione completa del pappetserver e la distribuzione del codice in questo articolo; dirò solo che immediatamente esiste una versione completamente funzionale del server che non richiede configurazioni aggiuntive per funzionare con un numero limitato di nodi (diciamo, fino a un centinaio). Un numero maggiore di nodi richiederà un'ottimizzazione: per impostazione predefinita, pupazzoserver non avvia più di quattro lavoratori, per prestazioni maggiori è necessario aumentarne il numero e non dimenticare di aumentare i limiti di memoria, altrimenti il ​​server effettuerà la raccolta dei rifiuti per la maggior parte del tempo.

Distribuzione del codice: se ne hai bisogno in modo rapido e semplice, guarda (su r10k)[https://github.com/puppetlabs/r10k], per piccole installazioni dovrebbe essere più che sufficiente.

Addendum 2: Linee guida per la codifica

  1. Inserisci tutta la logica in classi e definizioni.
  2. Mantieni le classi e le definizioni nei moduli, non nei manifest che descrivono i nodi.
  3. Usa i fatti.
  4. Non creare if basati sui nomi host.
  5. Sentiti libero di aggiungere parametri per classi e definizioni: questo è meglio della logica implicita nascosta nel corpo della classe/definizione.

Spiegherò perché consiglio di farlo nel prossimo articolo.

conclusione

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

Solo gli utenti registrati possono partecipare al sondaggio. AccediPer favore.

In effetti, c'è molto più materiale: posso scrivere articoli sui seguenti argomenti, votare ciò di cui ti interesserebbe leggere:

  • 59,1%Costrutti pupazzi avanzati: alcune cose di livello successivo: loop, mappature e altre espressioni lambda, raccoglitori di risorse, risorse esportate e comunicazione tra host tramite Puppet, tag, provider, tipi di dati astratti.13
  • 31,8%"Sono l'amministratore di mia madre" o come noi di Avito abbiamo stretto amicizia con diversi server Poppet di diverse versioni e, in linea di principio, la parte relativa all'amministrazione del server Poppet.7
  • 81,8%Come scriviamo il codice pupazzo: strumentazione, documentazione, test, CI/CD.18

22 utenti hanno votato. 9 utenti si sono astenuti.

Fonte: habr.com