Puppet ist YAML
Martin Alfke
Posted on February 8, 2023
oder: Puppet mit nur 7 Zeilen Code
Gelegentlich sehen wir, dass Menschen Probleme haben, wenn es um den Einsatz von Puppet Konfigurations-Management geht.
Meistens liegt die Ursache in dem notwendigen zeitlichen Aufwand, den man aufbringen muss, um die Puppet Programmiersprache kennen zu lernen.
Generell existieren 2 Wege, wie man eine Infrastruktur mit Puppet verwalten kann:
- eigener Code
- YAML Daten
Eigener Code macht Sinn, wenn man besondere Fälle verwalten muss. z.B. interne Anwendungen, für die keine Puppet Erweiterungen (Module) existieren.
Wir empfehlen grundsätzlich den Einsatz von Hiera in einem Puppet Umfeld.
Hiera ist das Puppet interne Daten Backend, in welchem man Daten in YAML oder JSON Syntax hinterlegen kann.
In diesem Posting erklären wir, wie man mit Hilfe von YAML Daten einen einfachen Einstieg in Puppet erreichen kann.
Node Klassifizierung
Hiera kann sehr einfach für die Node Klassifizierung genutzt werden.
Es stehen 2 unterschiedliche Daten Typen zur Node Klassifizierung zur Verfügung: Array
und Hash[String]
.
Zuerst erklären wir die Array
Syntax:
Innerhalb der Datei manifests/site.pp
nutzen wir die lookup
Funktion und geben den Klassen-Key an. Außerdem setzen wir den erwarteten Datentyp und einen Default-Wert:
# manifests/site.pp
lookup(
{
'name' => 'classes',
'value_type' => Array,
'default_value' => [],
}
).include
Bevor Daten hinzugefügt werden, muss man seine Infrastruktur analysieren: Alle Systeme sollen Standardklassen bekommen, manche Systeme bekommen weitere Klassen auf Basis ihres Einsatzzwecks.
e.g.
Alle Systeme brauchen:
- Sicherheitsrichtlinien
- LDAP/AD Integration
- Monitoring Clients
Datenbank-Server brauchen:
- Datenbank-Installation
- Backup Clients
- Metrik-Exporter
Webserver brauchen:
- Webserver-Installation
- Webanwendung(en)
Datenbank Servers für die Applikation 'A' brauchen:
- Spezifisches Datenbankschema
- Python-Erweiterungen
Webserver für die Applikation 'A' brauchen:
- Möglichkeit Emails zu versenden
- Erweiterte Sicherheitseinstellungen
Innerhalb von Hiera werden Hierarchie-Ebenen genutzt, um Unterschiede in der Infrastruktur hinterlegen zu können.
Die Namen der Hierarchie-Ebenen müssen auf Basis von Facter- oder Puppet Agent Zertifikats-Informationen angegeben werden.
Alle Systeme sollen common
Daten bekommen.
Spezifische Systeme sollen Daten basierend auf application
, service
dnd stage
(prod, test, dev) erhalten.
Üblicherweise empfehlen wir die folgenden Hiera Konfigurationseinstellungen:
# hiera.yaml
---
version: 5
defaults:
datadir: data
hierarchy:
- name: "All hierarchies"
paths:
# node specific data
- "nodes/%{trusted.certname}.yaml"
# application/service-stage data
- "%{trusted.extensions.pp_application}/%{trusted.extensions.pp_service}-%{trusted.extensions.pp_env}.yaml"
# application/service data
- "%{trusted.extensions.pp_application}/%{trusted.extensions.pp_service}.yaml"
# application data
- "%{trusted.extensions.pp_application}.yaml"
# network zone data
- "zone/%{trusted.extensions.pp_zone}.yaml"
# os specific data
- "os/%{facts.os.family}-%{facts.os.version.major}.yaml"
# default data
- "common.yaml"
Es ist zwingend notwendig, dass man die Infrastruktur versteht, bevor man anfängt mit Hiera zu arbeiten!
Jetzt können wir anfangen Klassendaten zu hinterlegen. Diese werden in den entsprechenden Hiera Hierarchie-Dateien hinterlegt. Außerdem wollen wir sicherstellen, dass Hiera in allen Ebenen nach Daten sucht. Dafür muss das merge
Verhalten auf unique
gesetzt werden.
Dies erreicht man mit Hilfe des lookup_options
Keys.
In der Default Hierarchie setzt man am Anfang der Datei (Sichtbarkeit) den Key und gibt dann den classes Key und das merge Verhalten an:
# data/common.yaml
---
lookup_options:
'classes':
merge: 'unique'
classes:
- 'class_a'
- 'class_b'
Die Array Notation hat eine Einschränkung:
Man kann lediglich Klassen in höheren Hierarchie-Ebenen hinzufügen.
Es ist nicht möglich, Klassen wieder zu entfernen!
Hier kann der Hash Datentyp genutzt werden.
Innerhalb eines Hashes setzt man einen eindeutigen Indentifikator (String) und gibt die Klasse als Wert an.
Wenn man den Wert auf einen leeren String setzt, kann man die Klasse wieder entfernen.
Außerdem kann man eine Information ausgeben, anhand derer man sehen kann, dass die Klasse entfernt wurde. Hierfür nutzen wir den echo Resource Type:
Der Puppet Code muss angepasst werden:
0 # manifests/site.pp
1 lookup( 'classes_hash', { 'value_type' => Hash, 'default_value' => {} } ).each |$name, $c| {
2 unless $c.empty {
3 contain $c
4 } else {
5 echo { "Class for ${name} on ${facts['networking']['fqdn']} is disabled": }
6 }
7 }
# data/common.yaml
---
lookup_options:
'classes_hash':
merge:
behavior: 'deep'
classes_hash:
'Beschreibung der Klasse A': 'class_a'
'Beschreibung der Klasse B': 'class_b'
Wenn ein System sehr speziell ist und eine Defaultklasse nicht bekommen soll, kann man einen leeren String angeben:
# data/nodes/different_server.domain.tld.yaml
---
classes_hash:
'Beschreibung der Klasse A': ''
Nutzung von Puppet Modulen (Bibliotheken)
Für viele Anwendungen findet man fertige Puppet Module auf der Puppet Forge.
Leider fehlen bei vielen Modulen Beispiele für die Nutzung der Hiera-Daten.
Glücklicherweise ist es heutzutage best-practice die Parameter zu dokumentieren und die Dokumentation in einer Datei REFERENCE.md zu hinterlegen.
Zukünftigt wird dies auch durch diverse Linter forciert werden.
Ein Beispiel für nginx:
# data/application/webserver.yaml
---
classes_hash:
'webserver for application': 'nginx'
nginx::port: '8080'
In Puppet Modulen gibt es Klassen und gelegentlich auch neue Resource Types. Mit Resource Types kann man angeben, wie eine spezifische Konfiguration erreicht werden soll (z.B. anlegen von nginx Virtual Hosts).
Aber: Resource Types können nicht wie Klassen in Hiera angegeben werden.
Einfache Installation, Konfiguration und Services
Das stdlib Modul beinhaltet eine Klasse, mit welcher Resource Types in Hiera hinterlegt werden können.
Die Klasse hat den Namen: stdlib::manage
Zum generellen Laden der Klasse folgendes in die common.yaml hinzufügen:
---
# data/common.yaml
classes_hash:
'puppet_is_yaml': 'stdlib::manage'
Jetzt muss man nur wissen, welche Resource Types es gibt und welche Parameter man setzen kann.
Innerhalb einer Puppet Agent Installation befinden sich bereits einige Resource Types:
- user
- group
- package
- file
- service
- ...
Die meisten anderen Resource Types werden durch zusätzliche Puppet Modules ausgeliefert.
z.B. PostgreSQL Datenbankverwaltung wird durch einen Resource Type im PostgreSQL Modul ermöglicht.
Auf einem bestehenden Puppet Agent System kann man sich die Liste aller Ruby-basierten Resource Typen mit dem Kommando sudo puppet describe -l
anzeigen lassen.
In Puppet DSL erstellt Defined Types werden in dieser Liste leider nicht angezeigt.
Innerhalb der Daten muss ein Hash unterhalb des Key stdlib::manage::create_resources
angegeben werden.
Der Hash besteht aus drei Ebenen. Die erste Ebene beschreibt den Resource Typ und die nächste Ebene beschreibt die Instanz (Title). In der dritten Ebene werden die Parameter angegeben.
Generelle Syntax:
---
stdlib::manage::create_resources:
'Resource Type1':
'Unique Name1':
'attribute': 'value'
'Unique Name2':
'attribute': 'value'
'Resource Type2':
'Unique Name':
'attribute': 'value'
Im Folgenden ein einfaches Beispiel zur Verwaltung von NTP:
# data/os/RedHat-7.yaml
---
stdlib::manage::create_resources: # Puppet library data lookup
'package': # Resource Type
'ntp': # Type title or unique name
ensure: 'present' # Parameter of resource type
'file':
'/etc/ntp.conf':
ensure: 'file'
source: 'puppet:///modules/profile/time/ntp.conf'
owner: 'root'
group: 'root'
mode: '0644'
require: 'Package[ntp]'
'service':
'ntp':
ensure: 'running'
enable: true
subscribe: 'File[/etc/ntp.conf]'
Defaults setzen und überschreiben, hinzufügen oder entfernen von Parametern
Mit Hilfe von YAML Anchors and Aliases können wir defaults setzen, z.B. file resource defaults.
Hinweis: In YAML müssen Anchors und Aliases in der gleichen Datei hinterlegt sein.
Jede YAML Datei kann ihre eigenen Anchors und Aliases haben.
Man kann nicht auf Anchors in anderen YAML Dateien zugreifen.
Zuerst muss der Anchor definiert werden, hierbei wird ein YAML Block mit &<name>
makiert, was dann auch gleich der Anchor-Name ist:
file_defaults: &file_defaults
owner: 'root'
group: 'root'
mode: '0644'
Inner halb der gleichen YAML Datei kann man den Anchor mit einem Alias referenzieren:
stdlib::manage::create_resources:
file:
'/etc/ntp.conf':
<< : *file_defaults
ensure: 'file'
source: 'puppet:///modules/profile/time/ntp.conf'
require: 'Package[ntp]'
'/etc/secure':
<< : *file_defauts
ensure: 'file'
content: 'admin'
mode: '0400'
Eine der großen Vorzüge von Hiera ist das Überschreiben von Daten.
So kann man etwas global als default hinterlegen, dies aber an Bedürfnisse anderer Systeme oder Anwendungen anpassen.
Dazu muss man Hiera anweisen, dass alle Hierarchie Ebenen ausgelesen werden.
Dies erreicht man mit dem Key lookup_options
:
# data/common.yaml
---
lookup_options:
'classes_hash':
merge:
behavior: 'deep'
'stdlib::manage::create_resources':
merge:
behavior: 'deep'
Nun kann man z.B. Node spezifisch Daten überschreiben:
# data/nodes/timeserver.yaml
---
stdlib::manage::create_resources:
file:
'/etc/ntp.conf':
source: 'puppet:///modules/profile/time/ntp-timeserver.conf'
Siehe dazu auch die Dokumentation der Merge Strategien. Diese müssen gut verstanden und nur mit Bedacht gesetzt werden!
Achtung!
YAML hat einige Besonderheiten, die man berücksichtigen muss.
Generell kann man sagen, dass Strings immer mit Anführungszeichen geschrieben werden sollen.
Wenn man dies nicht macht, sollte man wissen, was dies bedeuten kann.
Anbei einige Beispiele, an denen man die Probleme erkennen kann:
Sexagesimale Zahlen
Sexagesimale Zahlen sind auf Basis der Zahlen von 0 bs 59 und wurden mit YAML 1.1 eingeführt und in YAML 1.2 wieder entfernt. Je nachdem welche Implementierung der Parser nutzt, bekommt man unterschiedliche Ergebnisse:
port_map:
- 22:22
- 443:443
Mit YAML 1.1 erhält man folgenden Wert:
{ "port_map": [1342, "443:443"]}
Anchors, aliases und tags
Innerhalb von YAML existieren bestimte Zeichen, die das Verhalten von YAML beinflussen.
Anchors und Aliases haben wir bereits besprochen. Ein Anchor beginnt mit einem &
, ein Alias beginnt mit *
.
Wenn man nun einen String ohne Anführungszeichen setzt, der mit einem *
anfängt, dann sucht YAML den dazu gehörenden Anchor. Da dieser nicht gesetzt ist, wird bei YAML safe_load ein Fehler erzeugt.
Beispiel:
# blog_posts/yaml_demo.yaml
web_files:
- /robots.txt
- *.html
Jetzt laden wir die Datei:
# irb
require 'yaml'
YAML.load_file('blog_posts/yaml_demo.yaml')
Traceback (most recent call last):
11: from C:/Program Files/Puppet Labs/Bolt/bin/irb.bat:31:in `<main>'
10: from C:/Program Files/Puppet Labs/Bolt/bin/irb.bat:31:in `load'
9: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
8: from (irb):2
7: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/2.7.0/psych.rb:577:in `load_file'
6: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/2.7.0/psych.rb:577:in `open'
5: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/2.7.0/psych.rb:578:in `block in load_file'
4: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/2.7.0/psych.rb:277:in `load'
3: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/2.7.0/psych.rb:390:in `parse'
2: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/2.7.0/psych.rb:456:in `parse_stream'
1: from C:/Program Files/Puppet Labs/Bolt/lib/ruby/2.7.0/psych.rb:456:in `parse'
Psych::SyntaxError ((blog_posts/yaml_demo.yaml): did not find expected alphabetic or numeric character while scanning an alias at line 4 column 5)
Tags in YAML können genutzt werden, um komplexere Daten Typen zu parsen. Das größte Problem ist, dass hier beliebiger Code eingeschleust werden kann.
Das kleinere (aber auch wichtige) Problem: Wenn der YAML Parser kein Tag findet, dann wird die Tag Reference durch NIL
ersetzt.
Beispiel:
# blog_posts/yaml_demo.yaml
web_files:
- /robots.txt
- !local.html
Jetzt laden wir die Datei:
# irb
require 'yaml'
YAML.load_file('blog_posts/yaml_demo.yaml')
=> {"web_files"=>["/robots.txt", nil]}
Das Norwegen Problem
In YAML werden bestimmte Strings ohne Anführungszeichen als Boolsche Werte angesehen.
Die folgenden Werte werden als False evaluiert:
- off
- no
Groß- und Kleinschreibungen sind ebenfalls möglich.
Die folgenden Werte werden zu True evaluiert:
- on
- yes
Das Problem wurde mit YAML 1.2 gelöst, aber viele Parser nutzen immer noch YAML 1.1
Beispiel:
---
bool_strings:
- no
- off
- yes
- on
Auslesen der YAML Datei:
irb
require 'yaml'
YAML.load_file('blog_posts/yaml_demo.yaml')
=> {"bool_strings"=>[false, false, true, true]}
Man muss wissen, dass diese Bool Konvertierung auch bei Hash Keys vorgenommen wird!
Originaler Post: https://ruudvanasseldonk.com/2023/01/11/the-yaml-document-from-hell
Zusammenfassung
Das Konzept von Puppet und Hiera erlaubt es, eine Infrastruktur mit 7 Zeilen Puppet Code zu verwalten.
Alle Einstellungen sind YAML Daten.
Egal ob man fertige Module verwendet oder die Klasse stdlib::manage
nutzen möchte: es geht immer nur um YAML Daten.
Dieses Konzept wird die meisten notwendigen Konfigurationen abdecken. Wenn man mehr Flexibilität benötigt, hat man immer noch die Möglichkeit eigenen Puppet Code zu schreiben.
In Puppet Code kann man Methodiken nutzen, die in YAML nicht möglich sind:
- Code Logik (if, unless, case)
- Daten Type Validierung (Integer, Boolean, String)
- Komplexeres Setup
- Eigene Resource Types und Provider, Daten Typen
betadots GmbH wünscht allen Erfolg und Spass bei der Umsetzung von Puppet Configuration Management mit Hilfe von YAML Daten.
Posted on February 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.