Inleiding tot Puppet

Puppet is een configuratiebeheersysteem. Het wordt gebruikt om hosts in de juiste staat te brengen en deze staat te onderhouden.

Ik werk al meer dan vijf jaar met Puppet. Deze tekst is in feite een vertaalde en herschikte compilatie van de belangrijkste punten uit de officiële documentatie, die nieuwkomers snel inzicht zal geven in de essentie van Puppet.

Inleiding tot Puppet

Basisinformatie

De werking van Puppet is client-server, hoewel er ook een variant is zonder server met beperkte functionaliteit.

Er wordt een pull-model gebruikt: standaard vragen de cliënten elke halfuur de server om configuratie en passen deze toe. Als je met Ansible hebt gewerkt, gebruik je een andere, push-model: de beheerder initieert het proces om de configuratie toe te passen, cliënten doen zelf niets.

Bij netwerkcommunicatie wordt tweezijdige TLS-encryptie gebruikt: de server en de client hebben hun eigen private sleutels en de bijbehorende certificaten. Meestal geeft de server certificaten uit voor cliënten, maar het is in principe mogelijk om ook een externe CA te gebruiken.

Kennismaking met manifesten

In de terminologie van Puppet verbinden met de puppet-server zich aanmelden nodes (nodes). De configuratie voor nodes wordt geschreven in manifesten in een speciale programmeertaal — Puppet DSL.

Puppet DSL is een declaratieve taal. Hiermee wordt de gewenste staat van een node beschreven in de vorm van declaraties van afzonderlijke middelen, bijvoorbeeld:

  • Een bestand bestaat en heeft een bepaalde inhoud.
  • Een pakket is geïnstalleerd.
  • Een service draait.

Middelen kunnen met elkaar in verband staan:

  • Er zijn afhankelijkheden die invloed hebben op de volgorde van toepassing van de middelen.
    Bijvoorbeeld, "installeer eerst het pakket, pas dan het configuratiebestand aan, en start daarna de service."
  • Er zijn meldingen — als een middel is gewijzigd, stuurt het meldingen naar de ingeschreven middelen.
    Bijvoorbeeld, als het configuratiebestand wijzigt, kan de service automatisch opnieuw worden gestart.

Bovendien zijn er in Puppet DSL functies en variabelen, evenals conditionele operatoren en selectors. Ook worden verschillende sjabloonmechanismen ondersteund — EPP en ERB.

Puppet is geschreven in Ruby, daarom zijn veel constructies en termen daarvandaan gehaald. Ruby maakt het mogelijk om Puppet uit te breiden — complexe logica, nieuwe soorten middelen en functies toe te voegen.

Tijdens het gebruik van Puppet worden de manifesten voor elke specifieke node op de server in een map gecompileerd. Map — dit is een lijst van middelen en hun onderlinge relaties na het berekenen van de waarden van functies, variabelen en het uitbreiden van conditionele operators.

Syntax en codestijl

Hier zijn de secties van de officiële documentatie die je helpen om de syntax te begrijpen als de gegeven voorbeelden niet voldoende zijn:

Hier is een voorbeeld van hoe een manifest eruitziet:

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

Inspringingen en regelovergangen zijn geen verplicht onderdeel van een manifest, maar er is een aanbevolen stijlrichtlijn.Samenvatting:

  • Dubbele spaties en tabs worden niet gebruikt.
  • Accolades worden gescheiden door een spatie, maar een dubbele punt wordt er niet door gescheiden.
  • Komma's na elk parameter, inclusief de laatste. Elke parameter staat op een aparte regel. Uitzonderingen zijn de gevallen zonder parameters of met één parameter: deze kunnen op één regel zonder komma worden geschreven (d.w.z. resource { 'title': } en resource { 'title': param => value }).
  • Pijlen bij parameters moeten op hetzelfde niveau staan.
  • De pijlen voor de onderlinge verbanden van de middelen worden vóór de middelen geschreven.

Bestandlocaties op de Puppetserver

Voor verdere uitleg zal ik het begrip "rootmap" introduceren. De rootmap is de map waarin de Puppet-configuratie voor een specifieke node zich bevindt.

De rootmap verschilt afhankelijk van de versie van Puppet en het gebruik van omgevingen. Omgevingen zijn onafhankelijke configuratiesets die in aparte mappen worden opgeslagen. Ze worden meestal in combinatie met Git gebruikt, waarbij omgevingen uit Git-takken worden aangemaakt. Dienovereenkomstig bevindt elke node zich in een bepaalde omgeving. Dit wordt ingesteld op de node zelf, of in de ENC, waarover ik in het volgende artikel zal vertellen.

  • In de derde versie (“oude Puppet”) was de basisdirectory /etc/puppet. Het gebruik van omgevingen is optioneel – wij gebruiken ze bijvoorbeeld niet met de oude Puppet. Als omgevingen worden gebruikt, worden ze meestal opgeslagen in /etc/puppet/environments, de rootmap wordt de map van de omgeving. Als er geen omgevingen worden gebruikt, is de rootmap de basis.
  • Met de vierde versie (“nieuwe Puppet”) werd het gebruik van omgevingen verplicht, en werd de basisdirectory verplaatst naar /etc/puppetlabs/code. Om deze reden worden omgevingen opgeslagen in /etc/puppetlabs/code/environments, de rootdirectory is de directory van de omgeving.

