DEV Community

Cover image for Puppet ist YAML
Martin Alfke for betadots

Posted on

Puppet ist YAML

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:

  1. eigener Code
  2. 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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  }
Enter fullscreen mode Exit fullscreen mode
# data/common.yaml
---
lookup_options:
  'classes_hash':
    merge:
      behavior: 'deep'

classes_hash:
  'Beschreibung der Klasse A': 'class_a'
  'Beschreibung der Klasse B': 'class_b'
Enter fullscreen mode Exit fullscreen mode

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': ''
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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]'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Mit YAML 1.1 erhält man folgenden Wert:

{ "port_map": [1342, "443:443"]}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Jetzt laden wir die Datei:

# irb
require 'yaml'
YAML.load_file('blog_posts/yaml_demo.yaml')
=> {"web_files"=>["/robots.txt", nil]}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Auslesen der YAML Datei:

irb
require 'yaml'
YAML.load_file('blog_posts/yaml_demo.yaml')
=> {"bool_strings"=>[false, false, true, true]}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)