Puppet er et konfigurationsstyringssystem. Det bruges til at bringe værter i den ønskede tilstand og opretholde denne tilstand.
Jeg har arbejdet med Puppet i over fem år. Denne tekst er i bund og grund en oversat og omordnet komprimering af nøglepunkter fra den officielle dokumentation, som vil hjælpe nybegyndere med hurtigt at forstå essensen af Puppet.

Grundlæggende information
Funktionsmåden for Puppet er klient-server, selvom der også understøttes en version uden server med begrænset funktionalitet.
Der anvendes en pull-model: som standard kontakter klienterne serveren hver halve time for at hente konfigurationen og anvende den. Hvis du har arbejdet med Ansible, så der anvendes en anden, push-model: administratoren initierer processen til at anvende konfigurationen, klienterne anvender ikke noget af sig selv.
Ved netværk interaktion anvendes tovejs TLS-kryptering: både serveren og klienten har deres egne private nøgler og de tilsvarende certifikater. Normalt udsteder serveren certifikater til klienterne, men det er i princippet muligt at bruge en ekstern CA.
Introduktion til manifester
I Puppet-terminologi til puppet-serveren forbindes noder (noder). Konfigurationen for noder skrives i manifester på et særligt programmeringssprog - Puppet DSL.
Puppet DSL er et deklarativt sprog. Her beskrives den ønskede tilstand for noden i form af erklæringer af individuelle ressourcer, for eksempel:
- Filen eksisterer, og den har et bestemt indhold.
- Pakken er installeret.
- Tjenesten kører.
Ressourcer kan være indbyrdes forbundne:
- Der er afhængigheder, de påvirker rækkefølgen af anvendelsen af ressourcer.
For eksempel, "installer først pakken, derefter rediger konfigurationsfilen, og til sidst start tjenesten." - Der er meddelelser - hvis en ressource ændres, sender den meddelelser til de ressourcer, der er tilmeldt den.
For eksempel, hvis konfigurationsfilen ændres, kan tjenesten automatisk genstartes.
Derudover har Puppet DSL funktioner og variabler samt betingede operatører og vælgere. Der understøttes også forskellige templating-mekanismer - EPP og ERB.
Puppet er skrevet i Ruby, så mange konstruktioner og termer er taget derfra. Ruby gør det muligt at udvide Puppet - skrive kompleks logik, nye typer ressourcer og funktioner.
Under operation, Puppet manifests for each specific node on the server are compiled into a directory. Mappe — this is a list of resources and their interrelations after computing the values of functions, variables, and revealing conditional operators.
Syntax and code style
Here are sections of the official documentation that will help you understand the syntax if the provided examples are insufficient:
Here is an example of how a manifest looks:
# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
# Конфигурация по сути является перечислением ресурсов и их параметров.
#
# У каждого ресурса есть тип и название.
#
# Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
#
# Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
# Про разные типы ресурсов написано ниже.
#
# После типа в фигурных скобках пишется название ресурса, потом двоеточие,
# дальше идёт опциональное перечисление параметров ресурса и их значений.
# Значения параметров указываются через т.н. hash rocket (=>).
resource { 'title':
param1 => value1,
param2 => value2,
param3 => value3,
}
}Whitespace and line breaks are not a mandatory part of the manifest, however, there is a recommended . Summary:
- Two-space indents, tabs are not used.
- Curly braces are separated by a space, colons are not separated by a space.
- Commas after each parameter, including the last. Each parameter should be on a separate line. An exception is made for cases without parameters and one parameter: it can be written on one line and without a comma (i.e.
resource { 'title': }ogresource { 'title': param => value }). - Arrow parameters should be at the same level.
- Resource relationship arrows are written before them.
File location on the puppet server
For further explanations, I introduce the concept of 'root directory'. The root directory is the directory where the Puppet configuration for a specific node is located.
The root directory varies depending on the Puppet version and the use of environments. Environments are independent sets of configurations that are stored in separate directories. They are usually used in conjunction with Git; in this case, environments are created from Git branches. Accordingly, each node is in one environment or another. This is configured on the node itself or in the ENC, which I will discuss in the next article.
- In the third version ('old Puppet'), the base directory was
/etc/puppet. The use of environments is optional—we, for example, do not use them with the old Puppet. If environments are used, they are typically stored in/etc/puppet/environments, the root directory will be the environment directory. If environments are not used, the root directory will be the base. - Starting from the fourth version ('new Puppet'), the use of environments became mandatory, and the base directory was moved to
/etc/puppetlabs/code. Miljøerne gemmes i/etc/puppetlabs/code/environments, rodmappen - mappen for miljøet.
I rodmappen skal der være en undermappe manifests, hvor der ligger en eller flere manifestfiler, der beskriver noderne. Derudover skal der være en undermappe modules, hvor modulerne ligger. Hvad moduler er, vil jeg forklare lidt senere. Desuden kan der i den gamle Puppet være en undermappe files, hvor der ligger forskellige filer, som vi kopierer til noderne. I den nye Puppet er alle filer dog flyttet til moduler.
Manifestfiler har filtypen .pp.
Et par praktiske eksempler
Beskrivelse af noden og ressourcen på den
På noden server1.testdomain skal der oprettes en fil /etc/issue med indholdet Debian GNU/Linux n l. Filen skal tilhøre brugeren og gruppen root, tilgangsrettighederne skal være 644.
Skriver manifest:
node 'server1.testdomain' { # konfigurationsblok, der tilhører noden server1.testdomain
file { '/etc/issue': # beskriver filen /etc/issue
ensure => present, # denne fil skal eksistere
content => 'Debian GNU/Linux n l', # den skal have dette indhold
owner => root, # ejer
group => root, # ejergruppe
mode => '0644', # rettigheder på filen. De er angivet som en streng (i citationstegn), fordi et tal med 0 i begyndelsen ellers vil blive opfattet som skrevet i oktal, og så går alt ikke som planlagt
}
}Relationer mellem ressourcer på noden
På noden server2.testdomain skal have nginx kørende med en forberedt konfiguration.
Dekomponerer opgaven:
- Der skal installeres en pakke
nginx. - Konfigurationsfilerne skal kopieres fra serveren.
- Der skal startes en service
nginx. - Ved opdatering af konfigurationen skal tjenesten genstartes.
Skriver manifest:
node 'server2.testdomain' { # konfiguration blok der tilhører noden server2.testdomain
package { 'nginx': # beskriver pakken nginx
ensure => installed, # den skal være installeret
}
# Den direkte pil (->) angiver, at ressourcen nedenfor skal
# oprettes efter ressourcen, der er beskrevet ovenfor.
# Sådanne afhængigheder er transitive.
-> file { '/etc/nginx': # beskriver filen /etc/nginx
ensure => directory, # dette skal være et bibliotek
source => 'puppet:///modules/example/nginx-conf', # dens indhold skal tages fra puppet-serveren på den angivne adresse
recurse => true, # kopier filer rekursivt
purge => true, # skal fjerne unødvendige filer (dem der ikke findes i kilden)
force => true, # fjerne unødvendige biblioteker
}
# Den bølgede pil (~>) angiver, at ressourcen nedenfor skal
# abonnere på ændringer af ressourcen, der er beskrevet ovenfor.
# Den bølgede pil inkluderer den direkte (->).
~> service { 'nginx': # beskriver nginx-tjenesten
ensure => running, # den skal være kørende
enable => true, # den skal starte automatisk ved systemstart
}
# Når en ressource af typen service modtager en notifikation,
# genstartes den tilsvarende tjeneste.
}For at dette skal fungere, skal filerne være placeret omtrent sådan her på puppet-serveren:
/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│ └── site.pp
└── modules/
└── example/
└── files/
└── nginx-conf/
├── nginx.conf
├── mime.types
└── conf.d/
└── some.confTyper af ressourcer
Den fulde liste over understøttede typer af ressourcer findes , her vil jeg beskrive fem grundlæggende typer, som i min praksis er tilstrækkelige til at løse de fleste opgaver.
file
Håndterer filer, biblioteker, symlinks, deres indhold og adgangsrettigheder.
Parametre:
- navnet på ressourcen — stien til filen (valgfri)
- path — stien til filen (hvis den ikke er angivet i navnet)
- ensure — filtype:
fraværende— slette filenpresent— skal være en fil af enhver type (hvis filen ikke findes, vil der blive oprettet en almindelig fil)file— almindelig fildirectory— biblioteklink— symlink
- for indhold med billeder (alternativ tekst — indhold af filen (kun egnet til almindelige filer, kan ikke bruges sammen med source eller target)
- source — link til den sti, hvorfra indholdet af filen skal kopieres (kan ikke bruges sammen med for indhold med billeder (alternativ tekst eller target). Kan angives både som en URI med schema
puppet:(så vil der blive brugt filer fra puppet-serveren), samt med schemahttp:(jeg håber, det er klart, hvad der vil være i dette tilfælde), og endda med schemafile:eller som en absolut sti uden schema (så vil den bruge filen fra det lokale FS på noden) - target — hvor symlinket skal pege (kan ikke bruges sammen med for indhold med billeder (alternativ tekst eller source)
- owner — den bruger, som filen skal tilhøre
- gruppe — gruppen, som filen skal tilhøre
- mode — rettigheder til filen (i form af en streng)
- recurse — inkluderer rekursiv behandling af mapper
- purge — inkluderer sletning af filer, der ikke er beskrevet i Puppet
- force — inkluderer sletning af mapper, der ikke er beskrevet i Puppet
pakke
Installér og fjern pakker. Kan håndtere meddelelser — geninstallér pakken, hvis parameteren er angivet reinstall_on_refresh.
Parametre:
- navnet på ressourcen — pakkens navn (valgfrit)
- name — pakkens navn (hvis ikke angivet i navnet)
- provider — pakkehåndtereren, der skal bruges
- ensure — ønsket tilstand for pakken:
present,installed— enhver version er installeretlatest— den nyeste version er installeretfraværende— fjernet (apt-get remove)purged— fjernet sammen med konfigurationsfilerne (apt-get purge)held— pakken er låst version (apt-mark hold)enhver anden streng— den angivne version er installeret
- reinstall_on_refresh — hvis
true, så vil pakken blive geninstalleret ved modtagelse af meddelelse. Nyttefuldt for source-baserede distributioner, hvor pakker skal ombyges, når byggeparametre ændres. Som standardfalse.
service
Styrer tjenester. Kan håndtere meddelelser — genstarter tjenesten.
Parametre:
- navnet på ressourcen — tjenesten, der skal styres (valgfrit)
- name — tjenesten, der skal styres (hvis ikke angivet i navnet)
- ensure — ønsket tilstand for tjenesten:
løb— kørerstopped— stoppet
- enable — styrer muligheden for at starte tjenesten:
true— autostart er aktiveret (systemctl enable)mask— maskeret (systemctl mask)false— autostart er deaktiveret (systemctl disable)
- restart — kommando til at genstarte tjenesten
- status — kommando til at kontrollere tjenestens status
- hasrestart — angiv om init-scriptet for tjenesten understøtter genstart. Hvis
falseog parameteren er angivet restart — bruges parameterens værdi. Hvisfalseog parameteren restart ikke er angivet — stoppes tjenesten, og startes igen for at genstarte (men i systemd bruges kommandoensystemctl restart). - hasstatus — angiv om init-scriptet for tjenesten understøtter kommandoen
status. Hvisfalse, så bruges parameterens værdi status. Som standardtrue.
exec
Kører eksterne kommandoer. Hvis der ikke angives parametre creates, onlyif, unless eller refreshonly, vil kommandoen blive kørt ved hver kørsel af Puppet. Kan håndtere meddelelser — kører kommandoen.
Parametre:
- navnet på ressourcen — kommandoen, der skal udføres (valgfrit)
- command — kommandoen, der skal udføres (hvis ikke angivet i navnet)
- path — veje, hvorpå den eksekverbare fil kan findes
- onlyif — hvis den angivne kommando i denne parameter afsluttes med en nul returkode, vil hovedkommanden blive udført
- unless — hvis den angivne kommando i denne parameter afsluttes med en ikke-nul returkode, vil hovedkommanden blive udført
- creates — hvis den angivne fil i denne parameter ikke findes, vil hovedkommanden blive udført
- refreshonly — hvis
true, så vil kommandoen kun blive kørt, når denne exec modtager en meddelelse fra andre ressourcer - cwd — den mappe, hvorfra kommandoen køres
- user — brugeren, som kommandoen udføres af
- provider — ved hjælp af hvad kommandoen køres:
- posix — opretter simpelthen en datterproces, skal angives path
- shell — kommandoen køres i shell
/bin/sh, kan undlades path, kan globbing, pipes og andre shell-funktioner anvendes. Bestemmes normalt automatisk, hvis der er specialtegn (|,;,&&,||og så videre).
cron
Håndterer cron-jobs.
Parametre:
- navnet på ressourcen — bare en eller anden identifikator
- ensure — tilstanden for cron-jobbet:
present— opret, hvis ikke eksistererfraværende— slet, hvis eksisterer
- command — hvilken kommando der skal køres
- environment — i hvilken kontekst kommandoen skal køres (liste over miljøvariabler og deres værdier adskilt af
=) - user — fra hvilken bruger kommandoen skal køres
- minut, time, ugedag, måned, måneddag — hvornår cron skal køre. Hvis nogen af disse attributter ikke angives, vil dens værdi i crontab være
*.
I Puppet 6.0 cron så at sige i puppetserver, så der er ingen dokumentation på det generelle websted. Men det i puppet-agent, så du skal ikke installere det separat. Dokumentationen for det kan findes , eller .
Om ressourcer generelt
Krav til unikhed af ressourcer
Den mest almindelige fejl, vi støder på, er Duplicate declaration. Denne fejl opstår, når to eller flere ressourcer af samme type med samme navn kommer ind i mappen.
Så lad mig sige det igen: i manifester for én node skal der ikke være ressourcer af samme type med samme navn (title)!
Nogle gange er det nødvendigt at installere pakker med samme navn, men forskellige pakkehåndteringssystemer. I så fald skal parameteren bruges name, for at undgå fejl:
package { 'ruby-mysql':
ensure => installed,
name => 'mysql',
provider => 'gem',
}
package { 'python-mysql':
ensure => installed,
name => 'mysql',
provider => 'pip',
}I andre typer ressourcer er der lignende parametre, der hjælper med at undgå duplikering — name af service, command af exec, og så videre.
Metaparametre
Nogle specialparametre findes for hver type ressource, uanset dens enhed.
Den komplette liste over metaparametre .
Kort liste:
- require — i denne parameter angives, hvilke ressourcer denne ressource afhænger af.
- før — i denne parameter angives, hvilke ressourcer der afhænger af denne ressource.
- subscribe — i denne parameter angives, hvilke ressourcer denne ressource modtager beskeder fra.
- notify — i denne parameter angives, hvilke ressourcer der modtager beskeder fra denne ressource.
Alle nævnte metaparametre kan tage enten et enkelt link til en ressource eller et array af links i firkantede parenteser.
Links til ressourcer
Et link til en ressource er blot en henvisning til ressourcen. De bruges hovedsageligt til at angive afhængigheder. Et link til en ikke-eksisterende ressource vil fremkalde en kompileringsfejl.
Syntaxen for et link er som følger: ressourcetype med stort begyndelsesbogstav (hvis navnet på typen indeholder dobbelte kolon, skrives hver del af navnet med stort begyndelsesbogstav mellem kolon), derefter i firkantede parenteser navnet på ressourcen (størrelsen på navnet ændres ikke!). Der må ikke være mellemrum, de firkantede parenteser skrives direkte efter navnet på typen.
Eksempel:
file { '/file1': ensure => present }
file { '/file2':
ensure => directory,
before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']Afhængigheder og notifikationer
Som nævnt tidligere er enkle afhængigheder mellem ressourcer transitiv. Vær opmærksom på, at når du angiver afhængigheder, kan du skabe cykliske afhængigheder, som vil forårsage en kompileringsfejl.
I modsætning til afhængigheder er notifikationer ikke transitive. Følgende regler gælder for notifikationer:
- Hvis en ressource modtager en notifikation, opdateres den. Handlingerne ved opdatering afhænger af ressourcetype — exec udfører en kommando, service genstarter en tjeneste, pakke geninstallerer en pakke. Hvis der ikke er defineret en handling ved opdatering for ressourcen, sker der ikke noget.
- Under et enkelt kørsel af Puppet opdateres en ressource ikke mere end én gang. Dette er muligt, da notifikationer inkluderer afhængigheder, og afhængighedsgraphen ikke indeholder cykler.
- Hvis Puppet ændrer tilstanden for en ressource, sender ressourcen notifikationer til alle de ressourcer, der er tilmeldt den.
- Hvis ressourcen opdateres, sender den notifikationer til alle de ressourcer, der er tilmeldt den.
Behandling af uoplyste parametre
Som regel, hvis en parameter for ressourcen ikke har en standardværdi, og denne parameter ikke er angivet i manifestet, vil Puppet ikke ændre denne egenskab for den tilsvarende ressource på noden. For eksempel, hvis ressourcen af typen file manglende parameter owner, så vil Puppet ikke ændre ejeren af den tilsvarende fil.
Indførelse til klasser, variabler og definerede værdier
Antag, at vi har flere noder, hvor der er ens dele af konfigurationen, men også forskelle — ellers kunne vi beskrive alt dette i ét blok. node {}. Selvfølgelig kan vi bare kopiere de ens dele af konfigurationen, men i almindelighed er det en dårlig løsning — konfigurationen vokser, og når den fælles del af konfigurationen ændres, skal man rette det samme mange steder. Her er det let at lave fejl, og der er ikke uden grund, at princippet DRY (don’t repeat yourself) blev opfundet.
Til at løse et sådant problem findes der en konstruktion som klasse.
Klasser
— det er en navngivet blok af Puppet-kode. Klasser er nødvendige for genbrug af kode.
Først skal klassen beskrives. Selve beskrivelsen tilføjer ikke nogen ressourcer. Klasser beskrives i manifestene:
# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
...
}Derefter kan klassen bruges:
# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальшеEksemplet fra den tidligere opgave — lad os flytte installationen og konfigurationen af nginx til en klasse:
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
}Variabler
Klassen fra det forrige eksempel er slet ikke fleksibel, fordi den altid bringer den samme nginx-konfiguration. Lad os gøre, så stien til konfigurationen bliver variabel, så denne klasse kan bruges til installation af nginx med enhver konfiguration.
Dette kan gøres .
Bemærk: variable i Puppet er uforanderlige!
Derudover kan man kun tilgå en variabel, efter den er blevet erklæret, ellers vil værdien af variablen være undef.
Eksempel på arbejde med variable:
# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"I Puppet findes der navnerum, og i variabler har vi derfor synlighedsområde: en variabel med det samme navn kan defineres i forskellige navneområder. Ved opløsning af variablens værdi søges der først i det aktuelle navnerum, derefter i det overordnede, og så videre.
Eksempler på navneområder:
- globalt — variabler uden for klasse- eller nodebeskrivelsen havner her;
- node-navneområde i nodebeskrivelsen;
- klasse-navneområde i klassebeskrivelsen.
For at undgå tvetydighed ved henvisning til en variabel kan man angive navneområdet i variablens navn:
# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::varLad os antage, at stien til nginx-konfigurationen er gemt i variablen $nginx_conf_source. Sådan vil klassen se ud:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => $nginx_conf_source, # her bruger vi variablen i stedet for en hårdkodet streng
recurse => 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
}Men det nævnte eksempel er dårligt, fordi der er en slags "hemmelig viden" om, at der inden i klassen anvendes en variabel med et bestemt navn. Det vil være meget mere korrekt at gøre denne viden fælles — klasser kan have parametre.
Klasseparametre — er variabler i klasse-navneområdet, de defineres i klassens overskrift og kan bruges som almindelige variabler i klassens krop. Værdierne for parametrene angives, når klassen bruges i manifester.
Man kan angive en standardværdi for et parameter. Hvis en parameter ikke har en standardværdi, og værdien ikke er angivet ved anvendelsen, vil dette forårsage en compilerfejl.
Lad os parameterisere klassen fra ovenstående eksempel og tilføje to parametre: den første, obligatortisk — stien til konfigurationen, og den anden, valgfri — navnet på pakken med nginx (i Debian, for eksempel, findes der pakker 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', # задаём параметры класса точно так же, как параметры для других ресурсов
}
}I Puppet er variable typede. Der findes . Datatyper bruges normalt til validering af værdierne for parametre, der overføres til klasser og definitioner. Hvis den overførte parameter ikke svarer til den angivne type, vil der ske en compilerfejl.
Typen angives direkte før navnet på parameteren:
klasse eksempel (
String $param1,
Integer $param2,
Array $param3,
Hash $param4,
Hash[String, String] $param5,
) {
...
}Klasser: inkluder klassens navn vs klasse{‘klassens navn’:}
Hver klasse er en ressource af typen klasse. Som med andre ressourcetyper kan to instanser af den samme klasse ikke eksistere på én nod.
Hvis man prøver at tilføje en klasse til den samme node to gange ved hjælp af klasse { 'klassens navn':} (uanset om de har forskellige eller de samme parametre), vil der opstå en kompileringsfejl. Men i tilfælde af at bruge klassen i ressourcestil kan alle dens parametre straks angives i manifestet.
Men hvis man bruger include, kan klassen tilføjes så mange gange man vil. Det skyldes, at include — en idempotent funktion, der tjekker, om klassen allerede er tilføjet til kataloget. Hvis klassen ikke er i kataloget — tilføjer den den, men hvis den allerede findes, gør den ingenting. Men i tilfælde af at bruge include kan man ikke angive klassens parametre, mens klassen erklæres — alle obligatoriske parametre skal angives i en ekstern datakilde — Hiera eller ENC. Vi vil tale om dem i næste artikel.
Definerer
Som nævnt i det forrige afsnit, kan den samme klasse ikke være til stede på en node mere end én gang. Men i nogle tilfælde er der brug for at kunne anvende den samme kodeblok med forskellige parametre på én node. Med andre ord, der er behov for en egen type ressource.
For eksempel, for at installere PHP-modulet, gør vi følgende i Avito:
- Installerer pakken med dette modul.
- Opretter en konfigurationsfil for dette modul.
- Opretter et symlink til konfigurationen for php-fpm.
- Opretter et symlink til konfigurationen for php cli.
I sådanne tilfælde anvendes en konstruktion kaldet (define, defineret type, defineret ressource type). Define ligner en klasse, men der er forskelle: for det første er hver define en type ressource, ikke en ressource; for det andet har hver define en implicit parameter $title, som indeholder ressourcens navn ved erklæringen. Ligesom med klasser, skal define først beskrives, hvorefter den kan bruges.
Et forsimplet eksempel med et modul til PHP:
definer 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-package triggers automatically create symlinks and restart the php-fpm service - we don’t need that, as we manage both symlinks and services 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' }
}Det er nemmest at fange en fejl i den definerede deklaration. Dette sker, hvis der er en ressource med et konstant navn i definitionen, og der på en eller flere noder er to eller flere instanser af denne definition.
At beskytte sig mod dette er simpelt: alle ressourcer inden for definitionen skal have et navn, der afhænger af $title. Som et alternativ — idempotent tilføjelse af ressourcer, i det simpleste tilfælde er det tilstrækkeligt at trække de fælles ressourcer for alle instanser af definitionen ud i en separat klasse og inkludere denne klasse i definitionen — funktionen include er idempotent.
Der er også andre måder at opnå idempotens ved tilføjelse af ressourcer, nemlig ved at bruge funktioner defined og ensure_resources, men det vil jeg fortælle om i næste afsnit.
Afhængigheder og notificeringer for klasser og definitioner
Klasser og definitioner tilføjer følgende regler til behandling af afhængigheder og notifikationer:
- en afhængighed af klassen/definitionen tilføjer afhængigheder fra alle ressourcer i klassen/definitionen;
- en afhængighed af klassen/definitionen tilføjer afhængigheder til alle ressourcer i klassen/definitionen;
- en notifikation fra klassen/definitionen notificerer alle ressourcer i klassen/definitionen;
- en abonnement på klassen/definitionen tilmelder sig alle ressourcer i klassen/definitionen.
Betingede operatorer og vælgere
if
Her er det hele simpelt:
if UDTRYK1 {
...
} elsif UDTRYK2 {
...
} else {
...
}unless
medmindre — dette er if omvendt: koden blokeres, hvis udtrykket er falskt.
medmindre UDTRYK {
...
}case
Her er der heller ikke noget kompliceret. Som værdier kan du bruge almindelige værdier (strenge, tal osv.), regulære udtryk samt datatyper.
case UDSAGT {
VÆRDI1: { ... }
VÆRDI2, VÆRDI3: { ... }
default: { ... }
}Vælgerne
En vælger er en sprogkonstruktion, der ligner case, men i stedet for at udføre en kodeblok returnerer den en værdi.
$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }Moduler
Når konfigurationen er lille, kan den nemt holdes i en enkelt manifest. Men jo større konfigurationen bliver, jo flere klasser og noder bliver der i manifestet, det vokser, og det bliver besværligt at arbejde med.
Derudover er der et problem med genbrug af kode — når al koden er i et manifestation, er det svært at dele denne kode med andre. For at løse disse to problemer har Puppet en enhed kaldet moduler.
Moduler — det er sæt af klasser, defineringer og andre Puppet-enheder adskilt i en egen mappe. Med andre ord er en modul et uafhængigt stykke Puppet-logik. For eksempel kan der være et modul til at arbejde med nginx, og det indeholder kun det, der er nødvendigt for at arbejde med nginx, og der kan også være et modul til at arbejde med PHP, og så videre.
Moduler versioneres, og de understøtter også afhængigheder mellem moduler. Der er et åbent bibliotek af moduler — .
På Puppet-serveren ligger modulerne i undermappen modules i rodmappen. Inden i hver modul er der en standard mappestruktur — manifests, files, templates, lib og så videre.
Filstrukturen i en modul
I roden af modulet kan der være følgende mapper med sigende navne:
manifests— den indeholder manifesterfiles— den indeholder filertemplates— den indeholder skabelonerlib— den indeholder Ruby-kode
Dette er ikke en komplet liste over mapper og filer, men det er tilstrækkeligt til denne artikel for nu.
Navne på ressourcer og filnavne i modulet
Ressourcer (klasser, definitioner) i modulet kan ikke navngives vilkårligt. Derudover er der en direkte sammenhæng mellem navnet på ressourcen og filnavnet, hvor Puppet vil lede efter beskrivelsen af denne ressource. Hvis navngivningsreglerne overtrædes, vil Puppet simpelthen ikke finde beskrivelsen af ressourcerne, og der vil opstå en kompileringsfejl.
Reglerne er enkle:
- Alle ressourcer i modulet skal være i modulens navneområde. Hvis modulet hedder
foo, så skal alle ressourcer i det kaldesfoo::, eller blotfoo. - Ressourcen med modulnavnet skal være i filen
init.pp. - For de andre ressourcer er filnavngivningsskemaet som følger:
- præfikset med modulens navn kasseres
- alle dobbeltkolonner, hvis de findes, erstattes med skråstreg
- tilføjes en filendelse
.pp
Jeg vil demonstrere med et eksempel. Antag, at jeg skriver et modul nginx. Det indeholder følgende ressourcer:
- klasse
nginxbeskrevet i manifestetinit.pp; - klasse
nginx::servicebeskrevet i manifestetservice.pp; - definere
nginx::serverbeskrevet i manifestetserver.pp; - definere
nginx::server::locationbeskrevet i manifestetserver/location.pp.
Skabeloner
Du ved sikkert allerede, hvad skabeloner er, så jeg vil ikke uddybe det her. Men jeg vil for en sikkerheds skyld efterlade .
Sådan bruger du skabeloner: værdien af en skabelon kan udfoldes ved hjælp af funktionen template, der får stien til skabelonen som argument. For ressourcer af typen file bruges sammen med parameteren for indhold med billeder (alternativ tekst. For eksempel sådan her:
file { '/tmp/example': content => template('modulename/templatename.erb')En sti som / betyder en fil /modules//templates/.
Derudover er der en funktion inline_template – den modtager tekstskabelonen som input, ikke filnavnet.
Inden i skabeloner kan du bruge alle Puppet-variabler i det aktuelle scopes.
Puppet understøtter skabeloner i ERB og EPP-format:
Kort om ERB
Kontrolstrukturer:
<%= ВЫРАЖЕНИЕ %>– indsætte værdien af udtrykket<% ВЫРАЖЕНИЕ %>– beregne værdien af udtrykket (uden at indsætte det). Her gælder almindelige betingelser (if), sløjfer (each).<%# КОММЕНТАРИЙ %>
Udtryk i ERB skrives i Ruby (faktisk er ERB - det er Embedded Ruby).
For at få adgang til variabler fra manifestet skal du tilføje @ til variabelnavnet. For at fjerne linjeskiftet, der opstår efter kontrolstrukturen, skal du bruge lukketag -%>.
Eksempel på brug af skabelon
Antag, at jeg skriver et modul til at administrere ZooKeeper. Klassen, der står for oprettelsen af konfigurationen, ser omtrent sådan ud:
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'),
}
}Og den tilsvarende skabelon zoo.cfg.erb – ser ud som følger:
0 -%>
server.=:::
dataDir=
=Fakta og indbyggede variabler
Ofte afhænger en specifik del af konfigurationen af, hvad der sker på noden på et givent tidspunkt. For eksempel, afhængig af hvilken version af Debian der er installeret, skal den relevante version af pakken installeres. Man kan holde øje med alt dette manuelt ved at omskrive manifester, når der sker ændringer på noden. Men det er ikke en seriøs tilgang, automatisering er meget bedre.
For at få information om noder i Puppet findes der en mekanisme kaldet fakta. Fakta — er information om noden, der er tilgængelig i manifester som almindelige variabler i det globale navnerum. For eksempel, værtens navn, operativsystemets version, processorens arkitektur, listen over brugere, listen over netværksgrænseflader og deres adresser, og meget, meget mere. Fakta er tilgængelige i manifester og skabeloner som almindelige variabler.
Eksempel på arbejde med fakta:
notify { "Kører OS ${facts['os']['name']} version ${facts['os']['release']['full']}" : }
# notify-typen ressource viser blot en besked i loggen.Formelt set har et faktum et navn (streng) og en værdi (forskellige typer er tilgængelige: strenge, arrays, dictionaries). Der er . Man kan også skrive sine egne. Faktasamlere beskrives , eller som . Fakta kan også præsenteres i form af på noderne.
Under arbejdet kopierer Puppet-agenten først alle tilgængelige faktasamlere fra Puppet-serveren til noden, hvorefter de køres, og de indsamlede fakta sendes tilbage til serveren; først derefter begynder serveren at kompilere kataloget.
Fakta i form af eksekverbare filer
Sådanne fakta placeres i moduler i mappen facts.d. Selvfølgelig skal filerne være eksekverbare. Ved kørsel skal de udskrive oplysninger til standard output enten i YAML-format eller i formatet "nøgle=værdi".
Husk, at fakta gælder for alle noder, der styres af Puppet-serveren, hvor dit modul implementeres. Derfor må du i skriptet sørge for, at alle nødvendige programmer og filer til dit fakta er til stede i systemet.
#!/bin/sh
echo "testfact=success"#!/bin/sh
echo '{"testyamlfact":"success"}'Fakta i Ruby
Sådanne fakta placeres i moduler i mappen 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
endTekstfakta
Sådanne fakta placeres på noderne i mappen /etc/facter/facts.d i den gamle Puppet eller /etc/puppetlabs/facts.d i den nye Puppet.
examplefact=examplevalue---
examplefact2: examplevalue2
anotherfact: anothervalueAdgang til fakta
Facts can be accessed in two ways:
- through the dictionary
$facts:$facts['fqdn']; - using the fact name as a variable name:
$fqdn.
It is best to use the dictionary $facts, and even better to specify the global namespace ($::facts).
Built-in variables
In addition to facts, there are also , available in the global namespace.
- trusted facts — variables taken from the client's certificate (since the certificate is usually issued on the Puppet server, the agent cannot simply change its certificate, hence the variables are ‘trusted’): certificate name, host and domain name, extensions from the certificate.
- server facts — variables related to server information — version, name, server IP address, environment.
- agent facts — variables added directly by the puppet agent, not by facter — certificate name, agent version, Puppet version.
- master variables — variables of the Puppet master (sic!). It contains about the same information as server facts, plus values of configuration parameters are available.
- compiler variables — compiler variables that differ in each scope: name of the current module and name of the module from which the current object was accessed. They can be used, for example, to ensure that your private classes are not directly used from other modules.
Supplement 1: how to run and debug all this?
The article contained many examples of Puppet code, but did not cover how to run this code. Well, I'm correcting that.
For Puppet to work, an agent is sufficient, but in most cases a server will also be needed.
Agent
At least starting from version 5, puppet-agent packages from contain all dependencies (ruby and the corresponding gems), so there are no difficulties with installation (I’m talking about Debian-based distributions — we do not use RPM-based distributions).
In the simplest case, to apply puppet configuration, it is enough to run the agent in a serverless mode: provided that the Puppet code has been copied to the node, run puppet apply:
atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp
node default {
notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp
Bemærkning: Kompileret katalog for atikhonov.localdomain i miljøet produktion på 0,01 sekunder
Bemærkning: Hello world!
Bemærkning: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defineret 'message' som 'Hello world!'
Bemærkning: Anvendt katalog på 0,01 sekunderDet er selvfølgelig bedre at oprette en server og køre agenterne på noderne i daemon-tilstand — så vil de anvende konfigurationen, der er downloadet fra serveren, hver halve time.
Man kan simulere en push-model — gå ind på den ønskede node og køre sudo puppet agent -t. Nøglen -t (--test) inkluderer faktisk flere muligheder, som kan aktiveres enkeltvis. Blandt disse muligheder er følgende:
- ikke at arbejde i daemon-tilstand (som standard kører agenten i daemon-tilstand);
- stoppe arbejdet efter anvendelse af kataloget (som standard fortsætter agenten med at køre og anvender konfigurationen hver halve time);
- skrive detaljerede logfiler;
- vise ændringer i filer.
Agenten har en tilstand uden ændringer — den kan anvendes, når du ikke er sikker på, at du har skrevet en korrekt konfiguration, og ønsker at kontrollere, hvad agenten vil ændre under arbejdet. Denne tilstand aktiveres med parameteren --noop i kommandolinjen: sudo puppet agent -t --noop.
Desuden kan du aktivere en debugging-log — den logger alle de handlinger, som puppet udfører: om den resource, den behandler lige nu, om parametrene for denne resource, og hvilke programmer der startes. Selvfølgelig er dette en parameter --debug.
Server
Jeg vil ikke gennemgå den fulde opsætning af puppetserveren og deployment af kode til den i denne artikel, men vil blot sige, at en fuldt fungerende version af serveren leveres out-of-the-box, der ikke kræver yderligere opsætning for at fungere under forhold med et begrænset antal noder (lad os sige op til hundrede). Et større antal noder vil kræve finjustering — som standard starter puppetserver ikke mere end fire arbejdere, og for bedre ydeevne skal antallet øges, og glem ikke at øge hukommelsesgrænserne, ellers vil serveren bruge det meste af tiden på garbage collection.
Deployment af kode — hvis det skal være hurtigt og enkelt, så tjek (på r10k)[], dette burde være tilstrækkeligt til små installationer.
Tillæg 2: anbefalinger til kodning
- Fjern al logik i klasser og definerede.
- Hold classes and definitions in modules, not in manifests describing nodes.
- Use the facts.
- Avoid using if statements based on hostnames.
- Don't hesitate to add parameters for classes and definitions — it's better than implicit logic hidden in the body of the class/definition.
Let me explain why I recommend this in the next article.
Konklusion
That concludes our introduction. In the next article, I'll talk about Hiera, ENC, and PuppetDB.
Kun registrerede brugere kan deltage i undersøgelsen. , please.
In fact, there is much more material — I can write articles on the following topics; vote on what you would be interested in reading:
- 59,1%Advanced puppet constructs — some next-level stuff: loops, mapping, and other lambda expressions, resource collectors, exported resources, and inter-host interaction through Puppet, tags, providers, abstract data types.
- 31,8%"I’m the admin at my mom's" or how we at Avito brought together several Puppet servers of different versions, including a section on Puppet server administration.
- 81,8%How we write Puppet code: tooling, documentation, testing, CI/CD.
22 users voted. 9 users abstained.
Kilde: habr.com
