Introducere în Puppet

Puppet este un sistem de gestionare a configurației. Este folosit pentru a aduce gazdele la starea dorită și pentru a menține această stare.

Lucrez cu Puppet de mai bine de cinci ani. Acest text este, în esență, o compunere tradusă și reorganizată a punctelor cheie din documentația oficială, care va permite începătorilor să înțeleagă rapid esența Puppet.

Introducere în Puppet

Informații de bază

Schema de funcționare Puppet este client-server, deși este suportată și o variantă de funcționare fără server cu funcționalitate limitată.

Se folosește un model de lucru de tip pull: în mod implicit, la fiecare jumătate de oră, clienții se conectează la server pentru a obține configurația și o aplică. Dacă ați lucrat cu Ansible, acolo se folosește un alt model, push: administratorul inițiază procesul de aplicare a configurației, clienții în sine nu vor aplica nimic.

În interacțiunea de rețea se folosește criptarea TLS bidirecțională: serverul și clientul au propriile chei private și certificate corespunzătoare. În mod obișnuit, serverul emite certificate pentru clienți, dar în principiu este posibilă utilizarea și a unui CA extern.

Introducere în manifeste

În terminologia Puppet se conectează la serverul Puppet nodurile (nodes). Configurația pentru noduri este scrisă în manifeste într-un limbaj de programare special - Puppet DSL. Puppet DSL este un limbaj declarat. Acesta descrie starea dorită a nodului sub formă de declarații de resurse individuale, de exemplu:

Fișierul există și are un anumit conținut.

  • Pachetul este instalat.
  • Serviciul este pornit.
  • Resursele pot fi interconectate:

Există dependențe, care influențează ordinea aplicării resurselor.

  • De exemplu, „mai întâi instalează pachetul, apoi modifică fișierul de configurare, iar după aceea pornește serviciul”.
    Există notificări - dacă o resursă se schimbă, aceasta trimite notificări resurselor care s-au înscris la ea.
  • De exemplu, dacă se schimbă fișierul de configurare, se poate reporni automat serviciul.
    În plus, în Puppet DSL există funcții și variabile, precum și operatori condiționali și selectori. De asemenea, se suportă diverse mecanisme de șablonizare - EPP și ERB.

Puppet este scris în Ruby, astfel încât multe construcții și termeni sunt preluați de acolo. Ruby permite extinderea Puppet - adăugarea de logică complexă, tipuri noi de resurse, funcții.

Puppet este scris în Ruby, astfel încât multe construcții și termeni sunt preluați de acolo. Ruby permite extinderea Puppet - adăugarea de logică complexă, tipuri noi de resurse, funcții.

În timpul funcționării Puppet, manifestele pentru fiecare nod specific de pe server sunt compilate în director. Folderul — este o listă de resurse și relațiile lor după calcularea valorilor funcțiilor, variabilelor și desfășurarea operatorilor condiționali.

Sintaxă și stil de cod

Iată secțiunile din documentația oficială care te vor ajuta să înțelegi sintaxa, dacă exemplele oferite nu sunt suficiente:

Iată un exemplu de cum arată un manifest:

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

Indentările și întreruperile de linie nu sunt o parte obligatorie a manifestului, dar există un ghid de stil. Rezumat:

  • Indentări cu două spații, taburile nu sunt utilizate.
  • Parantezele acolade sunt separate printr-un spațiu, iar două puncte nu sunt separate printr-un spațiu.
  • Virgulele după fiecare parametru, inclusiv ultimul. Fiecare parametru trebuie să fie pe o linie separată. Există o excepție pentru cazul fără parametri și un singur parametru: se poate scrie pe o linie și fără virgulă (adică, resource { 'title': } și resource { 'title': param => value }).
  • Săgețile de la parametri trebuie să fie la același nivel.
  • Săgețile relațiilor dintre resurse sunt scrise înainte de acestea.

Locația fișierelor pe puppetserver

Pentru explicații suplimentare, voi introduce conceptul de „director rădăcină”. Directorul rădăcină este directorul în care se află configurația Puppet pentru un anumit nod.

Directorul rădăcină variază în funcție de versiunea Puppet și de utilizarea mediilor. Mediile sunt seturi independente de configurație, care sunt stocate în directori separați. De obicei, sunt utilizate în combinație cu git, în acest caz mediile sunt create din ramuri git. În consecință, fiecare nod se află într-un mediu sau altul. Aceasta se configurează pe nodul însuși sau în ENC, despre care voi vorbi în articolul următor.

  • În a treia versiune („vechiul Puppet”), directorul de bază era /etc/puppet. Utilizarea mediilor este opțională - noi, de exemplu, nu le folosim cu vechiul Puppet. Dacă mediile sunt utilizate, acestea sunt de obicei stocate în /etc/puppet/environments, directorul rădăcină va fi directorul mediu. Dacă mediile nu sunt utilizate, directorul rădăcină va fi baza.
  • Începând cu a patra versiune („noul Puppet”), utilizarea mediilor a devenit obligatorie, iar directorul de bază a fost mutat în /etc/puppetlabs/code. Prin urmare, mediile sunt stocate în /etc/puppetlabs/code/environments, directorul rădăcină fiind directorul mediului.