In de rootdirectory moet er een subdirectory zijn manifests, waar een of meer manifesten met beschrijvingen van knooppunten worden opgeslagen. Daarnaast moet er een subdirectory zijn modules, waar de modules zich bevinden. Wat modules zijn, vertel ik je later. Bovendien kan er in de oude Pappet ook een subdirectory zijn files, waar verschillende bestanden zich bevinden die we naar de knooppunten kopiëren. In de nieuwe Pappet zijn alle bestanden echter in modules ondergebracht.

Manifestbestanden hebben de extensie .pp.

Een paar praktische voorbeelden

Beschrijving van het knooppunt en de resources erop

Op het knooppunt server1.testdomain moet een bestand zijn aangemaakt /etc/issue met de inhoud Debian GNU/Linux n l. Het bestand moet toebehoren aan de gebruiker en groep root, de toegangsrechten moeten zijn 644.

We schrijven een manifest:

node 'server1.testdomain' {   # configuratieblok dat betrekking heeft op knooppunt server1.testdomain
    file { '/etc/issue':   # beschrijving van bestand /etc/issue
        ensure  => present,   # dit bestand moet bestaan
        content => 'Debian GNU/Linux n l',   # het moet deze inhoud hebben
        owner   => root,   # eigenaar gebruiker
        group   => root,   # eigenaar groep
        mode    => '0644',   # bestandsrechten. Deze zijn opgegeven als een string (tussen aanhalingstekens), omdat anders een getal met 0 aan het begin als octaal wordt geïnterpreteerd, en alles gaat niet zoals bedoeld
    }
}

Relaties van resources op het knooppunt

Op het knooppunt server2.testdomain moet nginx draaien met een vooraf voorbereide configuratie.

We decomponeren de taak:

  • Het is nodig dat het pakket is geïnstalleerd met alles wat daarin al aanwezig is, en de inhoud van de map.
  • Het is nodig dat de configuratiebestanden van de server zijn gekopieerd.
  • Het is nodig dat de service draait met alles wat daarin al aanwezig is, en de inhoud van de map.
  • In geval van configuratie-updates moet de service opnieuw worden gestart.

We schrijven een manifest:

node 'server2.testdomain' {   # configuratieblok voor de node server2.testdomain
    package { 'nginx':   # beschrijving van het nginx-pakket
        ensure => installed,   # het moet geïnstalleerd zijn
    }
  # De directe pijl (->) geeft aan dat de onderstaande bron moet
  # worden gemaakt na de hierboven beschreven bron.
  # Deze afhankelijkheden zijn transitief.
    -> file { '\/etc\/nginx':   # beschrijving van het bestand \/etc\/nginx
        ensure  => directory,   # dit moet een directory zijn
        source  => 'puppet://modules/example/nginx-conf',   # de inhoud moet van de puppetserver komen via het aangegeven adres
        recurse => true,   # bestanden recursief kopiëren
        purge   => true,   # overtollige bestanden verwijderen (die niet in de bron staan)
        force   => true,   # overtollige directories verwijderen
    }
  # De golvende pijl (~>) geeft aan dat de onderstaande bron moet
  # abonneren op veranderingen van de bovenstaand beschreven bron.
  # De golvende pijl omvat de directe (->).
    ~> service { 'nginx':   # beschrijving van de nginx-service
        ensure => running,   # het moet draaien
        enable => true,   # het moet automatisch worden gestart bij het opstarten van het systeem
    }
  # Wanneer een resource van het type service een melding ontvangt,
  # wordt de bijbehorende service herstart.
}

Om dit te laten werken, is ongeveer deze bestandsindeling op de puppetserver nodig:

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

Resource types

Een volledige lijst van ondersteunde resource types vindt u in de documentatie, hier beschrijf ik vijf basis types die in mijn praktijk voldoende zijn voor de meeste taken.

file

Beheert bestanden, directories, symlinks, hun inhoud, toegangrechten.

Parameters:

  • naam van de resource — pad naar het bestand (optioneel)
  • path — pad naar het bestand (als niet opgegeven in de naam)
  • ensure — type bestand:
    • afwezig — bestand verwijderen
    • present — moet een bestand van elk type zijn (als het bestand er niet is, wordt er een standaardbestand aangemaakt)
    • file — standaardbestand
    • directory — directory
    • link — symlink
  • content — inhoud van het bestand (alleen geschikt voor standaardbestanden, kan niet samen met source of target)
  • source — verwijzing naar het pad waaruit de inhoud van het bestand moet worden gekopieerd (kan niet samen met content of target). Het kan worden opgegeven als een URI met het schema puppet: (dan worden bestanden van de puppetserver gebruikt), of met het schema http: (ik hoop dat het duidelijk is wat er in dat geval zal zijn), en zelfs met het schema file: of als een absoluut pad zonder schema (dan wordt een bestand van het lokale FS op de node gebruikt)
  • target — waar de symlink naar moet wijzen (kan niet samen met content of source)
  • owner — gebruiker aan wie het bestand toebehoort
  • groep — groep waaraan het bestand toebehoort
  • modus — rechten op het bestand (in de vorm van een reeks)
  • recurse — omvat recursieve verwerking van mappen
  • purge — omvat het verwijderen van bestanden die niet zijn beschreven in Puppet
  • force — omvat het verwijderen van mappen die niet zijn beschreven in Puppet

package

Installeert en verwijdert pakketten. Kan meldingen verwerken — herinstalleert het pakket als de parameter is opgegeven reinstall_on_refresh.

