Puppet to system zarządzania konfiguracją. Jest używany do przystosowywania hostów do wymaganego stanu oraz utrzymywania tego stanu.
Pracuję z Puppet od ponad pięciu lat. Ten tekst jest w zasadzie przetłumaczoną i uporządkowaną kompilacją kluczowych punktów z oficjalnej dokumentacji, która pozwoli nowicjuszom szybko zrozumieć istotę Puppet.

Podstawowe informacje
Schemat działania Puppet to model klient-serwer, chociaż wspierany jest również wariant pracy bez serwera z ograniczoną funkcjonalnością.
Używana jest model pull: domyślnie co pół godziny klienci łączą się z serwerem w poszukiwaniu konfiguracji i ją stosują. Jeśli pracowałeś z Ansible, tam stosowany jest inny, model push: administrator inicjuje proces stosowania konfiguracji, klienci sami z siebie nic nie zastosują.
Podczas interakcji sieciowej stosowane jest dwukierunkowe szyfrowanie TLS: serwer i klient mają swoje prywatne klucze i odpowiednie certyfikaty. Zwykle serwer wydaje certyfikaty dla klientów, ale możliwe jest również użycie zewnętrznego CA.
Wprowadzenie do manifestów
W terminologii Puppet do serwera Puppet łączą się węzły (nodes). Konfiguracja dla nodów pisana jest w manifestach w specjalnym języku programowania – Puppet DSL.
Puppet DSL to język deklaratywny. Opisuje on pożądany stan nodu w postaci deklaracji pojedynczych zasobów, na przykład:
- Plik istnieje i ma określoną zawartość.
- Pakiet jest zainstalowany.
- Usługa jest uruchomiona.
Zasoby mogą być ze sobą powiązane:
- Istnieją zależności, które wpływają na kolejność stosowania zasobów.
Na przykład, "najpierw zainstaluj pakiet, następnie popraw plik konfiguracyjny, a potem uruchom usługę." - Są powiadomienia – jeśli zasób się zmienia, wysyła powiadomienia do zasobów, które się na niego subskrybowały.
Na przykład, jeśli zmienia się plik konfiguracyjny, można automatycznie zrestartować usługę.
Ponadto, w Puppet DSL znajdują się funkcje i zmienne, a także operatory warunkowe i selektory. Wspierane są również różne mechanizmy szablonowania – EPP i ERB.
Puppet napisany jest w Ruby, dlatego wiele konstrukcji i terminów pochodzi stamtąd. Ruby umożliwia rozszerzanie Puppet – dodawanie złożonej logiki, nowych typów zasobów, funkcji.
Podczas działania Puppet manifesty dla każdej konkretnej nody na serwerze są kompilowane do katalogu. Katalog — to lista zasobów i ich powiązań po obliczeniu wartości funkcji, zmiennych oraz rozwinięciu operatorów warunkowych.
Składnia i styl kodu
Oto sekcje oficjalnej dokumentacji, które pomogą zrozumieć składnię, jeśli podane przykłady będą niewystarczające:
Oto przykład tego, jak wygląda manifest:
# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
# Конфигурация по сути является перечислением ресурсов и их параметров.
#
# У каждого ресурса есть тип и название.
#
# Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
#
# Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
# Про разные типы ресурсов написано ниже.
#
# После типа в фигурных скобках пишется название ресурса, потом двоеточие,
# дальше идёт опциональное перечисление параметров ресурса и их значений.
# Значения параметров указываются через т.н. hash rocket (=>).
resource { 'title':
param1 => value1,
param2 => value2,
param3 => value3,
}
}Wcięcia i nowliny nie są obowiązkową częścią manifestu, jednak istnieje zalecany Podsumowanie:
- Podwójne wcięcia, tabulatory nie są używane.
- Klasy zamykające oddziela się spacją, dwukropek nie jest oddzielany spacją.
- Przecinki po każdym parametrze, w tym ostatnim. Każdy parametr – w osobnej linii. Wyjątek stanowią przypadki bez parametrów i z jednym parametrem: można pisać w jednej linii i bez przecinka (tzn.
resource { 'title': }iresource { 'title': param => value }). - Strzałki przy parametrach powinny być na tym samym poziomie.
- Strzałki powiązań zasobów pisane są przed nimi.
Lokalizacja plików na serwerze Puppet
Dla dalszych wyjaśnień wprowadzę pojęcie „katalog główny”. Katalog główny – to katalog, w którym znajduje się konfiguracja Puppet dla konkretnej nody.
Katalog główny różni się w zależności od wersji Puppet oraz używanych środowisk. Środowiska – to niezależne zestawy konfiguracji, które są przechowywane w oddzielnych katalogach. Zwykle używane w połączeniu z gitem, w takim przypadku środowiska są tworzone z gałęzi gita. Odpowiednio, każda noda znajduje się w tym czy innym środowisku. Jest to konfigurowane na samej nodzie lub w ENC, o czym opowiem w następnej części.
- W trzeciej wersji („stary Puppet”) katalogiem bazowym była
/etc/puppet. Użycie środowisk jest opcjonalne – my na przykład ich nie używamy ze starym Puppetem. Jeśli środowiska są używane, zazwyczaj przechowywane są w/etc/puppet/environments, katalogiem głównym będzie katalog środowiska. Jeśli środowiska nie są używane, katalogiem głównym będzie bazowy. - Zaczynając od czwartej wersji („nowy Puppet”) użycie środowisk stało się obowiązkowe, a katalog bazowy przeniesiono do
/etc/puppetlabs/code. Odpowiednio, środowiska są przechowywane w/etc/puppetlabs/code/environments, katalog główny to katalog środowiska.
W katalogu głównym powinna znajdować się podkatalog manifests, w którym leży jeden lub więcej manifestów opisujących węzły. Ponadto, powinna być tam podkatalog modules, w którym znajdują się moduły. Co to są moduły, opowiem nieco później. Ponadto, w starej wersji Pappera również może znajdować się podkatalog files, w którym leżą różne pliki, które kopiujemy na węzły. W nowej wersji Pappera wszystkie pliki zostały przeniesione do modułów.
Pliki manifestów mają rozszerzenie .pp.
Para przykładowych przypadków
Opis węzła i zasobu na nim
Na węźle server1.testdomain powinien być utworzony plik /etc/issue z zawartością Debian GNU/Linux n l. Plik powinien należeć do użytkownika i grupy root, uprawnienia dostępu powinny być 644.
Piszemy manifest:
node 'server1.testdomain' { # blok konfiguracji dotyczący węzła server1.testdomain
file { 'etc/issue': # opisujemy plik /etc/issue
ensure => present, # ten plik musi istnieć
content => 'Debian GNU/Linux n l', # jego zawartość powinna być taka
owner => root, # właściciel pliku
group => root, # grupa właściciela
mode => '0644', # uprawnienia do pliku. Określone są jako ciąg (w cudzysłowach), ponieważ w przeciwnym razie liczba z zerem na początku będzie traktowana jako zapisana w systemie ósemkowym, co spowoduje niepożądane efekty
}
}Zależności zasobów na węźle
Na węźle server2.testdomain powinien mieć uruchomiony nginx z wcześniej przygotowaną konfiguracją.
Dekonstruujemy zadanie:
- Należy, aby zainstalowano pakiet
nginx. - Należy, aby skopiowane zostały pliki konfiguracyjne z serwera.
- Należy, aby uruchomiona została usługa
nginx. - W przypadku aktualizacji konfiguracji należy zrestartować usługę.
Piszemy manifest:
węzeł 'server2.testdomain' { # blok konfiguracji związany z węzłem server2.testdomain
pakiet { 'nginx': # opisujemy pakiet nginx
zapewnij => zainstalowane, # musi być zainstalowane
}
# Prosta strzałka (-&) wskazuje, że zasób poniżej musi
# zostać utworzony po zasobie opisanym powyżej.
# Te zależności są transitive.
-> plik { '/etc/nginx': # opisujemy plik /etc/nginx
zapewnij => katalog, # to musi być katalog
źródło => 'puppet:///modules/example/nginx-conf', # jego zawartość należy wziąć z serwera puppet pod podanym adresem
rekurencja => prawda, # kopiuj pliki rekurencyjnie
oczyszczenie => prawda, # należy usunąć niepotrzebne pliki (te, których nie ma w źródle)
wymuszenie => prawda, # usuń niepotrzebne katalogi
}
# Falista strzałka (~&) wskazuje, że zasób poniżej musi
# subskrybować zmiany zasobu opisanego powyżej.
# Falista strzałka obejmuje prostą (->).
~> usługa { 'nginx': # opisujemy usługę nginx
zapewnij => uruchomione, # musi być uruchomiona
włącz => prawda, # musi być uruchamiana automatycznie przy starcie systemu
}
# Kiedy zasób typu usługa otrzymuje powiadomienie,
# odpowiednia usługa jest restartowana.
}Aby to działało, potrzebne jest mniej więcej takie rozmieszczenie plików na serwerze puppet:
/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│ └── site.pp
└── modules/
└── example/
└── files/
└── nginx-conf/
├── nginx.conf
├── mime.types
└── conf.d/
└── some.confTypy zasobów
Pełna lista obsługiwanych typów zasobów znajduje się , tutaj opiszę pięć podstawowych typów, które w mojej praktyce wystarczają do rozwiązania większości zadań.
plik
Zarządza plikami, katalogami, symlinkami, ich zawartością, prawami dostępu.
Parametry:
- nazwa zasobu — ścieżka do pliku (opcjonalnie)
- path — ścieżka do pliku (jeśli nie jest określona w nazwie)
- zapewnij — typ pliku:
nieobecny— usuń plikobecny— musi być plik dowolnego typu (jeśli pliku nie ma, zostanie utworzony zwykły plik)plik— zwykły plikkatalog— kataloglink— symlink
- zawartość — zawartość pliku (odpowiednie tylko dla zwykłych plików, nie można używać razem z source lub target)
- source — link do ścieżki, z której należy skopiować zawartość pliku (nie można używać razem z zawartość lub target). Może być określona zarówno jako URI z schematem
puppet:(wtedy użyte zostaną pliki z serwera puppet), jak i ze schematemhttp:(mam nadzieję, że jest jasne, co będzie w tym przypadku), a nawet ze schematemfile:lub jako absolutna ścieżka bez schematu (wtedy użyty zostanie plik z lokalnego systemu plików na węźle) - target — dokąd powinien wskazywać symlink (nie można używać razem z zawartość lub source)
- owner — użytkownik, któremu powinien należeć plik
- grupa — grupa, której powinien należeć plik
- tryb — prawa do pliku (w postaci ciągu)
- rekurencja — włącza rekurencyjne przetwarzanie katalogów
- oczyszczanie — włącza usuwanie plików, które nie są opisane w Puppet
- wymuś — włącza usuwanie katalogów, które nie są opisane w Puppet
pakiet
Instaluje i usuwa pakiety. Potrafi przetwarzać powiadomienia — reinstaluje pakiet, jeśli podany został parametr reinstalacja_po_odświeżeniu.
Parametry:
- nazwa zasobu — nazwa pakietu (opcjonalnie)
- name — nazwa pakietu (jeśli nie podano w nazwie)
- dostawca — menedżer pakietów, którego należy użyć
- zapewnij — pożądany stan pakietu:
obecny,installed— zainstalowana dowolna wersjalatest— zainstalowana najnowsza wersjanieobecny— usunięty (apt-get remove)oczyszczony— usunięty wraz z plikami konfiguracyjnymi (apt-get purge)zatrzymany— wersja pakietu zablokowana (apt-mark hold)jakikolwiek inny ciąg— zainstalowana określona wersja
- reinstalacja_po_odświeżeniu — jeśli
true, to przy otrzymaniu powiadomienia pakiet zostanie zreinstalowany. Przydatne w dystrybucjach opartych na źródłach, gdzie przebudowa pakietów może być konieczna w przypadku zmiany parametrów budowy. Domyślniefalse.
service
Zarządza usługami. Potrafi przetwarzać powiadomienia — restartuje usługę.
Parametry:
- nazwa zasobu — usługa, którą należy zarządzać (opcjonalnie)
- name — usługa, którą należy zarządzać (jeśli nie podano w nazwie)
- zapewnij — pożądany stan usługi:
bieganie— uruchomionazatrzymana— zatrzymana
- włącz — zarządza możliwością uruchamiania usługi:
true— włączony autostart (systemctl enable)maskuj— zmaskowany (systemctl mask)false— wyłączony autostart (systemctl disable)
- restart — komenda do restartowania usługi
- status — komenda do sprawdzania statusu usługi
- hasrestart — określić, czy skrypt inicjujący usługi obsługuje restart. Jeśli
falsei podany parametr restart — używane jest wartość tego parametru. Jeślifalsei parametr restart nie jest podany — usługa zatrzymuje się i jest ponownie uruchamiana (ale w systemd używana jest komendasystemctl restart). - hasstatus — określić, czy skrypt inicjujący usługi obsługuje komendę
status. Jeślifalse, to używana jest wartość parametru status. Domyślnietrue.
exec
Uruchamia zewnętrzne komendy. Jeśli nie podano parametrów tworzy, tylko jeśli, chyba że lub odśwież tylko, komenda będzie uruchamiana przy każdym przebiegu Puppeta. Potrafi przetwarzać powiadomienia — uruchamia komendę.
Parametry:
- nazwa zasobu — komenda, którą należy wykonać (opcjonalnie)
- komenda — komenda, którą należy wykonać (jeśli nie podano w nazwie)
- path — ścieżki, w których można szukać pliku wykonywalnego
- tylko jeśli — jeśli polecenie wskazane w tym parametrze zakończyło się zerowym kodem zwrotu, zostanie wykonane polecenie główne
- chyba że — jeśli polecenie wskazane w tym parametrze zakończyło się niezerowym kodem zwrotu, zostanie wykonane polecenie główne
- tworzy — jeśli wskazany w tym parametrze plik nie istnieje, zostanie wykonane polecenie główne
- odśwież tylko — jeśli
true, polecenie zostanie uruchomione tylko wtedy, gdy ten exec otrzyma powiadomienie od innych zasobów - cwd — katalog, z którego uruchamiać polecenie
- użytkownik — użytkownik, który uruchamia polecenie
- dostawca — za pomocą czego uruchamiać polecenie:
- posix — po prostu tworzy proces potomny, należy wskazać path
- shell — polecenie uruchamiane w shellu
/bin/sh, można nie podawać path, można używać globbingu, rurek i innych funkcji shell. Zwykle określane automatycznie, jeśli występują różne znaki specjalne (|,;,&&,||itd.
cron
Zarządza zadaniami cron.
Parametry:
- nazwa zasobu — po prostu jakiś identyfikator
- zapewnij — stan zadania cron:
obecny— utworzyć, jeśli nie istniejenieobecny— usunąć, jeśli istnieje
- komenda — jakie polecenie uruchomić
- environment — w jakim środowisku uruchomić polecenie (lista zmiennych środowiskowych i ich wartości oddzielona
=) - użytkownik — od jakiego użytkownika uruchomić polecenie
- minute, hour, weekday, month, monthday — kiedy uruchomić cron. Jeśli któryś z tych atrybutów nie jest podany, jego wartością w kronie będzie
*.
W Puppet 6.0 cron jakby w puppetserver, dlatego nie ma dokumentacji na wspólnej stronie. Ale on w puppet-agent, więc nie ma potrzeby instalowania go osobno. Dokumentację na jego temat można znaleźć , albo .
O zasobach ogólnie
Wymagania dotyczące unikalności zasobów
Najczęstszy błąd, z którym się spotykamy — Duplicate declaration. Ten błąd występuje, gdy do katalogu trafiają dwa lub więcej zasobów tego samego typu o takiej samej nazwie.
Dlatego jeszcze raz powiem: w manifestach dla jednej nody nie powinno być zasobów tego samego typu o takiej samej nazwie (title)!
Czasami konieczne jest zainstalowanie pakietów o tej samej nazwie, ale przy użyciu różnych menedżerów pakietów. W takim przypadku należy używać parametru name, aby uniknąć błędu:
package { 'ruby-mysql':
ensure => installed,
name => 'mysql',
provider => 'gem',
}
package { 'python-mysql':
ensure => installed,
name => 'mysql',
provider => 'pip',
}W innych typach zasobów znajdują się podobne parametry, które pomagają unikać duplikacji, — name u service, komenda u exec, i tak dalej.
Metaparametry
Każdy typ zasobu ma swoje specjalne parametry, niezależnie od jego charakterystyki.
Pełna lista metaparametrów .
Krótka lista:
- require — w tym parametrze określa się, od jakich zasobów zależy dany zasób.
- before — w tym parametrze określa się, które zasoby są zależne od danego zasobu.
- subscribe — w tym parametrze określa się, z jakich zasobów dany zasób otrzymuje powiadomienia.
- notify — w tym parametrze określa się, które zasoby otrzymują powiadomienia od danego zasobu.
Wszystkie wymienione metaparametry przyjmują albo jedną referencję do zasobu, albo tablicę referencji w nawiasach kwadratowych.
Referencje do zasobów
Referencja do zasobu to po prostu wspomnienie zasobu. Służą głównie do określania zależności. Referencja do nieistniejącego zasobu spowoduje błąd kompilacji.
Składnia referencji jest następująca: typ zasobu z dużej litery (jeśli w nazwie typu znajdują się podwójne dwukropki, to każda część nazwy pomiędzy dwukropkami pisana jest z dużej litery), następnie w nawiasach kwadratowych nazwa zasobu (wielkość liter nie ulega zmianie!). Nie może być spacji, nawiasy kwadratowe piszemy bezpośrednio po nazwie typu.
Przykład:
file { '/file1': ensure => present }
file { '/file2':
ensure => directory,
before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']Zależności i powiadomienia
Jak już wspomniano, proste zależności między zasobami są przechodnie. Uważaj przy ustalaniu zależności — można stworzyć cykliczne zależności, co spowoduje błąd kompilacji.
W przeciwieństwie do zależności, powiadomienia nie są przechodnie. Dla powiadomień obowiązują następujące zasady:
- Jeśli zasób otrzymuje powiadomienie, jest aktualizowany. Działania przy aktualizacji zależą od typu zasobu — exec uruchamia polecenie, service ponownie uruchamia usługę, pakiet przeinstalowuje pakiet. Jeśli dla zasobu nie określono działania przy aktualizacji, to nic się nie dzieje.
- W jednej iteracji Puppeta zasób jest aktualizowany nie więcej niż raz. Jest to możliwe, ponieważ powiadomienia obejmują zależności, a graf zależności nie zawiera cykli.
- Jeśli Puppet zmienia stan zasobu, zasób wysyła powiadomienia wszystkim zasobom, które się na niego subskrybowały.
- Jeśli zasób jest aktualizowany, wysyła powiadomienia wszystkim zasobom, które się na niego subskrybowały.
Obsługa nieokreślonych parametrów
Zazwyczaj, jeśli jakiś parametr zasobu nie ma wartości domyślnej i nie jest określony w manifeście, Puppet nie zmieni tej właściwości odpowiedniego zasobu na węźle. Na przykład, jeśli zasób typu plik nie ma określonego parametru owner, Puppet nie zmieni właściciela odpowiedniego pliku.
Wprowadzenie do klas, zmiennych i definicji
Załóżmy, że mamy kilka węzłów, na których znajduje się taka sama część konfiguracji, ale są też różnice — w przeciwnym razie moglibyśmy opisać to wszystko w jednym bloku node {}. Oczywiście, można po prostu skopiować identyczne części konfiguracji, ale w ogólnym przypadku to złe rozwiązanie — konfiguracja rośnie, a przy zmianie wspólnej części konfiguracji trzeba będzie edytować to samo w wielu miejscach. Łatwo popełnić błąd, a zasada DRY (don’t repeat yourself) nie bez powodu została wymyślona.
Aby rozwiązać ten problem, istnieje taka konstrukcja jak klasa.
Klasy
— to nazwany blok kodu Puppet. Klasy są potrzebne do ponownego użycia kodu.
Najpierw klasę należy opisać. Samo w sobie opisanie nie dodaje żadnych zasobów. Klasa jest opisywana w manifestach:
# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
...
}Po tym klasa może być używana:
# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальшеPrzykład z poprzedniego zadania — wydzielmy instalację i konfigurację nginx do klasy:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => 'puppet:///modules/example/nginx-conf',
recurse => true,
purge => true,
force => true,
}
-> service { 'nginx':
ensure => running,
enable => true,
}
}
node 'server2.testdomain' {
include nginx_example
}Zmienne
Klasa z poprzedniego przykładu jest zupełnie nieelastyczna, ponieważ zawsze przynosi tę samą konfigurację nginx. Zróbmy tak, aby ścieżka do konfiguracji stała się zmienną, wtedy ta klasa będzie mogła być używana do instalacji nginx z dowolną konfiguracją.
Można to zrobić .
Uwaga: zmienne w Puppet są niemodyfikowalne!
Ponadto można odwoływać się do zmiennej tylko po jej zadeklarowaniu, w przeciwnym razie wartością zmiennej będzie undef.
Przykład pracy z zmiennymi:
# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"W Puppet są przestrzenie nazw, a zmienne mają zakres widoczności: zmienna o tej samej nazwie może być zdefiniowana w różnych przestrzeniach nazw. Przy rozwiązywaniu wartości zmiennej najpierw szuka się jej w bieżącej przestrzeni nazw, a potem w przestrzeni obejmującej, i tak dalej.
Przykłady przestrzeni nazw:
- globalna — zmienne poza opisem klasy lub węzła trafiają tutaj;
- przestrzeń nazw węzła w opisie węzła;
- przestrzeń nazw klasy w opisie klasy.
Aby uniknąć niejednoznaczności przy odwoływaniu się do zmiennej, można wskazać przestrzeń nazw w nazwie zmiennej:
# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::varUmówmy się, że ścieżka do konfiguracji nginx będzie zawarta w zmiennej $nginx_conf_source. Klasa będzie wyglądać następująco:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => $nginx_conf_source, # tutaj używamy zmiennej zamiast stałego ciągu
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
}Jednakże przedstawiony przykład ma wadę, ponieważ istnieje pewna "ukryta wiedza" na temat tego, że gdzieś wewnątrz klasy używana jest zmienna o danej nazwie. O wiele lepiej byłoby uczynić tę wiedzę ogólną — klasy mogą mieć parametry.
Parametry klasy — to zmienne w przestrzeni nazw klasy, są określane w nagłówku klasy i mogą być używane jak zwykłe zmienne w ciele klasy. Wartości parametrów są podawane podczas użycia klasy w manifeście.
Parametrowi można nadać wartość domyślną. Jeśli parametr nie ma wartości domyślnej i wartość nie jest podana podczas użycia, spowoduje to błąd kompilacji.
Zparametryzujmy klasę z powyższego przykładu i dodajmy dwa parametry: pierwszy, obowiązkowy — ścieżkę do konfiguracji, i drugi, opcjonalny — nazwę pakietu z nginx (na przykład w Debianie są pakiety 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', # задаём параметры класса точно так же, как параметры для других ресурсов
}
}W Puppet zmienne są typowane. Istnieje . Typy danych są zazwyczaj używane do walidacji wartości parametrów przekazywanych do klas i definicji. Jeśli przekazany parametr nie odpowiada wskazanemu typowi, wystąpi błąd kompilacji.
Typ jest zapisany bezpośrednio przed nazwą parametru:
klasa przykład (
String $param1,
Integer $param2,
Array $param3,
Hash $param4,
Hash[String, String] $param5,
) {
...
}Klasy: include classname vs class{‘classname’:}
Każda klasa jest zasobem typu class. Tak jak w przypadku innych typów zasobów, nie mogą istnieć dwa egzemplarze tej samej klasy na jednej nodzie.
Jeżeli spróbujesz dodać klasę na tę samą nodę dwa razy przy użyciu class { 'classname':} (niezależnie od tego, czy parametry są różne czy takie same), wystąpi błąd kompilacji. Natomiast w przypadku użycia klasy w stylu zasobu można od razu w manifeście ustawić wszystkie jej parametry.
Jednak jeśli użyjesz include, to klasę można dodawać dowolną ilość razy. Chodzi o to, że include jest funkcją idempotentną, która sprawdza, czy klasa została dodana do katalogu. Jeśli klasa nie znajduje się w katalogu — dodaje ją, a jeśli już jest, nic nie robi. Ale w przypadku użycia include nie można ustawić parametrów klasy podczas jej deklaracji — wszystkie obowiązkowe parametry muszą być ustawione w zewnętrznym źródle danych — Hiera lub ENC. O tym porozmawiamy w następnym artykule.
Definice
Jak wspomniano w poprzednim bloku, ta sama klasa nie może występować na nodzie więcej niż raz. Jednak w niektórych przypadkach konieczne jest zastosowanie tego samego bloku kodu z różnymi parametrami na jednej nodzie. Innymi słowy, istnieje potrzeba stworzenia własnego typu zasobu.
Na przykład, aby zainstalować moduł PHP, robimy w Avito następująco:
- Instalujemy pakiet z tym modułem.
- Tworzymy plik konfiguracyjny dla tego modułu.
- Tworzymy symlink na konfigurację dla php-fpm.
- Tworzymy symlink na konfigurację dla php cli.
W takich przypadkach używa się konstrukcji zwanej (definować, zdefiniowany typ, zdefiniowany typ zasobu). Defin jest podobny do klasy, ale są różnice: po pierwsze, każdy defin jest typem zasobu, a nie zasobem; po drugie, każdy defin ma domyślny parametr $title, do którego trafia nazwa zasobu podczas jego deklaracji. Tak samo jak w przypadku klas, defin najpierw trzeba opisać, a następnie można go używać.
Uproszczony przykład z modułem do 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'], # debian php packages triggers create symlinks and restart the php-fpm service themselves - we don't need this, as we manage both the symlinks and the service using 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' }
}Najłatwiej złapać błąd Duplicate declaration w definicji. Dzieje się tak, jeśli w definicji znajduje się zasób z stałą nazwą, a na jakimś węźle jest dwa lub więcej egzemplarzy tej definicji.
Aby się przed tym uchronić, wszystkie zasoby wewnątrz definicji powinny mieć nazwy zależne od $title. Alternatywnie — idempotentne dodawanie zasobów, w najprostszej wersji wystarczy wydzielić wspólne dla wszystkich egzemplarzy zasoby do osobnej klasy i załączyć tę klasę w definicji — funkcja include jest idempotentna.
Są także inne sposoby osiągnięcia idempotentności przy dodawaniu zasobów, a mianowicie używanie funkcji defined i ensure_resources, ale o tym opowiem w następnej serii.
Zależności i powiadomienia dla klas i definicji
Klasy i definicje dodają następujące zasady do przetwarzania zależności i powiadomień:
- Zależność od klasy/definicji dodaje zależności od wszystkich zasobów klasy/definicji;
- Zależność klasy/definicji dodaje zależności wszystkim zasobom klasy/definicji;
- Powiadomienie klasy/definicji powiadamia wszystkie zasoby klasy/definicji;
- Subskrypcja klasy/definicji subskrybuje wszystkie zasoby klasy/definicji.
Operatory warunkowe i selektory
if
Tu wszystko jest proste:
if WYRAŻENIE1 {
...
} elsif WYRAŻENIE2 {
...
} else {
...
}chyba że
unless — to odwrotność if: blok kodu zostanie wykonany, jeśli wyrażenie jest fałszywe.
unless WYRAŻENIE {
...
}case
Tu też nie ma nic skomplikowanego. Jako wartości można użyć zwykłych wartości (ciągów, liczb itp.), wyrażeń regularnych, a także typów danych.
case WYRAŻENIE {
WARTOŚĆ1: { ... }
WARTOŚĆ2, WARTOŚĆ3: { ... }
default: { ... }
}Selektory
Selektor to konstrukcja językowa, podobna do case, tylko zamiast wykonywania bloku kodu zwraca wartość.
$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }Moduły
Kiedy konfiguracja jest mała, łatwo ją utrzymać w jednym manifeście. Ale im więcej konfiguracji opisujemy, tym więcej klas i węzłów pojawia się w manifeście, co powoduje, że staje się on nieporęczny.
Ponadto pojawia się problem ponownego wykorzystania kodu - gdy cały kod jest w jednym manifeście, trudno jest się nim dzielić z innymi. Aby rozwiązać te dwa problemy, w Puppet istnieje jednostka zwana modułami.
Moduły — to zestawy klas, definicji i innych jednostek Puppet, przeniesione do osobnego katalogu. Innymi słowy, moduł to niezależny kawałek logiki Puppet. Na przykład, może istnieć moduł do pracy z nginx, który będzie zawierał tylko to, co jest potrzebne do pracy z nginx, a także może być moduł do pracy z PHP, i tak dalej.
Moduły są wersjonowane, a także wspierają zależności między sobą. Istnieje otwarty repozytorium modułów — .
Na serwerze Puppet moduły znajdują się w podkatalogu modules w katalogu głównym. Wewnątrz każdego modułu obowiązuje standardowy schemat katalogów — manifests, files, templates, lib i tak dalej.
Struktura plików w module
W katalogu głównym modułu mogą znajdować się następujące katalogi o mówiących nazwach:
manifests— zawiera manifestyfiles— zawiera plikitemplates— zawiera szablonylib— zawiera kod Ruby
To nie jest pełna lista katalogów i plików, ale na potrzeby tego artykułu wystarczy.
Nazwy zasobów i nazwy plików w module
Zasoby (klasy, definicje) w module nie mogą być nazywane dowolnie. Ponadto istnieje bezpośrednie powiązanie między nazwą zasobu a nazwą pliku, w którym Puppet będzie szukał opisu tego zasobu. Jeśli naruszysz zasady nazw, Puppet po prostu nie znajdzie opisu zasobów, co spowoduje błąd kompilacji.
Zasady są proste:
- Wszystkie zasoby w module muszą znajdować się w przestrzeni nazw modułu. Jeśli moduł nosi nazwę
foo, to wszystkie zasoby w nim muszą nazywać sięfoo::, lub po prostufoo. - Zasób o nazwie modułu musi znajdować się w pliku
init.pp. - Dla pozostałych zasobów schemat nazw plików jest następujący:
- prefiks z nazwą modułu jest odrzucany
- wszystkie podwójne dwukropki, jeśli występują, są zastępowane ukośnikami
- dodawane jest rozszerzenie
.pp
Pokażę na przykładzie. Załóżmy, że piszę moduł nginx. Zawiera następujące zasoby:
- klasa
nginxopisana w manifeścieinit.pp; - klasa
nginx::serviceopisana w manifeścieservice.pp; - defin
nginx::serveropisana w manifeścieserver.pp; - defin
nginx::server::locationopisana w manifeścieserver/location.pp.
Szablony
Z pewnością wiesz, czym są szablony, nie będę szczegółowo tego opisywać. Ale na wszelki wypadek zostawię .
Jak korzystać z szablonów: wartość szablonu można ujawnić za pomocą funkcji template, której przekazywana jest ścieżka do szablonu. Dla zasobów typu plik używamy razem z parametrem zawartość. Na przykład tak:
file { '/tmp/example': content => template('modulename/templatename.erb')Ścieżka w postaci / zakłada plik /modules//templates/.
Ponadto istnieje funkcja inline_template — przyjmuje tekst szablonu, a nie nazwę pliku.
Wewnątrz szablonów można używać wszystkich zmiennych Puppet w bieżącym zakresie.
Puppet obsługuje szablony w formacie ERB i EPP:
Krótko o ERB
Konstrukcje sterujące:
<%= ВЫРАЖЕНИЕ %>— wstawia wartość wyrażenia<% ВЫРАЖЕНИЕ %>— oblicza wartość wyrażenia (nie wstawiając go). Tutaj zazwyczaj występują operatory warunkowe (if), pętle (each).<%# КОММЕНТАРИЙ %>
Wyrażenia w ERB pisane są w Ruby (właściwie ERB to Embedded Ruby).
Aby uzyskać dostęp do zmiennych z manifestu, trzeba dodać @ do nazwy zmiennej. Aby usunąć znak nowej linii, który pojawia się po konstrukcji sterującej, należy użyć zamykającego tagu -%>.
Przykład użycia szablonu
Załóżmy, że piszę moduł do zarządzania ZooKeeper. Klasa odpowiadająca za tworzenie konfiguracji wygląda mniej więcej tak:
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'),
}
}A odpowiadający mu szablon zoo.cfg.erb — wygląda tak:
0 -%>
server.={:::}
dataDir=
={Fakty i wbudowane zmienne
Konkretną część konfiguracji często warunkuje to, co aktualnie dzieje się na nodzie. Na przykład, w zależności od zainstalowanej wersji Debiana, konieczne jest zainstalowanie odpowiedniej wersji pakietu. Można to wszystko śledzić ręcznie, przepisując manifesty w przypadku zmian w nodach. Jednak takie podejście jest niewłaściwe, automatyzacja jest znacznie lepsza.
Aby uzyskać informacje o nodach w Puppet, istnieje taki mechanizm, jak fakty. Fakty to informacje o nodzie, dostępne w manifestach jako zwykłe zmienne w globalnej przestrzeni nazw. Na przykład, nazwa hosta, wersja systemu operacyjnego, architektura procesora, lista użytkowników, lista interfejsów sieciowych i ich adresów, i wiele, wiele innych. Fakty są dostępne w manifestach i szablonach jako zwykłe zmienne.
Przykład pracy z faktami:
notify { "Uruchomiony OS ${facts['os']['name']} wersja ${facts['os']['release']['full']}": }
# Zasób typu notify po prostu wypisuje wiadomość w loguMówiąc formalnie, fakt ma nazwę (ciąg) i wartość (dostępne są różne typy: ciągi, tablice, słowniki). Istnieje . Można też pisać własne. Zbieracze faktów są opisane , lub jako . Fakty mogą być również reprezentowane w postaci na nodach.
W trakcie działania agenta Puppet, najpierw kopiuje on z serwera Puppet wszystkie dostępne zbieracze faktów na nodę, następnie je uruchamia i wysyła zebrane fakty na serwer; dopiero potem serwer zaczyna kompilację katalogu.
Fakty w postaci plików wykonywalnych
Takie fakty umieszczane są w modułach w katalogu facts.d. Oczywiście, pliki muszą być wykonywalne. Po uruchomieniu powinny wypisywać na standardowe wyjście informacje w formacie YAML lub w formacie "klucz=wartość".
Nie zapominaj, że fakty dotyczą wszystkich nodów, które są zarządzane przez serwer Puppet, na który publikujesz swój moduł. Dlatego w skrypcie upewnij się, że w systemie są wszystkie niezbędne programy i pliki do działania twojego faktu.
#!/bin/sh
echo "testfact=success"#!/bin/sh
echo '{"testyamlfact":"success"}'Fakty w Ruby
Takie fakty umieszczane są w modułach w katalogu 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
endFakty tekstowe
Takie fakty umieszczane są na nodach w katalogu /etc/facter/facts.d w starym Puppetcie lub /etc/puppetlabs/facts.d w nowym Puppetcie.
examplefact=examplevalue---
examplefact2: examplevalue2
anotherfact: anothervalueDostęp do faktów
Można odwołać się do faktów na dwa sposoby:
- przez słownik
$facts:$facts['fqdn']; - używając nazwy faktu jako nazwy zmiennej:
$fqdn.
Najlepiej używać słownika $facts, a jeszcze lepiej wskazać globalną przestrzeń nazw ($::facts).
Wbudowane zmienne
Oprócz faktów istnieją także , dostępne w globalnej przestrzeni nazw.
- trusted facts — zmienne pobierane z certyfikatu klienta (ponieważ certyfikat jest zazwyczaj wydawany na serwerze Puppet, agent nie może po prostu zmienić swojego certyfikatu, dlatego zmienne te są 'zaufane'): nazwa certyfikatu, nazwa hosta i domeny, rozszerzenia z certyfikatu.
- server facts — zmienne związane z informacjami o serwerze — wersja, nazwa, adres IP serwera, środowisko.
- agent facts — zmienne dodawane bezpośrednio przez puppet-agenta, a nie factera — nazwa certyfikatu, wersja agenta, wersja Puppeta.
- master variables — zmienne puppetmastera (sic!). Tam znajduje się mniej więcej to samo, co w server facts, plus dostępne wartości parametrów konfiguracyjnych.
- compiler variables — zmienne kompilatora, które różnią się w każdej przestrzeni widoku: nazwa bieżącego modułu i nazwa modułu, w którym dokonano odwołania do bieżącego obiektu. Można je używać, aby na przykład upewnić się, że prywatne klasy nie są bezpośrednio używane w innych modułach.
Dodatek 1: jak to wszystko uruchamiać i debugować?
W artykule było wiele przykładów kodu puppet, ale nie było w nim mowy o tym, jak ten kod uruchamiać. Cóż, poprawiam się.
Aby działać, Puppet potrzebuje jedynie agenta, ale w większości przypadków potrzebny będzie także serwer.
Agent
Co najmniej od piątej wersji pakiety puppet-agent z zawierają wszystkie zależności (ruby i odpowiednie gem'y), więc nie ma problemów z instalacją (mówię o dystrybucjach opartych na Debianie — dystrybucji opartych na RPM nie używamy).
W najprostszym przypadku, aby zastosować konfigurację puppet, wystarczy uruchomić agenta w trybie bezserwerowym: zakładając, że kod puppet został skopiowany na węzeł, uruchamiacie 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 secondsLepiej jest uruchomić serwer i uruchomić agenty na węzłach w trybie demona — wtedy co pół godziny będą stosować konfigurację pobraną z serwera.
Można symulować model push — wejść na interesujący Cię węzeł i uruchomić sudo puppet agent -t. Opcja -t (--test) tak naprawdę obejmuje kilka opcji, które można włączać i osobno. Oto niektóre z tych opcji:
- nie działać w trybie demona (domyślnie agent uruchamia się w trybie demona);
- zakończyć działanie po zastosowaniu katalogu (domyślnie agent kontynuuje pracę i będzie stosować konfigurację co pół godziny);
- zapisywać szczegółowy log działania;
- pokazywać zmiany w plikach.
Agent ma tryb pracy bez zmian — można z niego korzystać, gdy nie jesteś pewien, czy napisałeś poprawną konfigurację i chcesz sprawdzić, co dokładnie zmieni agent podczas pracy. Ten tryb włącza się parametrem --noop w linii poleceń: sudo puppet agent -t --noop.
Ponadto można włączyć tryb logowania debugowania — w nim puppet zapisuje wszystkie działania, które wykonuje: o zasobie, który obecnie przetwarza, o parametrach tego zasobu oraz o tym, jakie programy uruchamia. Oczywiście, to jest parametr --debug.
Serwer
Nie będę omawiać pełnej konfiguracji serwera puppet i wdrażania na niego kodu w tym artykule, powiem tylko, że z pudełka instalowana jest całkowicie działająca wersja serwera, która nie wymaga dodatkowej konfiguracji do pracy w warunkach niewielkiej liczby węzłów (powiedzmy do stu). Większa liczba węzłów będzie już wymagać tuningu — domyślnie puppetserver uruchamia nie więcej niż czterech pracowników, więc dla większej wydajności trzeba zwiększyć ich liczbę i nie zapomnieć o zwiększeniu limitów pamięci, w przeciwnym razie przez większą część czasu serwer będzie zajmował się garbage collection.
Wdrażanie kodu — jeśli potrzebujesz szybkiego i prostego rozwiązania, patrz (na r10k)[], dla niewielkich instalacji powinno to być wystarczające.
Uzupełnienie 2: zalecenia dotyczące pisania kodu
- Wydzielaj całą logikę do klas i definiuj.
- Trzymaj klasy i definicje w modułach, a nie w manifestach z opisem węzłów.
- Korzystaj z faktów.
- Nie rób if’ów na podstawie nazw hostów.
- Nie wahaj się dodawać parametrów do klas i definicji — to lepsze niż ukryta logika w ciele klasy/definicji.
A dlaczego polecam to robić — wyjaśnię w następnym artykule.
Podsumowanie
Na tym kończymy wprowadzenie. W następnym artykule opowiem o Hiera, ENC i PuppetDB.
Tylko zarejestrowani użytkownicy mogą brać udział w ankiecie. , proszę.
W rzeczywistości materiału jest znacznie więcej — mogę napisać artykuły na następujące tematy, zagłosujcie, o czym chcielibyście przeczytać:
- 59,1%Zaawansowane konstrukcje pupeta — niektóre rzeczy z wyższej półki: pętle, mapowanie i inne wyrażenia lambda, kolektory zasobów, zasoby eksportowane oraz interakcja między hostami poprzez Puppet, tagi, dostawcy, abstrakcyjne typy danych.13
- 31,8%«Ja u mamusi admin» czyli jak w Avito połączyliśmy kilka serwerów puppet różnych wersji, oraz w zasadzie część o administracji serwerem puppet.7
- 81,8%Jak piszemy kod puppet: narzędzia, dokumentacja, testowanie, CI/CD.18
Zagłosowało 22 użytkowników. 9 użytkowników wstrzymało się od głosu.
Źródło: habr.com