În directorul rădăcină trebuie să existe un subdirector manifests, care conține unul sau mai multe manifeste cu descrierea nodurilor. De asemenea, trebuie să existe un subdirector modules, în care se află modulele. Ce sunt modulele, voi explica mai târziu. De asemenea, în vechiul Puppet poate exista un subdirector files, în care se află diferite fișiere pe care le copiem pe noduri. În noul Puppet, toate fișierele sunt mutate în module.

Fișierele manifestelor au extensia .pp.

Exemple de luptă

Descrierea nodului și a resursei pe acesta

Pe nodul server1.testdomain trebuie să fie creat un fișier /etc/issue cu conținutul Debian GNU/Linux n l. Fișierul trebuie să aparțină utilizatorului și grupului root, permisiunile trebuie să fie 644.

Scriem manifestul:

node 'server1.testdomain' {   # bloc de configurație asociat nodului server1.testdomain
    file { '\/etc\/issue':   # descriem fișierul \/etc\/issue
        ensure  => present,   # acest fișier trebuie să existe
        content => 'Debian GNU\/Linux n l',   # acesta trebuie să aibă un asemenea conținut
        owner   => root,   # utilizatorul proprietar
        group   => root,   # grupul proprietar
        mode    => '0644',   # permisiuni asupra fișierului. Ele sunt date sub formă de șir (între ghilimele), pentru că altfel cifra cu 0 la început va fi percepută ca fiind scrisă în sistem octal, iar totul va merge diferit față de cum a fost planificat
    }
}

Interdependențele resurselor pe nod

Pe nodul server2.testdomain trebuie să fie pornit nginx, funcționând cu configurația pregătită anterior.

Decompozăm sarcina:

  • Trebuie să fie instalat pachetul nginx.
  • Trebuie să fie copiate fișierele de configurație de pe server.
  • Trebuie să fie pornit serviciul nginx.
  • În cazul actualizării configurației, trebuie să repornim serviciul.

Scriem manifestul:

nodul 'server2.testdomain' {   # bloc de configurare asociat cu nodul server2.testdomain
    pachet { 'nginx':   # descriem pachetul nginx
        asigura => instalat,   # el trebuie să fie instalat
    }
  # Săgeata dreaptă (->) indică faptul că resursa de mai jos trebuie
  # să fie creată după resursa descrisă mai sus.
  # Astfel de dependențe sunt tranzitive.
    -> fișier { ' /etc/ ngnix ':   # descriem fișierul /etc/nginx
        asigura  => director,   # aceasta ar trebui să fie un director
        sursă  => 'puppet:///modules/example/nginx-conf',   # conținutul său trebuie să fie preluat de la serverul puppet la adresa specificată
        recursiv => adevărat,   # copiază fișierele recursiv
        purga   => adevărat,   # trebuie să ștergem fișierele inutile (cele care nu există în sursă)
        forțat   => adevărat,   # șterge directoarele inutile
    }
  # Săgeata ondulată (~>) indică faptul că resursa de mai jos trebuie
  # să se aboneze la modificările resursei descrise mai sus.
  # Săgeata ondulată include săgeata dreaptă (->).
    ~> serviciu { 'nginx':   # descriem serviciul nginx
        asigura => rulant,   # acesta trebuie să fie pornit
        activează => adevărat,   # acesta trebuie să fie pornit automat la pornirea sistemului
    }
  # Când resursa de tip serviciu primește o notificare,
  # serviciul corespunzător este repornit.
}

Pentru ca aceasta să funcționeze, este necesară o structură similară de fișiere pe serverul puppet:

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

Tipuri de resurse

Lista completă a tipurilor de resurse suportate se găsește în documentație, aici voi descrie cinci tipuri de bază, care în practica mea sunt suficiente pentru a rezolva cele mai multe sarcini.

file

Gestionează fișiere, directoare, linkuri simbolice, conținutul acestora, permisiunile.

Parametrii:

  • numele resursei — calea către fișier (opțional)
  • cale — calea către fișier (dacă nu este specificată în nume)
  • asigura — tipul fișierului:
    • inexistent — elimină fișierul
    • existent — trebuie să fie un fișier de orice tip (dacă fișierul nu există — va fi creat un fișier normal)
    • file — fișier normal
    • director — director
    • link — link simbolic
  • conținut — conținutul fișierului (se potrivește doar pentru fișiere normale, nu poate fi folosit împreună cu source sau destinația)
  • source — referință către calea din care trebuie copiat conținutul fișierului (nu poate fi folosit împreună cu conținut sau destinația). Poate fi specificată atât sub formă de URI cu schema puppet: (atunci vor fi folosite fișiere de pe serverul puppet), cât și cu schema http: (sper că este clar ce va fi în acest caz), și chiar cu schema file: sau sub formă de cale absolută fără schema (atunci fișierul de pe FS-ul local de pe nod va fi folosit)
  • destinația — către ce ar trebui să indice linkul simbolic (nu poate fi folosit împreună cu conținut sau source)
  • owner — utilizatorul căruia ar trebui să-i aparțină fișierul
  • grup — grupul căruia ar trebui să-i aparțină fișierul
  • mod — permisiuni pentru fișier (sub formă de șir)
  • recursiv — include procesarea recursivă a directorilor
  • sterge — include ștergerea fișierelor care nu sunt descrise în Puppet
  • forțat — include ștergerea directorilor care nu sunt descriși în Puppet