Parameters:

  • naam van de resource — naam van het pakket (optioneel)
  • naam — naam van het pakket (als deze niet is opgegeven in de naam)
  • provider — pakketbeheerder die moet worden gebruikt
  • ensure — gewenste status van het pakket:
    • present, geïnstalleerd — enige versie is geïnstalleerd
    • latest — laatste versie is geïnstalleerd
    • afwezig — verwijderd (apt-get remove)
    • purged — samen met configuratiebestanden verwijderd (apt-get purge)
    • held — versie van het pakket is vergrendeld (apt-mark hold)
    • elke andere string — opgegeven versie is geïnstalleerd
  • reinstall_on_refresh — als true, dan bij het ontvangen van de melding wordt het pakket opnieuw geïnstalleerd. Handig voor source-based distributies, waar het opnieuw bouwen van pakketten nodig kan zijn bij het wijzigen van bouwparameters. Standaard false.

voor het SystemD-initiesysteem:

Beheert services. Kan meldingen verwerken — herstart de service.

Parameters:

  • naam van de resource — service die moet worden beheerd (optioneel)
  • naam — service die moet worden beheerd (als deze niet is opgegeven in de naam)
  • ensure — gewenste status van de service:
    • running — actief
    • stopped — gestopt
  • enable — beheert de mogelijkheid om de service te starten:
    • true — autostart is ingeschakeld (systemctl enable)
    • mask — gemaskerd (systemctl mask)
    • false — autostart is uitgeschakeld (systemctl disable)
  • restart — commando voor het herstarten van de service
  • status — commando voor het controleren van de status van de service
  • hasrestart — aangeven of het init-script van de service herstart ondersteunt. Als false en de parameter is opgegeven restart — wordt de waarde van deze parameter gebruikt. Als false en de parameter restart niet is opgegeven — de service wordt gestopt en opnieuw gestart voor herstart (maar in systemd wordt het commando systemctl restart).
  • hasstatus — aangeven of het init-script van de service de opdracht ondersteunt status. Als false, dan wordt de waarde van de parameter gebruikt status. Standaard true.

exec

Start externe commando's. Als geen parameters worden opgegeven creates, onlyif, unless of refreshonly, wordt het commando bij elke uitvoering van Puppet uitgevoerd. Kan meldingen verwerken — voert het commando uit.

Parameters:

  • naam van de resource — commando dat moet worden uitgevoerd (optioneel)
  • command — commando dat moet worden uitgevoerd (als het niet is opgegeven in de naam)
  • path — paden om het uitvoerbare bestand te zoeken
  • onlyif — als het commando dat in deze parameter is opgegeven, met een nulretourcode is beëindigd, wordt het hoofdcommando uitgevoerd
  • unless — als het commando dat in deze parameter is opgegeven, met een niet-nulretourcode is beëindigd, wordt het hoofdcommando uitgevoerd
  • creates — als het bestand dat in deze parameter is opgegeven, niet bestaat, wordt het hoofdcommando uitgevoerd
  • refreshonly — als true, dan wordt het commando alleen uitgevoerd wanneer deze exec een melding ontvangt van andere bronnen
  • cwd — de directory van waaruit het commando wordt uitgevoerd
  • gebruiker — de gebruiker van wie het commando wordt uitgevoerd
  • provider — waarmee het commando wordt uitgevoerd:
    • posix — er wordt gewoon een kindproces gemaakt, moet worden opgegeven path
    • shell — het commando wordt in de shell uitgevoerd /bin/sh, hoeft niet opgegeven te worden path, kan globbing, pipes en andere shell-functies worden gebruikt. Gewoonlijk automatisch bepaald als er speciale tekens zijn (|, ;, &&, || enzovoort).

cron

Beheert cron-taken.

Parameters:

  • naam van de resource — gewoon een identificator
  • ensure — de status van de cron-taak:
    • present — aanmaken als het niet bestaat
    • afwezig — verwijderen als het bestaat
  • command — welk commando moet worden uitgevoerd
  • omgeving — in welke omgeving het commando moet worden uitgevoerd (lijst van omgevingsvariabelen en hun waarden door =)
  • gebruiker — onder welke gebruiker het commando moet worden uitgevoerd
  • minuut, uur, weekdag, maand, maanddag — wanneer de cron moet worden uitgevoerd. Als geen van deze attributen is opgegeven, is de waarde in de crontab *.

In Puppet 6.0 cron zijn ze verwijderd uit de doos in puppetserver, daarom is er geen documentatie op de algemene website. Maar het is er in de doos in puppet-agent, dus je hoeft het niet apart te installeren. De documentatie hierover kan worden bekeken in de documentatie voor versie vijf van Puppet, ofwel op GitHub.

Over bronnen in het algemeen

Vereisten voor de uniciteit van bronnen

De meest voorkomende fout die we tegenkomen is - Duplicate declaration. Deze fout treedt op wanneer er twee of meer bronnen van hetzelfde type met dezelfde naam in de map komen.

Daarom zal ik nogmaals zeggen: in manifesten voor één node mag er geen bronnen van hetzelfde type met dezelfde naam (title) zijn!

Soms is het noodzakelijk om pakketten met dezelfde naam, maar verschillende pakketbeheerders te installeren. In dat geval moet je de parameter gebruiken naam, om fouten te voorkomen:

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

Andere types van middelen hebben vergelijkbare parameters om duplicatie te voorkomen, - naam ‘ voor het SystemD-initiesysteem:, command ‘ exec, enzovoort.

Metaparameters

Elke type resource heeft enkele speciale parameters, ongeacht zijn aard.

Volledige lijst van metaparameters in de Puppet-documentatie.

Korte lijst:

  • vereisen - deze parameter geeft aan van welke middelen deze resource afhankelijk is.
  • voor - deze parameter geeft aan welke middelen afhankelijk zijn van deze resource.
  • abonneren - deze parameter geeft aan van welke middelen deze resource notificaties ontvangt.
  • meld - deze parameter geeft aan welke middelen notificaties ontvangen van deze resource.

Alle genoemde metaparameters accepteren ofwel één verwijzing naar een resource of een array van verwijzingen tussen vierkante haken.

Verwijzingen naar resources

Een verwijzing naar een resource is gewoon een vermelding van de resource. Ze worden voornamelijk gebruikt om afhankelijkheden aan te geven. Een verwijzing naar een niet-bestaande resource zal een compilatiefout veroorzaken.

De syntaxis van een verwijzing is als volgt: type resource met een hoofdletter (als de naam van het type dubbele dwarsstrepen bevat, wordt elk deel tussen de dwarsstrepen met een hoofdletter geschreven), daarna in vierkante haken de naam van de resource (de hoofdletters in de naam blijven onveranderd!). Er mogen geen spaties zijn, de vierkante haken worden onmiddellijk na de naam van het type geschreven.

Voorbeeld:

bestand { '/file1': zorg => aanwezig }
bestand { '/file2':
  zorg => directory,
  voor => Bestand['/file1'],
}
bestand { '/file3': zorg => afwezig }
Bestand['/file1'] -> Bestand['/file3']

Afhankelijkheden en notificaties

Documentatie hier.

Zoals eerder genoemd, zijn eenvoudige afhankelijkheden tussen resources transitief. Let op bij het instellen van afhankelijkheden - het is mogelijk om cyclische afhankelijkheden te creëren, wat een compilatiefout zal veroorzaken.

In tegenstelling tot afhankelijkheden zijn notificaties niet transitief. Voor notificaties gelden de volgende regels:

  • Als een resource een notificatie ontvangt, wordt hij geüpdatet. De acties bij het updaten zijn afhankelijk van het type resource - exec start een commando, voor het SystemD-initiesysteem: herstart een service, package herinstalleert een pakket. Als voor de resource geen acties bij het updaten zijn gedefinieerd, gebeurt er niets.
  • Tijdens één run van Puppet wordt een resource niet meer dan één keer geüpdatet. Dit is mogelijk omdat notificaties afhankelijkheden omvatten en de afhankelijkheidsgrafiek geen cycli bevat.
  • Als Puppet de status van een resource wijzigt, dan stuurt de resource notificaties naar alle resources die erop zijn aangemeld.
  • Als een resource wordt bijgewerkt, dan stuurt deze notificaties naar alle resources die erop zijn aangemeld.

Afhandeling van niet opgegeven parameters

Over het algemeen, als een resource parameter geen standaardwaarde heeft en deze parameter niet in het manifest is opgenomen, zal Puppet dit kenmerk van de overeenkomstige resource op de node niet wijzigen. Bijvoorbeeld, als de resource van het type file de parameter niet is opgegeven owner, dan zal Puppet de eigenaar van het overeenkomstige bestand niet wijzigen.

Kennismaking met klassen, variabelen en definities

Stel, we hebben meerdere nodes waarop dezelfde configuratie aanwezig is, maar er zijn ook verschillen - anders zouden we dit alles in één blok kunnen beschrijven. node {}. Natuurlijk kun je gewoon de gelijke delen van de configuratie kopiëren, maar in het algemeen is dit een slechte oplossing - de configuratie groeit en bij wijzigingen aan het algemene deel van de configuratie moet je hetzelfde op meerdere plaatsen aanpassen. Dit kan gemakkelijk fout gaan, en in het algemeen werd het principe van DRY (don't repeat yourself) niet voor niets bedacht.

Om zo'n probleem op te lossen, is er een constructie zoals klasse.

Klassen

Klasse is een benoemde blok van Puppet-code. Klassen zijn nodig voor het hergebruiken van code.

Eerst moet de klasse worden beschreven. De beschrijving op zich voegt geen enkele resource toe. Een klasse wordt in manifesten beschreven:

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

Na dit kan de klasse worden gebruikt:

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

Laten we in het voorbeeld van de vorige taak de installatie en configuratie van nginx verplaatsen naar een 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
}

Variabelen

De klasse uit het vorige voorbeeld is helemaal niet flexibel, omdat deze altijd dezelfde nginx-configuratie aanlevert. Laten we het zo maken dat het pad naar de configuratie variabel wordt, zodat deze klasse kan worden gebruikt voor het installeren van nginx met elke configuratie.

Dit kan worden gedaan met behulp van variabelen.

Let op: variabelen in Puppet zijn onveranderlijk!

Bovendien kan je pas naar een variabele verwijzen nadat deze is gedeclareerd, anders is de waarde van de variabele undef.

Voorbeeld van werken met variabelen:

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

In Puppet zijn er naamruimten, en variabel heeft respectievelijk zichtbaarheid: een variabele met dezelfde naam kan worden gedefinieerd in verschillende naamruimten. Bij het oplossen van de waarde van de variabele wordt de variabele in de huidige namespace gezocht, vervolgens in de omvattende, en zo verder.

Voorbeelden van naamruimten:

  • globaal — daar komen variabelen die buiten de beschrijving van de klasse of node vallen;
  • de namespace van de node in de beschrijving van de node;
  • de namespace van de klasse in de beschrijving van de klasse.

Om ambiguïteit bij het aanroepen van een variabele te vermijden, kan de namespace in de naam van de variabele worden aangegeven:

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

Laten we afspreken dat het pad naar de nginx-configuratie in de variabele $nginx_conf_sourceligt. Dan zou de klasse als volgt eruitzien:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # hier gebruiken we een variabele in plaats van een vaste string
        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
}

Echter, het gegeven voorbeeld is slecht omdat er enige "verborgen kennis" is dat ergens binnen de klasse een variabele met deze naam wordt gebruikt. Het is veel correcter om deze kennis algemeen te maken — klassen kunnen parameters hebben.

Klasseparameters — dat zijn variabelen in de namespace van de klasse, ze worden opgegeven in de header van de klasse en kunnen als gewone variabelen in het lichaam van de klasse worden gebruikt. De waarden van de parameters worden opgegeven bij het gebruik van de klasse in het manifest.

Een parameter kan een standaardwaarde krijgen. Als een parameter geen standaardwaarde heeft en er geen waarde is opgegeven bij gebruik, zal dit een compilatiefout veroorzaken.

Laten we de klasse uit het bovenstaande voorbeeld parametriseren en twee parameters toevoegen: de eerste, verplichte — het pad naar de configuratie, en de tweede, optionele — de naam van het pakket met nginx (in Debian zijn er bijvoorbeeld pakketten met alles wat daarin al aanwezig is, en de inhoud van de map, nginx-light, nginx-full).

# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
  $conf_source,
  $package_name = 'nginx-light', # параметр со значением по умолчанию
) {
  package { $package_name:
    ensure => installed,
  }
  -> file { '/etc/nginx':
    ensure  => directory,
    source  => $conf_source,
    recurse => true,
    purge   => true,
    force   => true,
  }
  ~> service { 'nginx':
    ensure => running,
    enable => true,
  }
}

node 'server2.testdomain' {
  # если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
  # *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
  class { 'nginx_example':
    conf_source => 'puppet:///modules/example/nginx-conf',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

In Puppet zijn variabelen getypeerd. Er zijn veel datatypes. Datatypes worden meestal gebruikt voor de validatie van waarden van parameters die aan klassen en definities worden doorgegeven. Als de doorgegeven parameter niet overeenkomt met het opgegeven type, zal dit een compilatiefout veroorzaken.

Het type wordt direct voor de naam van de parameter geschreven:

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

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

Elke klasse is een resource type klasse. Net als bij andere types resources, kan er niet meer dan één exemplaar van dezelfde klasse op één node zijn.

Als je probeert een klasse twee keer aan dezelfde node toe te voegen met behulp van class { 'classname':} (ongeacht of met verschillende of dezelfde parameters), zal er een compilatiefout optreden. In het geval van het gebruik van een klasse in de stijl van een resource kun je echter onmiddellijk in de manifest duidelijk al zijn parameters opgeven.

Echter, als je include, kun je de klasse zoveel keer toevoegen als je wilt. Het komt erop neer dat include — een idempotente functie die controleert of de klasse al in de catalogus is toegevoegd. Als de klasse nog niet in de catalogus staat, voegt hij deze toe; als deze al bestaat, doet hij verder niets. Maar in het geval van het gebruik van include kun je de parameters van de klasse tijdens de verklaring van de klasse niet opgeven - alle vereiste parameters moeten worden opgegeven in de externe gegevensbron - Hiera of ENC. We zullen hierover in het volgende artikel spreken.

Defines

Zoals in het vorige blok gesproken, kan dezelfde klasse niet meer dan eens op een node aanwezig zijn. In sommige gevallen moet je echter de mogelijkheid hebben om hetzelfde codeblok met verschillende parameters op dezelfde node toe te passen. Met andere woorden, er is behoefte aan een nieuw type resource.

Bijvoorbeeld, om een PHP-module te installeren, doen we in Avito het volgende:

  1. Installeer het pakket met deze module.
  2. Maak een configuratiebestand aan voor deze module.
  3. Maak een symlink naar de config voor php-fpm.
  4. Maak een symlink naar de config voor php cli.

In dergelijke gevallen wordt een constructie zoals define (define, defined type, defined resource type) gebruikt. Define lijkt op een klasse, maar er zijn verschillen: ten eerste is elke define een type resource, niet een resource; ten tweede heeft elke define een impliciete parameter $title, waar de naam van de resource tijdens de verklaring naartoe gaat. Net als bij klassen moet de define eerst worden beschreven, waarna deze gebruikt kan worden.

Een vereenvoudigd voorbeeld met een module voor PHP:

definie 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 van Debian php-pakketten creëren zelf symlinks en herstarten de php-fpm service - dit is niet nodig, omdat we zowel met symlinks als de service werken via 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' }
}

In de definitie is het het gemakkelijkst om een Duplicate declaration fout te vangen. Dit gebeurt als er in de definitie een resource met een constant naam is, en op een bepaalde node meer dan één instantie van die definitie.

Om je hiertegen te beschermen, moeten alle resources binnen de definitie een naam hebben die afhankelijk is van $title. Als alternatief - idempotente toevoeging van resources, in de eenvoudigste gevallen is het voldoende om gemeenschappelijke resources voor alle instanties van de definitie in een aparte klasse te plaatsen en deze klasse in de definitie in te sluiten - de functie include is idempotent.

Er zijn ook andere manieren om idempotentie te bereiken bij het toevoegen van resources, namelijk het gebruik van functies defined en ensure_resources, maar daarover vertel ik in de volgende aflevering.

Afhankelijkheden en meldingen voor klassen en definities

Klassen en definities voegen de volgende regels toe aan het verwerken van afhankelijkheden en meldingen:

  • een afhankelijkheid van een klasse/definitie voegt afhankelijkheden van alle resources van de klasse/definitie toe;
  • de afhankelijkheid van een klasse/definitie voegt afhankelijkheden toe aan alle resources van de klasse/definitie;
  • een melding van een klasse/definitie meldt alle resources van de klasse/definitie;
  • een abonnement op een klasse/definitie schrijft zich in voor alle resources van de klasse/definitie.

Voorwaardelijke operatoren en selectors

Documentatie hier.

als

Hier is het eenvoudig:

if UITDRUKKING1 {
  ...
} elsif UITDRUKKING2 {
  ...
} else {
  ...
}

unless

tenzij — is eigenlijk het tegenovergestelde van if: de codeblok wordt uitgevoerd als de uitdrukking onjuist is.

tenzij UITDRUKKING {
  ...
}

case

Hier is ook niets ingewikkelds. Gewone waarden (strings, getallen, enz.), reguliere expressies, en ook datatypes kunnen als waarden worden gebruikt.

case UITDRUKKING {
  WAARDE1: { ... }
  WAARDE2, WAARDE3: { ... }
  default: { ... }
}

Selectoren

Een selector is een taalkonstrukt dat lijkt op case, maar in plaats van een codeblok uit te voeren, geeft het een waarde terug.

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

Modules

Wanneer de configuratie klein is, is het gemakkelijk om deze in één manifest te houden. Maar naarmate we meer configuratie beschrijven, wordt het manifest groter met meer klassen en knooppunten, en wordt het ongemakkelijk om mee te werken.

Bovendien is er het probleem van hergebruik van code — wanneer al de code in één manifest staat, is het moeilijk om deze code met anderen te delen. Om deze twee problemen op te lossen, heeft Puppet een entiteit genaamd modules.

Modules — dit zijn verzamelingen van klassen, definities en andere Puppet-entiteiten die in een aparte directory zijn geplaatst. Met andere woorden, een module is een onafhankelijk stuk Puppet-logica. Bijvoorbeeld, er kan een module zijn voor het werken met nginx, en daarin staat alleen dat wat nodig is om met nginx te werken, en er kan een module zijn voor het werken met PHP, enzovoort.

Modules worden versiebeheer en ondersteunen ook afhankelijkheden tussen modules. Er is een open repository voor modules — Puppet Forge.

Op de puppet-server bevinden modules zich in de subdirectory modules van de hoofd directory. Binnen elke module is er een standaard mappenstructuur — manifests, files, templates, lib, enzovoorts.

Bestandsstructuur in een module

In de wortel van de module kunnen de volgende directories met beschrijvende namen staan:

  • manifests — hierin bevinden zich de manifesten
  • files — hierin bevinden zich de bestanden
  • templates — hierin bevinden zich de sjablonen
  • lib — hierin bevindt zich de Ruby-code

Dit is geen volledige lijst van directories en bestanden, maar voor dit artikel is dit voorlopig voldoende.

Resource namen en bestandsnamen in een module

Documentatie hier.

Resources (klassen, definities) in een module kunnen niet willekeurig worden genoemd. Daarnaast is er een directe overeenkomst tussen de naam van de resource en de naam van het bestand waarin Puppet de beschrijving van deze resource zoekt. Als de naamgevingsregels worden geschonden, zal Puppet de beschrijving van de resources simpelweg niet vinden, wat resulteert in een compilatiefout.

De regels zijn eenvoudig:

  • Alle resources in de module moeten binnen de namespace van de module zijn. Als de module foo heet, foo, dan moeten alle resources daarin de naam foo::hebben, of simpelweg foo.
  • Een resource met de naam van de module moet in het bestand init.pp.
  • staan. Voor andere resources is de naamgevingsstructuur als volgt:
    • de prefix met de naam van de module wordt weggelaten
    • alle dubbele punten, indien aanwezig, worden vervangen door schuine strepen
    • de extensie wordt toegevoegd .pp

Ik demonstreer dit aan de hand van een voorbeeld. Stel dat ik een module schrijf met alles wat daarin al aanwezig is, en de inhoud van de map. Deze bevat de volgende bronnen:

  • klasse met alles wat daarin al aanwezig is, en de inhoud van de map beschreven in het manifest init.pp;
  • klasse nginx::service beschreven in het manifest service.pp;
  • define nginx::server beschreven in het manifest server.pp;
  • define nginx::server::location beschreven in het manifest server/location.pp.

Sjablonen

Zeker weet u zelf wel wat sjablonen zijn, ik zal hier niet in detail op ingaan. Maar voor de zekerheid laat ik een link naar Wikipedia.

Hoe sjablonen te gebruiken: de waarde van het sjabloon kan worden onthuld met behulp van de functie template, waarbij het pad naar het sjabloon wordt doorgegeven. Voor bronnen van het type file gebruik samen met de parameter content. Bijvoorbeeld zo:

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

Een pad van de vorm / impliceert het bestand /modules//templates/.

Bovendien is er de functie inline_template — deze neemt de tekst van het sjabloon als invoer, niet de bestandsnaam.

In sjablonen kunnen alle Puppet-variabelen in de huidige scope worden gebruikt.

Puppet ondersteunt sjablonen in ERB- en EPP-formaat:

Kort over ERB

Sturingsconstructies:

  • <%= ВЫРАЖЕНИЕ %> — plaatst de waarde van de uitdrukking
  • <% ВЫРАЖЕНИЕ %> — berekent de waarde van de uitdrukking (zonder deze in te voegen). Hierin komen gebruikelijk voor: voorwaardelijke operatoren (if), lussen (each).
  • <%# КОММЕНТАРИЙ %>

Uitspraken in ERB worden geschreven in Ruby (eigenlijk is ERB Embedded Ruby).

Om toegang te krijgen tot variabelen uit het manifest, moet je toevoegen @ aan de naam van de variabele. Om de nieuwe regel die na de sturingsconstructie verschijnt te verwijderen, moet de sluit-tag worden gebruikt -%>.

Voorbeeld van het gebruik van een sjabloon

Stel dat ik een module schrijf voor het beheren van ZooKeeper. De klasse die verantwoordelijk is voor het maken van de configuratie ziet er ongeveer zo uit:

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

En het bijbehorende sjabloon zoo.cfg.erb — zo:

0 -%>

server.=::;



dataDir=


=

Feiten en ingebouwde variabelen

Vaak hangt een specifieke configuratieonderdeel af van wat er op dat moment op de node gebeurt. Bijvoorbeeld, afhankelijk van welke Debian-revisie is geïnstalleerd, moet de juiste versie van het pakket worden geïnstalleerd. Het is mogelijk om dit handmatig bij te houden door de manifesten opnieuw te schrijven wanneer de nodes veranderen. Maar dit is een onbetrouwbare aanpak; automatisering is veel beter.

Voor informatie over nodes in Puppet is er een mechanisme genaamd feiten. Feiten zijn informatie over de node die in de manifesten beschikbaar is in de vorm van gewone variabelen in de globale naamruimte. Bijvoorbeeld, de hostnaam, versie van het besturingssysteem, architectuur van de processor, lijst van gebruikers, lijst van netwerkinterfaces en hun adressen, en nog veel meer. Feiten zijn beschikbaar in manifesten en sjablonen als gewone variabelen.

Voorbeeld van werken met feiten:

notify { "Running OS ${facts['os']['name']} versie ${facts['os']['release']['full']}": }
# Het type notify resource geeft simpelweg een bericht uit in de log

Formeel gesproken heeft een feit een naam (string) en een waarde (verschillende types zijn beschikbaar: strings, arrays, dictionaries). Er zijn een set ingebouwde feiten. Je kunt ook je eigen schrijven. Feitenverzamelaars worden beschreven als functies in Ruby, of als uitvoerbare bestanden. Feiten kunnen ook worden gepresenteerd in de vorm van tekstbestanden met gegevens op de nodes.

Tijdens de werking kopieert de puppet-agent eerst alle beschikbare feitenverzamelaars van de puppetserver naar de node, waarna deze wordt uitgevoerd en de verzamelde feiten naar de server worden verzonden; pas daarna begint de server met het compileren van de catalogus.

Feiten in de vorm van uitvoerbare bestanden

Deze feiten worden in modules geplaatst in de directory facts.d. Uiteraard moeten de bestanden uitvoerbaar zijn. Bij uitvoering moeten ze informatie naar de standaarduitvoer sturen, hetzij in YAML-formaat, hetzij in het formaat 'sleutel=waarde'.

Vergeet niet dat feiten worden verspreid naar alle nodes die onder het bestuur van de puppetserver staan waarop je module wordt uitgerold. Zorg er daarom in het script voor dat alle benodigde programma's en bestanden voor de werking van je feit aanwezig zijn in het systeem.

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

Feiten in Ruby

Deze feiten worden in modules geplaatst in de directory lib/facter.

# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
  confine do
    Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
  end
  confine do
    File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
  end
# в блоке setcode происходит собственно вычисление значения факта
  setcode do
    hash = {}
    if (out = Facter::Core::Execution.execute('ladvdc -b'))
      out.split.each do |l|
        line = l.split('=')
        next if line.length != 2
        name, value = line
        hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
      end
    end
    hash  # значение последнего выражения в блоке setcode является значением факта
  end
end

Tekstfeiten

Deze feiten worden op nodes geplaatst in de directory /etc/facter/facts.d in de oude Puppet of /etc/puppetlabs/facts.d in de nieuwe Puppet.

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

Toegang tot feiten

Je kunt de feiten op twee manieren raadplegen:

  • via een woordenboek $facts: $facts['fqdn'];
  • door de naam van het feit als variabelenaam te gebruiken: $fqdn.

Het is het beste om het woordenboek te gebruiken $facts, nog beter is het om de globale namespace op te geven ($::facts).

Hier is het benodigde documentatiegedeelte.

Ingebouwde variabelen

Naast feiten zijn er ook nog een aantal variabelen, beschikbaar in de globale namespace.

  • vertrouwde feiten — variabelen die worden afgeleid van het klantcertificaat (aangezien het certificaat meestal op de Puppet-server wordt uitgegeven, kan de agent zijn certificaat niet zomaar wijzigen, daarom zijn de variabelen ‘vertrouwd’): naam van het certificaat, naam van de host en domein, extensies uit het certificaat.
  • server feiten — variabelen die betrekking hebben op serverinformatie — versie, naam, IP-adres van de server, omgeving.
  • agent feiten — variabelen die direct door puppet-agent worden toegevoegd, en niet door facter — naam van het certificaat, versie van de agent, versie van Puppet.
  • master variabelen — variabelen van de puppetmaster (sic!). Daar is ongeveer hetzelfde als in server feiten, plus de waarden van configuratieparameters zijn beschikbaar.
  • compiler variabelen — compiler variabelen die verschillen in elke scope: naam van de huidige module en de naam van de module waarin naar het huidige object is verwezen. Ze kunnen worden gebruikt om bijvoorbeeld te controleren of je privéklassen niet rechtstreeks vanuit andere modules worden gebruikt.

Aanvulling 1: hoe voer je dit allemaal uit en debug je het?

In het artikel waren veel voorbeelden van Puppet-code, maar er werd helemaal niet uitgelegd hoe je deze code moet uitvoeren. Nou, ik corrigeer dat.

Voor Puppet is alleen een agent voldoende, maar in de meeste gevallen is er ook een server nodig.

Agent

Minimaal versie vijf, de puppet-agent pakketten uit de officiële Puppetlabs-repository bevatten alle afhankelijkheden (ruby en de bijbehorende gem's), dus er zijn geen problemen met installatie (ik heb het over Debian-gebaseerde distributies — we gebruiken geen RPM-gebaseerde distributies).

In de eenvoudigste gevallen is het voldoende om de puppet-configuratie te laten uitvoeren door de agent in een serverloze modus: op voorwaarde dat de Puppet-code op de node is gekopieerd, start je puppet apply:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Notice: Gecompileerde catalogus voor atikhonov.localdomain in productieomgeving in 0,01 seconden
Notice: Hallo wereld!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: gedefinieerd 'bericht' als 'Hallo wereld!'
Notice: Toegepaste catalogus in 0,01 seconden

Het is beter om de server op te starten en de agents in daemon-modus op de nodes te draaien — dan passen ze elke half uur de configuratie toe die van de server is gedownload.

Je kunt het push-model imiteren — ga naar de node die je interesseert en voer uit sudo puppet agent -t. De sleutel -t (--test) omvat eigenlijk verschillende opties die ook afzonderlijk kunnen worden ingeschakeld. Onder deze opties zijn de volgende:

  • niet in daemon-modus werken (de agent wordt standaard in daemon-modus gestart);
  • stoppen na het toepassen van de catalogus (de agent blijft standaard werken en toepassen configuratie elke half uur);
  • een gedetailleerd logbestand schrijven;
  • wijzigingen in bestanden tonen.

De agent heeft een modus zonder wijzigingen — deze kan worden gebruikt wanneer je niet zeker weet of je een juiste configuratie hebt geschreven en wilt controleren wat de agent tijdens het proces zal veranderen. Deze modus wordt ingeschakeld met de parameter --noop in de opdrachtregel: sudo puppet agent -t --noop.

Daarnaast kan je een debug-log inschakelen — hierin schrijft puppet over alle acties die het onderneemt: over de hulpbron die op dat moment wordt verwerkt, over de parameters van die hulpbron, over welke programma's worden uitgevoerd. Uiteraard is deze parameter --debug.

Server

De volledige configuratie van de puppetserver en deployment van code op deze server zal ik in dit artikel niet bespreken, ik zal alleen zeggen dat er uit de doos een goed werkend serverversie wordt geïnstalleerd, die geen extra configuratie vereist om te werken met een klein aantal nodes (zeg maar tot honderd). Een groter aantal nodes vereist al tuning — standaard start de puppetserver niet meer dan vier werkers, voor hogere prestaties moet je dit aantal verhogen en niet vergeten de geheugenlimieten te verhogen, anders zal de server het grootste deel van de tijd bezig zijn met garbage collection.

Voor code-deployment — als je snel en eenvoudig wilt, kijk dan naar (r10k)[https://github.com/puppetlabs/r10k], voor kleine installaties zou dit voldoende moeten zijn.

Aanvulling 2: aanbevelingen voor het schrijven van code

  1. Verplaats alle logica naar klassen en definities.
  2. Houd klassen en definities in modules, niet in manifesten met beschrijvingen van knooppunten.
  3. Maak gebruik van feiten.
  4. Maak geen if-statements op basis van hostnamen.
  5. Aarzel niet om parameters voor klassen en definities toe te voegen — dit is beter dan impliciete logica die verborgen is in de body van de klasse/definitie.

Waarom ik dit aanraad — dat zal ik uitleggen in het volgende artikel.

Conclusie

Daarmee beëindigen we de inleiding. In het volgende artikel zal ik praten over Hiera, ENC en PuppetDB.

Alleen geregistreerde gebruikers kunnen deelnemen aan de enquête. Log in, alstublieft.

In werkelijkheid is er veel meer materiaal — ik kan artikelen schrijven over de volgende onderwerpen, stem omdat het je interessant leek om te lezen:

  • 59,1%Geavanceerde Puppet-constructies — sommige next-level zaken: lussen, mapping en andere lambda-uitdrukkingen, resource collectors, geëxporteerde resources en inter-host communicatie via Puppet, tags, providers, abstracte datatypes.
  • 31,8%«Ik ben de admin van mijn moeder» of hoe wij bij Avito verschillende Puppet-servers van verschillende versies met elkaar hebben verbonden, en in principe een deel over het beheren van een Puppet-server.
  • 81,8%Hoe wij Puppet-code schrijven: tooling, documentatie, testen, CI/CD.

Er hebben 22 gebruikers gestemd. 9 gebruikers hebben zich onthouden.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster