In deze blogpost zullen we een aantal eenvoudige basismethoden bekijken over het omgaan met default waarden in profielen en (defined) resources in Puppet. Ook zal ik een aantal algemene richtlijnen met betrekking tot de naamgeving van parameters behandelen alsmede een paar methoden over het omgaan met grotere hoeveelheden parameters. Dit artikel betreft alleen site-specifieke Puppet code, m.a.w profielen specifiek voor een organisatie of omgeving. Als je modules wilt maken voor algemeen gebruik is het altijd het best om de daarvoor geldende conventies te gebruiken, dat wil zeggen een params.pp file om verschillen tussen Operating Systems en distributies op te vangen. Ik doe tevens de aanname dat de nu gangbare “roles and profiles” methode gehanteerd wordt, hoewel de meeste aanbevelingen ook opgaan als dat niet het geval is.

Een paar algemene overwegingen

Het juist gebruik van parameters kan je Puppet modules bruikbaarder en meer algemeen inzetbaar maken, ook voor andere mensen in je team of organisatie. Het kiezen van de juiste default waarden helpt daarbij door je Puppet code leesbaarder en duidelijker te maken waardoor het eenvoudig te hergebruiken is of ingezet kan worden in verschillende rollen.

Goede default waarden moeten de volgende eigenschappen hebben:

  1. De default moet de meest redelijke, vaakst gebruikte, veilig te gebruiken waarde zijn.
  2. Elke default waarde moet overschreven kunnen worden.
  3. Voor zover mogelijk moeten default waarden slechts eenmalig gedefinieerd zijn en niet op meerdere plekken bestaan.
  4. Parameter namen moeten bondig, beschrijvend, consistent en niet voor meerdere uitleg vatbaar zijn.
  5. Zet geen default als het van belang is dat een bepaalde parameter of optie zorgvuldig en weloverwogen gekozen wordt.

Punt vier verdient wat extra uitleg, het best geïllustreerd door middel van een voorbeeld van hoe niet class parameters te kiezen:

class profile::our_application(
  String  $enable_option1  = ‘yes’,
  Boolean $option2_enable  = true,
  Boolean $disable_option3 = false,
  Enum[’yes’, ‘off’] $donot_disable_option4 = ‘off’
) {
..
}

Zoals duidelijk te zien is in dit voorbeeld zijn deze parameters, hoewel syntaxtisch correct, erg slecht gekozen. Om precies te zijn:

  • Opties moeten, voor zover mogelijk, dezelfde methode gebruiken om aan of uit gezet te worden. Een set van Boolean waarden is gemakkelijker te interpreteren dan verscheidene Boolean en String types door elkaar.
  • Boolean waarden moeten consistent zijn in hun betekenis. Dat wil zeggen, de waarde “true” zou moeten betekenen dat het bewuste ding aan, actief of aanwezig is, en “false” moet betekenen dat het betreffende ding uit, uitgeschakeld, inactief of niet aanwezig is.
  • Voorkom dubbele (of drievoudige) negatieven. Zeg niet: “disable $x = false”, maar “enable $x = true”.

Tenslotte, en dit gaat zeker op voor site-specifieke Puppet profielen, in het algemeen is het een goed idee om de totale hoeveelheid class parameters te beperken tot een bescheiden hoeveelheid. In een volgende blogpost over “option hashes” zal ik demonstreren hoe je dat kan doen en tegelijkertijd kan zorgen dat alle opties overschrijfbaar blijven.

Default waarden als class parameters

Goed gebruik van default waarden kan het aantal parameters verminderen dat expliciet moet worden opgegeven bij het gebruik van een bepaalde class. De syntax om dit te doen is eenvoudig: specificeer simpelweg de class variabelen en geef deze een waarde.

Bijvoorbeeld: stel installatie-directories in door deze als strings te definiëren:

class profile::site_application(
  String $bindir = ‘/opt/site_app/bin’,
  String $logdir = ‘/opt/site_app/log’,
  String $cfgdir = ‘/opt/site_app/etc’
){ ...

Een betere manier om hetzelfde te bereiken door het gebruik van een “basedir” parameter:

class profile::site_application(
String $basedir = ‘/opt/site_app’,
String $bindir  = “${basedir}/bin”,
String $logdir  = “${basedir}/log”,
String $cfgdir  = “${basedir}/etc”
){ …

In dit voorbeeld volgen de bin-, log- en etc-directories uit de default die gezet is in $basedir waardoor het nu mogelijk is om met een enkele parameter alledrie de directories in te stellen op de meest gebruikelijke optie (alledrie in dezelfde directory). Je zou dit kunnen generaliseren als een profile dat een set aan directories maakt voor gebruik in meerdere applicaties door er een defined type van de maken met een verplichte basedir parameter (of die laten volgen uit de applicatie-naam):

define profile::prep::directories(
  String $app_name,  # A required parameter
  String $basedir  = “/opt/${app_name}”,
  String $bindir   = “${basedir}/bin”,
  String $logdir   = “${basedir}/log”
){ ...

Nu is het eenvoudig om het maken van een directorystructuur vanuit verschillende profiles uit te voeren door het defined type aan te roepen met app_name als het enige vereiste argument. Je zou zelfs $app_name de default waarde $title kunnen geven, waardoor als default de resource title gebruikt wordt en deze aanroep:

profile::prep::directories { ‘foobar_app’: }

… genoeg is om een hele directory-structuur onder /opt/foobar_app op te zetten. Met name voor zaken waarin er nauwelijks of geen afwijkingen ten opzichte van de standaard zijn is dit heel handig. Denk hierbij bijvoorbeeld aan useraccounts, home-directories en SSH-keys, maar ook aan configuraties die in veel applicaties moeten worden geconfigureerd maar veelal hetzelfde zijn, zoals e-mailadressen en smtp-server adressen.

Zoals uit het voorbeeld blijkt maakt zelfs het verplaatsen van variabelen zoals $logdir uit de body naar de class-parameters een profiel meer bruikbaar. Houd er hier wel rekening mee dat een parameter die gewijzigd kan worden, ook daadwerkelijk gewijzigd moet kunnen worden. Als de gebruiker een logdirectory kan opgeven en de default kan overschrijven moet deze wijziging mogelijk ook tot uitdrukking komen in de applicatie die naar die directory logt en in bijvoorbeeld geautomatiseerde jobs die logfiles opschonen.

Default waarden vastleggen in Hiera

Het vastleggen van default waarden in Hiera is een andere manier om default class parameters te gebruiken. Deze methode heeft een aantal voor- en nadelen maar kan goed gebruikt worden naast defaults in class parameters. Als er twee of meer meer waarden gedefinieerd zijn voor een bepaalde parameter zal Puppet de te gebruiken waarde bepalen op basis van deze volgorde:

  1. Gebruik de parameter die expliciet gezet is in de aanroep van de class of resource, tenzij deze waarde undef is.
  2. Doe een hiera lookup van de “fully qualified parameter name” (bijv. profile::foo::bar).
  3. Gebruik de default waarde uit de class definitie.
  4. Faal met een compilatiefout als geen waarde gevonden kon worden.

Eerst een voorbeeld om het gebruik van Hiera te illustreren:

class profile::foobar_app::accounts (
  Array $admins,
  Array $users     = []
) {...

En in de Hiera data (hier in yaml):

profile::foobar_app::accounts::admins:

– ‘john’

profile::foobar_app::accounts::users:

– ‘terry’

– ‘graham’

– ‘eric’

– ‘michael’

Als deze class aangeroepen wordt zal Puppet een lookup doen van de class parameters admins en users en de twee arrays uit Hiera vinden en gebruiken. Merk op dat, in dit voorbeeld, “admins” geen default waarde in de class definitie heeft, terwijl “users” een lege array als default heeft. Dit verschil is belangrijk omdat het betekent dat de “admins” parameter vereist is (omdat het anders zal falen, zie stap 4 hierboven) terwijl het profile een lege waarde zal gebruiken voor de $users variabele als er in Hiera niets gevonden wordt.

Kortom, in de meest eenvoudige vorm kan je volstaan met het simpelweg vervangen van waarden in de class definitie (of ze vervangen door lege waarden of “undef”), en de defaults specificeren in Hiera. Dit levert een aantal voordelen op:

  • Scheiding van code en data. Hiera wordt gebruikt voor het opslaan van data, en het profile zelf bevat slechts code.
  • sommige pameters moeten opgegeven worden, maar het is niet mogelijk een default waarde in de class definitie te zetten die aan alle eisen van een goede default voldoet. Het voorbeeld hierboven illustreert dit: Het is noodzakelijk ten minste één administrator op te geven, maar het is een erg slecht idee om een dergelijke waarde als default op te nemen in een profile.
  • Hiera is beter geschikt voor het omgaan met met grotere hoeveelheden data. Eenvoudige strings zijn goed op te nemen in een profile, maar hashes of arrays met tientallen of honderden elementen maken een profile onoverzichtelijk en moeilijker beheerbaar.
  • Hiera is de betere keuze als een profile gebruikt wordt door verschillende groepen systemen waarbij elke groep een eigen specifieke set aan default waarden heeft (bijvoorbeeld een domainname, proxy-server adres of klant-specifieke instellingen)

Er zijn echter ook een aantal nadelen, of beter gezegd beperkingen aan het gebruik van Hiera voor het vastleggen van default waarden:

  • Je kan Hiera niet gebruiken om defaults in te stellen voor defined resources. Dit zijn geen classes, en er wordt dus geen impliciete lookup gedaan. (Er is wel een andere methode om dit toch te doen, zie daarvoor het stukje “parameter defaults door middel van puppet expressies” voor een voorbeeld over het gebruik van een expliciete lookup ).
  • Default waarden instellen als class parameters maakt het veelal eenvoudiger om het profile te onderhouden of aan te passen omdat het duidelijker maakt wat het beoogde of verwachte gebruik van het profile is. Neem als voorbeeld een profile dat een MySQL database installeert en beheert. Als je nooit een andere username dan “mysql” gebruikt, waarom zou je het dan in Hiera opslaan in plaats van als de default in het profile zelf? Een gewone class parameter of een option-hash met mysql als default is dan een betere keuze, een die in een oogopslag duidelijk maakt dat als er een andere user opgegeven is er iets bijzonders aan de hand is.
  • Zoals geillustreerd in het profile::prep::directories voorbeeld, je kan Hiera niet gebruiken om te refereren aan andere class parameters (n.b. het is wel degelijk mogelijk, maar dit is een geavanceerd gebruik dat spaarzaam gebruikt moet worden en buiten de strekking van dit artikel valt).
  • Het is mogelijk dat je defaults wil laten volgen uit constructies of logica in je profile, of bepaalde waarden wil zetten gebaseerd op andere parameters. Beide opties zijn moeilijk te combineren met het gebruik van Hiera.

Expressies als parameter defaults

Naast de gebruikelijke eenvoudige class parameters als Booleans, Strings of Arrays, of het gebruik van Hiera om deze zelfde typen vast te leggen, is er nog een methode om de gewenste default waarden vast te stellen, namelijk als de output van een expressie. In het algemeen kunnen de expressies die in een profile gebruikt worden ook gebruikt worden als class parameter. Ter illustratie een paar voorbeelden:

class profile::foobar_app::settings(
  $mailhost = $profile::base::netconfig::mailhost
)

Hier wordt verwezen naar en ander profile om de default waarde uit te betrekken. Dit vereist uiteraard dat profile::base::netconfig geÏnclude is.

class profile::foobar_app::settings(
  $admins = lookup(’application::foobar::admins’, {})
)

Hier wordt de lookup functie gebruikt om een expliciete lookup te doen van een waarde uit Hiera door een andere, meer beschrijvender key (application::foobar::admins) te gebruiken. Dit stelt je in staat om groepen van aan elkaar gerelateerde instellingen te bundelen. Zeker als bepaalde data door verschillende profiles voor verschillende doeleinden gebruikt wordt. Let op dat als profile::foobar_app::settings::admins in Hiera gezet is die waarde voorrang krijgt. Merk ook op dat alle functionaliteit van de lookup functie beschikbaar zijn. Zo zou je bijvoorbeeld ook aan de lookup functie een default mee kunnen geven door middel van de default_value parameter van lookup().

Nog een andere mogelijkheid is het gebruik van een zogenaamde selector, zoals hier:

class profile::foobar_app::settings(
  Enum[’low’, ‘high’] $verbosity = ‘low’,
  Integer $loglevel      = $verbosity ? { ‘low’ => 1, ‘high’ => 5 },
  Boolean $mail_on_error = $verbosity ? { ‘low’ => false, ‘high’ => true }
)

In dit voorbeeld laten we $loglevel en $mail_on_error volgen uit $verbosity, terwijl het nog steeds mogelijk blijft om deze instellingen anders in te stellen via Hiera. Een laatste voorbeeld, hier door gebruik te maken van de “fact()” functie uit Puppet’s stdlib:

class profile::foobar_app::settings(
  $assigned_memory = round(fact('memory.system.total_bytes') * 0.50 )
)

In dit voorbeeld wordt de instelling $assigned_memory ingesteld op de helft van de totale hoeveelheid geheugen dat het systeem heeft.

Zoals uit deze voorbeelden duidelijk blijkt is het gebruik van functies en selectors direct in de class parameters een krachtige wijze om je Puppet profielen eenvoudiger en beter onderhoudbaar te maken. Het combineert twee van de belangrijkste vereisten aan default waarden: Het maakt het duidelijk waar een default waarde vandaan komt, en het maakt het eenvoudig aan te passen.

Parameter hashes

Niet alle class parameters zijn altijd onafhankelijke instellingen. Soms komt het voor dat het nodig is om een hele groep aan aan elkaar gerelateerde opties in te stellen alleen, en alleen als, aan een andere voorwaarde wordt voldaan of als een andere instelling is gezet. Wanneer je bijvoorbeeld de “data-at-rest-encryption” optie in MariaDB aan zet, moet je ook een set andere instellingen toevoegen. Deze instellingen worden door MariaDB alleen geaccepteerd als de bijbehorende plugin is geladen. Er is dus een eenvoudige methode nodig om een hele set aan nieuwe default waarden toe te voegen op basis van een enkele conditie, terwijl het wel mogelijk moet blijven om al die waarden individueel te overschrijven.  Daarbij kan het bovendien noodzakelijk zijn om andere default waarden te veranderen als die conditie zich voordoet om fouten als gevolg van conflicterende parameters te voorkomen.

Een oplossing voor dit probleem is het gebruik van een parameter hash, wat niets meer is dan een datastructuur van het type hash waarin alle extra gewenste parameters opgeslagen zijn als key-value paren. Puppet heeft ingebouwde functies om deze waarden te manipuleren en samen te voegen.

De eenvoudigste vorm gebruikt de + operator om meerdere hashes samen te voegen. Deze operator maakt een nieuwe hash die alle keys uit de samen te voegen hashes bevat. Als een key in meerdere hashes voorkomt wordt de waarde gebruikt uit de laatst toegevoegde (meest rechtse) hash. Bijvoorbeeld:

$parameters = $default_parameters + $override_parameters

Als de default- en override-parameter hashes de keys en values hebben zoals in onderstaande tabel, dan zal de resulterende hash de keys en values hebben uit beide hashes, waarbij de waarden uit override_parameters gebruikt zullen worden als een key in beide hashes voorkomt.

Defaults Overrides Resulting hash
$default_parameters = {
  ‘color’    => ‘blue’,
  ‘shape’    => ‘square’,
  ‘material’ => ‘metal’,
  ‘size’     => ‘small’
}
$override_parameters = {
  ‘color’    => ‘red’,
  ‘shape’    => ‘square’,
  ‘material’ => undef,
  ‘name’     => ‘Dave’
}
$parameters = {
  ‘color’    => ‘red’,
  ‘shape’    => ‘square’,
  ‘material’ => ‘metal’,
  ‘size’     => ‘small’,
  ‘name’     => ‘Dave’
}

Dit kan met meerdere hashes gedaan worden, bijvoorbeeld:

$params = $default_params + $site_params + $customer_settings + $override_params

Merk op dat een of meerdere van deze hashes leeg mogen zijn, dat veranderd niets aan de wijze waarop deze methode werkt. Een som van meerdere lege hashes zal simpelweg resulteren in een lege hash.

De op deze wijze verkregen gecombineerde hash kan vervolgens gebruikt worden met de zogenaamde splat-operator om de gecombineerde waarden (hier $params) door te geven aan de resource:

pet_block { $order_id:
  * => $params
}

Dit is functioneel identiek aan het onderstaande voorbeeld:

$color = ‘red’
$shape = ‘square’
$material = ‘metal’
$size = ‘small’
$name = ‘Dave’

pet_block { $order_id:
  color    => $color,
  shape    => $shape,
  material => $material
  size     => $size,
  name     => $name
}

Zoals eerder vermeld kan deze methode goed gebruikt worden om een set aan (default) data toe voegen als aan een andere conditie voldaan wordt, bijvoorbeeld:

class profile::foobar_app::settings(
  Boolean $enable_log_export = false,
  Hash $log_export_overrides  = {}
) {
  ## A hash containing defaults. Note: you could
  ## retrieve these from Hiera as well, either by
  ## using a class parameter or an explicit lookup()
  $log_export_defaults = {
    ‘enable’     => true,
    ‘logserver’  => $profile::base::netconfig::logserver,
    ‘proto’      => ‘syslog’,
    ‘port’       => 514,
    ‘log_format’ => ‘%e %x %a %m %p %l %e’,
  }

  ## If $enable_log_export is true, store the
  ## merged parameter hashes in $log_export_params.
  ## if it is false, fill it with “enable => false”.
  $log_export_params = $enable_log_export ? {
    true => $log_export_defaults + $log_export_overrides,
    false => { ‘enable’ => false }
  }

  ## Merge all options together in the final options hash
$foobar_config = $defaults + $log_export_params + ........

In dit voorbeeld wordt een enkele eenvoudige Boolean switch $enable_log_export gebruikt om een aantal andere parameters toe te voegen die anders overbodig (of mogelijk zelfs verstorend) zouden zijn.

Dezelfde methode kan gebruikt worden om op eenvoudige wijze defaults afkomstig uit verschillende bronnen met elkaar te combineren. Neem als voorbeeld een situatie waarbij een aantal parameters met site-specifieke settings uit Hiera komen,  andere in de class definitie gezet worden als losse option-hashes zoals in het voorbeeld hierboven en weer anderen het gevolg zijn van logica in het profile of uit functies komen.

Door al deze parameters op te slaan als key-value paren in hashes kunnen ze eenvoudig gecombineerd en later gebruikt worden, wat je profiles beter onderhoudbaar, eenvoudiger en flexibeler maakt.

Een bijkomend voordeel van deze methode is dat het je in staat stelt om complexe instellingen te abstraheren als meerdere eenvoudige “on/off” switches waardoor het voor operators of eerste-lijns support mogelijk wordt om, zonder diepgaande kennis van de onderliggende applicatie, functionaliteit of features aan of uit te schakelen.

 

Reinder Schuitemaker
Consultant
Connata BV