pachet

Instalează și îndepărtează pachete. Poate gestiona notificări — reinstalează pachetul dacă parametrul este specificat reinstalare_la_actualizare.

Parametrii:

  • numele resursei — numele pachetului (opțional)
  • name — numele pachetului (dacă nu este specificat în denumire)
  • furnizor — managerul de pachete care trebuie folosit
  • asigura — starea dorită a pachetului:
    • existent, instalat — este instalată orice versiune
    • latest — este instalată ultima versiune
    • inexistent — îndepărtat (apt-get remove)
    • îndepărtat complet — îndepărtat împreună cu fișierele de configurare (apt-get purge)
    • reținut — versiunea pachetului este blocată (apt-mark hold)
    • orice alt șir — este instalată versiunea specificată
  • reinstalare_la_actualizare — dacă true, atunci, la primirea unei notificări, pachetul va fi reinstalat. Util pentru distribuțiile bazate pe sursă, unde recompilarea pachetelor poate fi necesară la modificarea parametrilor de compilare. Implicit false.

service

Gestionează servicii. Poate gestiona notificări — repornește serviciul.

Parametrii:

  • numele resursei — serviciul pe care trebuie să-l gestionați (opțional)
  • name — serviciul pe care trebuie să-l gestionați (dacă nu este specificat în denumire)
  • asigura — starea dorită a serviciului:
    • funcționează — este pornit
    • oprit — este oprit
  • activează — gestionează capacitatea de a porni serviciul:
    • true — activat la pornire (systemctl enable)
    • mask — este mascat (systemctl mask)
    • false — dezactivat la pornire (systemctl disable)
  • repornire — comanda pentru repornirea serviciului
  • status — comanda pentru verificarea stării serviciului
  • are_repornire — specifică dacă scriptul de inițiere al serviciului suportă repornirea. Dacă false și parametrul este specificat repornire — se folosește valoarea acestui parametru. Dacă false și parametrul repornire nu este specificat — serviciul se oprește și se repornește pentru repornire (dar în systemd se folosește comanda systemctl restart).
  • are_stare — specifică dacă scriptul de inițiere al serviciului suportă comanda status. Dacă false, atunci se folosește valoarea parametrului status. Implicit true.

exec

Rulează comenzi externe. Dacă nu sunt specificate parametere creați, doar dacă, nu sau numai_refresh, comanda va fi executată la fiecare rulare a Puppetului. Poate gestiona notificări — rulează comanda.

Parametrii:

  • numele resursei — comanda care trebuie executată (opțional)
  • command — comanda care trebuie executată (dacă nu este specificată în denumire)
  • cale — căi în care să cauți fișierul executabil
  • doar dacă — dacă comanda specificată în acest parametru s-a încheiat cu un cod de returnare zero, comanda principală va fi executată
  • nu — dacă comanda specificată în acest parametru s-a încheiat cu un cod de returnare diferit de zero, comanda principală va fi executată
  • creați — dacă fișierul specificat în acest parametru nu există, comanda principală va fi executată
  • numai_refresh — dacă true, comanda va fi lansată doar în cazul în care acest exec primește notificare de la alte resurse
  • cwd — directorul din care se lansează comanda
  • utilizator — utilizatorul care va lansa comanda
  • furnizor — prin ceea ce se lansează comanda:
    • posix — se creează pur și simplu un proces copil, este obligatoriu să specificați cale
    • shell — comanda se lansează în shell /bin/sh, nu este necesar să specificați cale, puteți utiliza globbing, pipe-uri și alte caracteristici ale shell-ului. De obicei, este determinat automat, dacă există diverse caractere speciale (|, ;, &&, || și așa mai departe).

cron

Gestionează cron job-urile.

Parametrii:

  • numele resursei — pur și simplu un identificator
  • asigura — starea cron job-ului:
    • existent — să creăm, dacă nu există
    • inexistent — să ștergem, dacă există
  • command — ce comandă să se execute
  • environment — în ce mediu să se lanseze comanda (listă de variabile de mediu și valorile acestora separate prin =)
  • utilizator — de subordonarea cărei utilizator pentru a lansa comanda
  • minute, oră, zi a săptămânii, lună, zi a lunii — când să se lanseze cron. Dacă vreunul dintre acești atributi nu este specificat, valoarea sa în crontab va fi *.

În Puppet 6.0 cron se poate spune că a fost șters din cutie în puppetserver, prin urmare nu există documentație pe site-ul general. Dar este disponibil în cutie în puppet-agent, așa că nu trebuie să-l instalați separat. Documentația pentru acesta poate fi consultată în documentația versiunii a cincea a Puppet-ului, sau pe GitHub.

Despre resurse în general

Cerinte de unicitate a resurselor

Cea mai frecventă eroare cu care ne întâlnim este — Declarație duplicată. Această eroare apare atunci când în director ajung două sau mai multe resurse de același tip cu același nume.

Așa că o voi scrie din nou: în manifestele pentru aceeași nodă nu ar trebui să existe resurse de același tip cu același nume (titlu)!

Uneori este necesar să instalați pachete cu același nume, dar cu diferite manageri de pachete. În acest caz, trebuie să folosiți parametrul name, pentru a evita eroarea:

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

În alte tipuri de resurse există parametri similari care ajută la evitarea duplicării, - name u service, command u exec, și așa mai departe.

Metaparametrii

Fiecare tip de resursă are anumiți parametri speciali, indiferent de natura sa.

Lista completă a metaparametrilor în documentația Puppet.

Listă scurtă:

  • require — acest parametru specifică de ce resurse depinde această resursă.
  • before — acest parametru specifică ce resurse depind de această resursă.
  • subscribe — acest parametru specifică de la ce resurse primește notificări această resursă.
  • notify — acest parametru specifică ce resurse primesc notificări de la această resursă.

Toți metaparametrii menționați acceptă fie o singură referință la o resursă, fie un array de referințe între paranteze quadra.

Referințe la resurse

O referință la o resursă este pur și simplu o mențiune a resursei. Acestea sunt folosite în principal pentru a specifica dependențele. O referință la o resursă inexistentă va provoca o eroare de compilare.

Sintaxa pentru referință este următoarea: tipul resursei cu literă mare (dacă în denumirea tipului sunt prezente două puncte, fiecare parte între puncte este scrisă cu literă mare), urmată de numele resursei între paranteze quadra (nu se schimbă registrul numelui!). Nu trebuie să existe spații, iar parantezele quadra se scriu imediat după denumirea tipului.

Exemplu:

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

Dependințe și notificări

Documentația este aici.

După cum s-a menționat anterior, dependențele simple între resurse sunt tranzitive. De asemenea, fiți atenți la stabilirea dependențelor – pot apărea dependențe circulare, ceea ce va provoca o eroare de compilare.

Spre deosebire de dependențe, notificările nu sunt tranzitive. Pentru notificări se aplică următoarele reguli:

  • Dacă o resursă primește o notificare, aceasta se actualizează. Acțiunile la actualizare depind de tipul resursei - exec execrează o comandă, service reporneste un serviciu, pachet reinstalează un pachet. Dacă pentru resursă nu este definită o acțiune la actualizare, atunci nu se întâmplă nimic.
  • Într-o singură rulare a Puppet, o resursă se actualizează maxim o dată. Acest lucru este posibil, deoarece notificările includ dependențele, iar graficul dependențelor nu conține cicluri.
  • Dacă Puppet schimbă starea unei resurse, atunci resursa trimite notificări tuturor resurselor care sunt abonate la ea.
  • Dacă resursa este actualizată, aceasta trimite notificări tuturor resurselor care sunt abonate la ea.

Gestionarea parametrilor neprecizați

De obicei, dacă un parametru al resursei nu are o valoare implicită și acest parametru nu este specificat în manifest, Puppet nu va schimba această proprietate a resursei corespunzătoare de pe nod. De exemplu, dacă resursa de tip file nu are specificat parametrul owner, Puppet nu va schimba proprietarul fișierului corespunzător.

Introducere în clase, variabile și definiții

Să presupunem că avem mai multe noduri care au aceeași parte de configurație, dar există și diferențe – altfel am putea descrie totul într-un singur bloc. node {}. Desigur, poți pur și simplu să copiezi părțile identice ale configurației, dar în general acesta este o soluție proastă – configurația se extinde, iar la modificarea părții comune a configurației va trebui să corectezi același lucru în multe locuri. Este ușor să greșești, iar principiul DRY (don’t repeat yourself) nu a fost inventat degeaba.

Pentru a rezolva o astfel de problemă există o construcție numită clasă.

Clase

Clasă este un bloc denumit de cod Puppet. Clasele sunt necesare pentru reutilizarea codului.

Mai întâi, trebuie să descrii clasa. Descrierea în sine nu adaugă nicio resursă. O clasă este descrisă în manifeste:

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

După aceea, clasa poate fi utilizată:

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

Exemplu din sarcina anterioară – vom extrage instalarea și configurarea nginx într-o clasă:

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
}

Variabile

Clasa din exemplul anterior nu este deloc flexibilă, deoarece aduce întotdeauna aceeași configurație nginx. Să facem astfel încât calea către configurație să devină o variabilă, astfel încât această clasă să poată fi utilizată pentru instalarea nginx cu orice configurație.

Aceasta se poate face cu ajutorul variabilelor.

Atenție: variabilele în Puppet sunt imutabile!

În plus, poți accesa o variabilă doar după ce a fost declarată, altfel valoarea variabilei va fi undef.

Exemplu de lucru cu variabile:

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

În Puppet există spațiile de nume, iar variabilele au, de asemenea, domeniul de vizibilitate: o variabilă cu același nume poate fi definită în diferite spații de nume. La rezolvarea valorii variabilei, aceasta este căutată în spațiul de nume curent, apoi în cel părinte, și așa mai departe.

Exemple de spații de nume:

  • global — acolo sunt incluse variabilele din afara descrierii clasei sau nodului;
  • spațiul de nume al nodului în descrierea nodului;
  • spațiul de nume al clasei în descrierea clasei.

Pentru a evita ambiguitatea atunci când se face referire la o variabilă, se poate specifica spațiul de nume în numele variabilei:

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

Să stabilim că calea către configurația nginx se află în variabila $nginx_conf_source. Atunci clasa va arăta în felul următor:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # aici folosim variabila în loc de un șir fix
        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
}

Totuși, exemplul dat este slab prin faptul că există o anumită „cunoștință secretă” că undeva în interiorul clasei este utilizată o variabilă cu un anumit nume. Este mult mai corect să facem această cunoștință comună — clasele pot avea parametri.

Parametrii clasei — sunt variabile în spațiul de nume al clasei, ele sunt definite în antetul clasei și pot fi utilizate ca variabile obișnuite în corpul clasei. Valorile parametrilor se specifică atunci când se utilizează clasa în manifest.

Unui parametru i se poate atribui o valoare implicită. Dacă un parametru nu are o valoare implicită și nu este specificată o valoare la utilizare, acest lucru va genera o eroare de compilare.

Să parametrizăm clasa din exemplul de mai sus și să adăugăm doi parametri: unul, obligatoriu — calea către configurație, și al doilea, opțional — numele pachetului cu nginx (de exemplu, în Debian, există pachete 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',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

În Puppet, variabilele sunt tipizate. Există multe tipuri de date. Tipurile de date sunt utilizate de obicei pentru validarea valorilor parametrilor transmise în clase și definiții. Dacă parametrul trimis nu corespunde tipului specificat, va apărea o eroare de compilare.

Tipul se scrie direct înaintea numelui parametrului:

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

Clase: include classname vs class{‘classname’:}

Fiecare clasă este un tip de resursă class. La fel ca în cazul oricăror alte tipuri de resurse, nu pot exista două instanțe ale aceleași clase pe aceeași nod.

Dacă încerci să adaugi o clasă pe aceeași nod de două ori folosind class { 'classname':} (indiferent dacă cu parametrii diferiți sau identici), va apărea o eroare de compilare. Însă în cazul utilizării clasei în stilul resursei, poți specifica toate parametrii săi direct în manifest.

Cu toate acestea, dacă folosești include, atunci clasa poate fi adăugată de câte ori dorești. Problema este că include — o funcție idempotentă care verifică dacă clasa a fost adăugată în catalog. Dacă clasa nu se află în catalog, o adaugă, iar dacă există deja, nu face nimic. Dar în cazul utilizării include nu poți specifica parametrii clasei în timpul declarării clasei — toate parametrii obligatorii trebuie să fie specificați în sursa de date externă — Hiera sau ENC. Despre aceasta vom vorbi în următorul articol.

Definiri

Așa cum s-a menționat în blocul anterior, aceeași clasă nu poate apărea pe o nod de mai multe ori. Totuși, în anumite cazuri, este necesar să poți aplica aceeași bucată de cod cu parametrii diferiți pe aceeași nod. Cu alte cuvinte, există o nevoie de un tip propriu de resursă.

De exemplu, pentru a instala un modul PHP, facem următoarele la Avito:

  1. Instalăm pachetul cu acest modul.
  2. Creăm un fișier de configurare pentru acest modul.
  3. Creăm un symlink pentru config pentru php-fpm.
  4. Creăm un symlink pentru config pentru php cli.

În aceste cazuri se folosește o construcție precum definire (define, defined type, defined resource type). Definirea este asemănătoare cu o clasă, dar există diferențe: în primul rând, fiecare definire este un tip de resursă, nu o resursă; în al doilea rând, fiecare definire are un parametru implicit $title, unde ajunge numele resursei la timpul declarării. La fel ca și în cazul claselor, definirea trebuie descrisă mai întâi, după care poate fi utilizată.

Un exemplu simplificat cu un modul pentru 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'],  # triggers for Debian's PHP packages create symlinks and restart the PHP-FPM service on their own - we don’t need that, as we manage both symlinks and the service with 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' }
}

În definiție, este cel mai simplu să prindem eroarea de declarație duplicată. Acest lucru se întâmplă dacă în definiție există o resursă cu un nume constant și pe un anumit nod există două sau mai multe instanțe ale acestei definiții.

Pentru a te proteja de asta, este simplu: toate resursele din cadrul definiției trebuie să aibă nume care depind de $title. Ca alternativă - adăugarea idempotentă a resurselor, în cel mai simplu caz este suficient să scoatem resursele comune pentru toate instanțele definiției într-o clasă separată și să includem acea clasă în definiție - funcția include este idempotentă.

Există și alte moduri de a atinge idempotenta la adăugarea resurselor, și anume utilizarea funcțiilor defined și ensure_resources, dar voi vorbi despre asta în următorul episod.

Dependențe și notificări pentru clase și definiții

Clasele și definițiile adaugă următoarele reguli la gestionarea dependențelor și notificărilor:

  • o dependență de clasă/definiție adaugă dependențe pentru toate resursele clasei/definiției;
  • o dependență a clasei/definiției adaugă dependențe tuturor resurselor clasei/definiției;
  • o notificare a clasei/definiției notifică toate resursele clasei/definiției;
  • o abonare la clasă/definiție se abonează la toate resursele clasei/definiției.

Operatori condiționali și selecții

Documentația este aici.

if

Aici totul este simplu:

if EXPRESIE1 {
  ...
} elsif EXPRESIE2 {
  ...
} else {
  ...
}

nu

unless - este inversul if: blocul de cod va fi executat dacă expresia este falsă.

unless EXPRESIE {
  ...
}

caz

Aici, de asemenea, nimic complicat. Ca valori, poți utiliza valori obișnuite (șiruri, numere etc.), expresii regulate, precum și tipuri de date.

case EXPRESIE {
  VALOARE1: { ... }
  VALOARE2, VALOARE3: { ... }
  default: { ... }
}

Selectoare

Selectorul este o construcție a limbajului, asemănătoare cu caz, dar în loc să execute un bloc de cod, returnează o valoare.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }

Module

Când configurația este mică, poate fi ușor păstrată într-un singur manifest. Dar cu cât descriem mai multă configurație, cu atât devin mai multe clase și noduri în manifest, acesta se extinde, devenind mai greu de gestionat.

În plus, există problema reutilizării codului - când tot codul este într-un singur manifest, este dificil să se partajeze acest cod cu alții. Pentru a rezolva aceste două probleme, în Puppet există o entitate numită module.

Module — acesta este un set de clase, definiții și alte entități Puppet, extrase într-un director separat. Cu alte cuvinte, un modul este un segment independent de logică Puppet. De exemplu, poate exista un modul pentru lucru cu nginx, care va conține doar ceea ce este necesar pentru a lucra cu nginx, sau un modul pentru lucru cu PHP, și așa mai departe.

Modulele sunt versionate și, de asemenea, sunt susținute dependențele modulelor între ele. Există un repository deschis de module - Puppet Forge.

Pe serverul Puppet, modulele sunt plasate în subdirectorul modules al directorului rădăcină. În interiorul fiecărui modul, schema standard de directoare este - manifests, files, templates, lib și așa mai departe.

Structura fișierelor din modul

În rădăcina modulului pot exista următoarele directoare cu denumiri sugestive:

  • manifests — aici se află manifestele
  • files — aici se află fișierele
  • templates — aici se află șabloanele
  • lib — aici se află codul Ruby

Aceasta nu este o listă completă de directoare și fișiere, dar pentru acest articol este suficient.

Numele resurselor și numele fișierelor în modul

Documentația este aici.

Resursele (clase, definiții) din modul nu pot fi numite în mod aleatoriu. În plus, există o corespondență directă între denumirea resursei și numele fișierului în care Puppet va căuta descrierea acestei resurse. Dacă se încalcă regulile de denumire, Puppet pur și simplu nu va găsi descrierea resurselor și va apărea o eroare de compilare.

Regulile sunt simple:

  • Toate resursele din modul trebuie să fie în spațiul de nume al modulului. Dacă modulul se numește foo, atunci toate resursele din el trebuie să se numească foo::, sau pur și simplu foo.
  • Resursa cu denumirea modulului trebuie să fie în fișierul init.pp.
  • Pentru celelalte resurse, schema de denumire a fișierelor este următoarea:
    • prefixul cu numele modulului este eliminat
    • toate două puncte, dacă există, sunt înlocuite cu slash-uri
    • se adaugă extensia .pp

Voi demonstra cu un exemplu. Să spunem că scriu un modul nginx. Acesta conține următoarele resurse:

  • clasă nginx descris în manifest init.pp;
  • clasă nginx::service descris în manifest service.pp;
  • definire nginx::server descris în manifest server.pp;
  • definire nginx::server::location descris în manifest server/location.pp.

Șabloane

Probabil că știți și singuri ce sunt șabloanele, nu voi detalia aici. Dar, pentru orice eventualitate, voi lăsa un link către Wikipedia.

Cum să folosești șabloane: valoarea șablonului poate fi dezvăluită prin funcția template, căreia i se transmite calea către șablon. Pentru resursele de tip file folosim împreună cu parametrul conținut. De exemplu, așa:

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

Calea tipului / implică fișierul /modules//templates/.

De asemenea, există funcția inline_template — aceasta primește textul șablonului, nu numele fișierului.

În interiorul șabloanelor, se pot folosi toate variabilele Puppet din domeniul de vizibilitate curent.

Puppet suportă șabloane în format ERB și EPP:

Pe scurt despre ERB

Structuri de control:

  • <%= ВЫРАЖЕНИЕ %> — introduce valoarea expresiei
  • <% ВЫРАЖЕНИЕ %> — calculează valoarea expresiei (fără a o introduce). Aici, se folosesc operatori condiționali obișnuiți (if), bucle (each).
  • <%# КОММЕНТАРИЙ %>

Expresiile în ERB sunt scrise în Ruby (de fapt, ERB este Embedded Ruby).

Pentru a accesa variabilele din manifest, trebuie să adaugi @ la numele variabilei. Pentru a elimina linia nouă care apare după structura de control, trebuie să folosești eticheta de închidere -%>.

Exemplu de utilizare a șablonului

Să spunem că scriu un modul pentru gestionarea ZooKeeper. Clasa responsabilă pentru crearea configurației arată cam așa:

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'),
  }
}

Și șablonul asociat zoo.cfg.erb — arată astfel:

0 -%>

server.=:::



dataDir=


=

Fapte și variabile încorporate

Adesea, o parte specifică a configurației depinde de ceea ce se întâmplă în acel moment pe nod. De exemplu, în funcție de ce versiune a Debian este instalată, este necesar să instalați o anumită versiune a pachetului. Este posibil să urmăriți toate acestea manual, rescriind manifeștele în cazul schimbării nodurilor. Dar aceasta este o abordare neserioasă, automatizarea este mult mai bună.

Pentru a obține informații despre noduri în Puppet, există un mecanism numit fapte. Faptele sunt informații despre nod disponibilă în manifești sub formă de variabile obișnuite în spațiul de nume global. De exemplu, numele gazdei, versiunea sistemului de operare, arhitectura procesorului, lista de utilizatori, lista de interfețe de rețea și adresele acestora și multe, multe altele. Faptele sunt disponibile în manifești și template-uri ca variabile obișnuite.

Exemplu de lucru cu faptele:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# resursa de tip notify pur și simplu afișează un mesaj în log

Dacă vorbim formal, fiecare fapt are un nume (șir) și o valoare (sunt disponibile diverse tipuri: șiruri, aranjamente, dicționare). Există un set de fapte încorporate. De asemenea, este posibil să scrieți propriile fapte. Colectoarele de fapte sunt descrise ca funcții în Ruby, sau ca fișiere executabile.De asemenea, faptele pot fi prezentate sub formă de fișiere text cu date pe noduri.

În timpul funcționării, agentul Puppet copiază mai întâi de la serverul Puppet toate colectoarele de fapte disponibile pe nod, după care le rulează și trimite faptele colectate înapoi la server; abia după aceea serverul începe compilarea catalogului.

Faptele sub formă de fișiere executabile

Aceste fapte sunt plasate în module în directorul facts.d. Desigur, fișierele trebuie să fie executabile. La rulare, acestea trebuie să afișeze pe ieșirea standard informații fie în format YAML, fie în format „cheie=valoare”.

Nu uitați că faptele se răspândesc la toate nodurile care sunt gestionate de serverul Puppet pe care este implementat modulul dumneavoastră. De aceea, în script asigurați-vă că există toate programele și fișierele necesare funcționării faptului dumneavoastră.

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

Faptele în Ruby

Aceste fapte sunt plasate în module în directorul 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

Faptele textuale

Aceste fapte sunt plasate pe noduri în directorul /etc/facter/facts.d în vechiul Puppet sau /etc/puppetlabs/facts.d în noul Puppet.

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

Accesarea faptelor

Există două modalități de a apela faptele:

  • prin intermediul unui dicționar $facts: $facts['fqdn'];
  • folosind numele faptului ca nume de variabilă: $fqdn.

Cel mai bine este să folosești un dicționar $facts, dar este și mai bine să specifici spațiul de nume global ($::facts).

Iată secțiunea necesară din documentație.

Variabile încorporate

Pe lângă fapte, mai sunt câteva variabile, disponibile în spațiul global de nume.

  • fapte de încredere — variabile care provin din certificatul clientului (deoarece certificatul este de obicei emis pe serverul Puppet, agentul nu poate schimba pur și simplu certificatul său, prin urmare variabilele sunt considerate „de încredere”): numele certificatului, numele gazdei și domeniului, extensiile din certificat.
  • fapte ale serverului — variabile referitoare la informațiile despre server — versiune, nume, adresă IP a serverului, mediu.
  • fapte ale agentului — variabile adăugate direct de puppet-agent, și nu de facter — numele certificatului, versiunea agentului, versiunea Puppet-ului.
  • variabile master — variabilele puppetmaster-ului (sic!). Acolo sunt aproximativ aceleași informații ca în fapte ale serverului, plus valori disponibile pentru parametrii de configurare.
  • variabile ale compilatorului — variabile ale compilatorului, care variază în fiecare domeniu de vizibilitate: numele modulului curent și numele modulului în care a fost apelat obiectul curent. Acestea pot fi folosite, de exemplu, pentru a verifica că clasele tale private nu sunt utilizate direct din alte module.

Anexa 1: cum se lansează și se dezolvă tot acest lucru?

Articolul a avut multe exemple de cod Puppet, dar nu a fost spus deloc cum se lansează acest cod. Așadar, mă corectez.

Pentru a funcționa, Puppet are nevoie de agent, dar în majoritatea cazurilor, va fi necesar și serverul.

Agentul

Cel puțin de la versiunea 5, pachetele puppet-agent din repository-ul oficial Puppetlabs conțin toate dependențele (ruby și gem-urile corespunzătoare), așa că nu sunt dificultăți de instalare (vorbesc despre distribuțiile bazate pe Debian — distribuțiile bazate pe RPM nu sunt folosite de noi).

În cel mai simplu caz, pentru a aplica configurația puppet, este suficient să pornești agentul în modul fără server: cu condiția ca codul Puppet să fie copiat pe nod, pornești 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

Este mai bine să ridicați un server și să rulați agenții pe noduri în modul demon — astfel, din 30 în 30 de minute, aceștia vor aplica configurația descărcată de pe server.

Puteți imita modelul de lucru push — accesați nodul care vă interesează și rulați sudo puppet agent -t. Opțiunea -t (--test) include de fapt mai multe opțiuni, care pot fi activate și individual. Printre aceste opțiuni se numără:

  • a nu funcționa în modul demon (implicit, agentul rulează în modul demon);
  • a se opri după aplicarea catalogului (implicit, agentul va continua să funcționeze și va aplica configurația din 30 în 30 de minute);
  • a scrie un log detaliat al muncii;
  • a arăta modificările în fișiere.

Agentul are un mod de funcționare fără modificări — este util în cazul în care nu sunteți sigur că ați scris o configurație corectă și doriți să verificați ce va schimba agentul în timpul execuției. Acest mod se activează cu ajutorul parametrului --noop în linia de comenzi: sudo puppet agent -t --noop.

De asemenea, puteți activa un log de depanare — în el, puppet scrie despre toate acțiunile pe care le execută: despre resursa pe care o procesează în acel moment, despre parametrii acestei resurse, despre ce programe rulează. Desigur, acesta este parametrul --debug.

Server

Nu voi discuta în această articol despre configurarea completă a serverului puppet și despre desfășurarea codului pe acesta, voi menționa doar că o versiune funcțională a serverului este disponibilă din cutie, fără a necesita configurații suplimentare pentru a funcționa în condiții de număr mic de noduri (să spunem, până la o sută). Un număr mai mare de noduri va necesita ajustări — implicit, puppetserver rulează nu mai mult de patru lucrători, pentru o performanță mai bună trebuie să creșteți numărul lor și să nu uitați să creșteți limitele de memorie, altfel serverul va petrece majoritatea timpului în procesul de colectare a gunoiului.

Desfășurarea codului — dacă doriți ceva rapid și simplu, atunci consultați (r10k)[https://github.com/puppetlabs/r10k] pentru instalații mici, ar trebui să fie suficient.

Aducere aminte 2: recomandări pentru scrierea de cod

  1. Scoateți toată logica în clase și definiții.
  2. Păstrați clasele și definițiile în module, nu în manifesturi cu descrierea nodurilor.
  3. Utilizați faptele.
  4. Nu faceți if-uri după numele gazdelor.
  5. Nu ezitați să adăugați parametrii pentru clase și definiții — este mai bine decât logica implicită ascunsă în corpul clasei/definiției.

Și de ce recomand să facem așa — voi explica în următorul articol.

Concluzie

Pe acesta îl încheiem cu introducerea. În următorul articol voi vorbi despre Hiera, ENC și PuppetDB.

Numai utilizatorii înregistrați pot participa la sondaj. Conectați-vă, vă rugăm.

De fapt, materialul este mult mai mult — pot scrie articole pe următoarele teme, votați ce v-ar interesa să citiți:

  • 59,1%Concepte avansate Puppet — lucruri de nivel superior: bucle, mapări și alte expresii lambda, colecționare de resurse, resurse exportate și interacțiunea între gazde prin Puppet, etichete, provideri, tipuri abstracte de date.
  • 31,8%„Sunt administrator de la mama” sau cum am îmblânzit câteva servere Puppet de versiuni diferite la Avito, și, în principiu, o parte despre administrarea serverului Puppet.
  • 81,8%Cum scriem cod Puppet: legătura instrumentală, documentație, testare, CI/CD.

Au votat 22 de utilizatori. 9 utilizatori s-au abținut.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster