Puppet ist YAML
Tags: puppet, yaml, automation, learning
Published: 2023-02-08 08:31:54

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
1. 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.

- [Puppet ist YAML](#puppet-ist-yaml)
  - [Node Klassifizierung](#node-klassifizierung)
  - [Nutzung von Puppet Modulen (Bibliotheken)](#nutzung-von-puppet-modulen-bibliotheken)
  - [Einfache Installation, Konfiguration und Services](#einfache-installation-konfiguration-und-services)
  - [Defaults setzen und überschreiben, hinzufügen oder entfernen von Parametern](#defaults-setzen-und-überschreiben-hinzufügen-oder-entfernen-von-parametern)
  - [Achtung!](#achtung)
  - [Zusammenfassung](#zusammenfassung)

## Node Klassifizierung<a name="node-klassifizierung"></a>

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:

```puppet
    # 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:

```yaml
# 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:

```yaml
# 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](https://forge.puppet.com/modules/ipcrm/echo/readme) Resource Type:

Der Puppet Code muss angepasst werden:

```puppet
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  }
```

```yaml
# 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:

```yaml
# data/nodes/different_server.domain.tld.yaml
---
classes_hash:
  'Beschreibung der Klasse A': ''
```

## Nutzung von Puppet Modulen (Bibliotheken)<a name="nutzung-von-puppet-modulen-bibliotheken"></a>

Für viele Anwendungen findet man fertige Puppet Module auf der [Puppet Forge](https://forge.puppet.com).
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:

```yaml
# 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<a name="einfache-installation-konfiguration-und-services"></a>

Das [stdlib Modul](https://forge.puppet.com/modules/puppetlabs/stdlib) 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:

```yaml
---
# 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](https://puppet.com/docs/puppet/latest/type.html):

- 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](https://forge.puppet.com/modules/puppetlabs/postgresql) 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:

```yaml
---
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:

```yaml
# 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<a name="defaults-setzen-und-überschreiben-hinzufügen-oder-entfernen-von-parametern"></a>

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:

```yaml
file_defaults: &file_defaults
  owner: 'root'
  group: 'root'
  mode: '0644'
```

Inner halb der **gleichen** YAML Datei kann man den Anchor mit einem Alias referenzieren:

```yaml
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`:

```yaml
# 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:

```yaml
# 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](https://puppet.com/docs/puppet/7/hiera_merging.html). Diese müssen gut verstanden und nur mit Bedacht gesetzt werden!

## Achtung!<a name="achtung"></a>

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:

```yaml
port_map:
  - 22:22
  - 443:443
```

Mit YAML 1.1 erhält man folgenden Wert:

```json
{ "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:
```yaml
# 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:
```yaml
# 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:

```yaml
---
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<a name="zusammenfassung"></a>

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.