Moderne Puppet Node Klassifizierung
Tags: puppet, tutorial, cfgmgmt, automation
Published: 2022-08-08 06:34:53

Innerhalb von Puppet werden mit Hilfe von Modulen bestimmte technische Komponenten konfiguriert.
Module können Upstream entwickelt und auf der [Puppet Forge](https://forge.puppet.com) zur Verfügung gestellt werden. Diese Module nennen wir gerne Bibliotheken oder Komponenten Module.
Alternativ handelt es sich um selbst geschriebene Technische Implementierungs-Profile.

Da es sich bei Puppet um ein Client-Server Modell handelt, muss der Server wissen, welche Module auf einem System zum Einsatz kommen sollen. Diesen Vorgang nennt man Node Klassifizierung.

Innerhalb von Puppet stehen unterschiedliche Varianten für die Node Klassifizierung zur Verfügung.
Dieser Artikel beschreibt die klassische Node Klassifizierung und ihre Einschränkungen und geht danach auf flexiblere, Hiera daten-basierte Node Klasifizierung ein.

1. [Klassische Node Klassifizierung](#classical)
  1. [Node Resource Typ](#node_resource)
  2. [Rollen Klassifizierung](#role)
  3. [External Node Klassifizierung](#enc)
1. [Hiera basierte Klassifizierung](#hiera)
  1. [Hiera Array](#hiera_array)
  2. [Hiera Hash](#hiera_hash)
  2. [Hiera Multiple Hashes](#hiera_multiple_hashes)
1. [Zusammenfassung](#conclusion)

## Klassische Node Klassifizierung<a name="classical"></a>

Der Puppet Server muss wissen, wo die Node Klassifizierungen zu finden sind. Dafür liest der Puppet Server zuerst die Konfigurationseinstellung `manifest`. Hier erwartet der Puppet Server einen Pfad und innerhalb des Pfades die Datei `site.pp`.
Der Standardwert lautet: `/etc/puppetlabs/code/environments/production/manifests`

Innerhalb der `site.pp` Datei (und weiteren Dateien und Unter-Verzeichnissen) kann man Puppet Code für Node Klassifizierung hinterlegen.

### Node Resource Typ<a name="node_resource"></a>

Die einfachste Lösung ist die Verwendung des `node` Resource Typen. Die Node Resource verwendet den Distinguished oder Common Name des Puppet Agent Zertifikates zur Identifikation. Dieser basiert normalerweise auf dem FQDN des Systems.
Ausserdem sind reguläre Ausdrücke möglich.

    # manifests/site.pp
    ...
    node 'app1web03-dev.domain.tld' {
      include profile::base
      include profile::accounts::dev
      include profile::webserver::nginx
      include profile::application::app01
    }
    ...

Hinweis: Der Puppet Server **muß** ein Node Klassifizierungs Objekt erhalten. Daher kann man einen leeren 'default' Fallback Node hinterlegen:

    node default {}

Die Node Resource ist eine einfache Lösung und eignet sich für kleinere Serverlandschaften mit wenigen Nodes.
Bei größeren Umgebungen wird dieser Ansatz sehr zeitintensiv - auch wenn man die Möglichkeit nutzt, Servergruppen in einzelne Dateien und Verzeichnisse auszulagern.

### Rollen Klassifizierung<a name="role"></a>

In der Datei `site.pp` kann beliebiger Puppet Code hinterlegt werden. Hier kann man z.B. auch auf Facts oder Hiera Daten zugreifen.
Wenn alle Systeme einen Facts Namen `role` hinterlegt bekommen haben, kann man diesen Fact verwenden, um Rollen Klassen einzubinden:

    # manifests/site.pp
    include "role::${facts['role']}"

Das Rollen Prinzip macht Sinn, wenn man eine Vielzahl gleicher Systeme hat, die identisch konfiguriert werden müssen.
Dies bedeutet aber auch, dass jeder Server eine Rolle bekommen **muss**.
Bei vielen unterschiedlichen Server Rollen ist auch hier mit einem Mehraufwand zu rechnen.

### External Node Klassifizierung <a name="enc"></a>

Mit Puppet ist man in der Lage beliebige Tools für die Node Klassifizierung zu verwenden. Diese Tools werden aus Puppet Sicht 'External Node Classifiers (ENC)' genant. Diese Option wird von [Puppet Enterprise](https://puppet.com/products/puppet-enterprise/) und [Foreman](https://www.theforeman.org/) genutzt.

**Achtung**: Man muss beachten, dass die Node Klassifizierungen in einem ENC eine zusätzliche Option ist und kein Ersatz!
Wenn ein Node eine Klassifizierung sowohl in manifests wie auch im ENC hat, werden die Klassifizierungen beider Verfahren genutzt.

Damit Puppet einen ENC verwenden kann, müssen Konfigurationseinstellungen vorgenommen werden:

    # /etc/puppetlabs/puppet/puppet.conf
    [master]
      node_terminus = exec
      external_nodes = /usr/local/bin/enc

In diesem Fall führt der Puppet Server das Kommando aus, welches unter `external_nodes` angegeben ist und übergibt den Common Name des Agent Zertifikates als Argument.
Das Kommando wird durch den Puppet Server user ausgeführt, kann in einer beliebigen Programmiersprache erstellt worden sein und unterschiedliche Quellen referenzieren (Anfrage an eine Web API, eine Datenbank, Inhalte von Dateien, ...) und muss YAML Ausgabe produzieren und :

    ---
    environment: production
    classes:
      profile::base:
        time_servers: ['time.domain.tld']
      profile::accounts::dev: {}
      profile::webserver::nginx: {}
      profile::application::app01: {}

Innerhalb des `classes` Keys erwartet der Puppet Server ein Array oder einen Hash von Klassen, welche für den Node eingebunden werden sollen.
Wenn Hashes verwendet werden, kann man bei jedem Element einen Sub-Hash von Klassenparametern mit Werten setzen.
Im optionalen Key `environment` kann man das Node Environment festlegen, welches für den Node verwendet werden muss.

Im folgenden ein Shell Script, welches Dateien in einem Verzeichnis ausliest:

    # /usr/local/bin/enc
    #!/bin/bash
    if [ -e /etc/enc/nodes/$1.yaml ]; then
      cat /etc/enc/nodes/$1.yaml
    else
      cat /etc/enc/default.yaml
    fi

Die Puppet ENC Klassifizierung macht Sinn, wenn man die Möglichkeit schaffen möchte, Node Klassifizierungen extern von Puppet zu verwalten.

Hier muss man darauf achten, dass der Klassifizierungsprozess durch die Freiheiten, die eine Programmiersprache bietet nicht zu komplex und schlecht wartbar wird.
Weiterhin sollte das Ergebnis schnell vorhanden sein. In größeren Landschaften sollte man ausserdem die Last und Performance prüfen. Wenn andere Systeme befragt werden, sollten diese Hochverfügbar und Hochperformant sein.

## Hiera basierte Klassifizierung<a name="hiera"></a>

Hiera ist das in Puppet eingebaute Daten Backend, in dem üblicherweise Klassen Parameter hinterlegt werden. So kann man darstellen, dass Server im Rechenzentrum A andere DNS Server verwenden sollen, als die Server in Rechenzentrum B.

In der Hiera Konfigurationsdatei kann man verschiedene Ebenen hinterlegen. Üblicherweise empfehlen wir den folgenden Ansatz:

- Node spezifische Daten
- Applikations- und Stage-Daten
- Lokation oder Netwerk-spezifische Daten
- OS spezifische Daten (nur falls notwendig)
- Allgemeine oder globale Daten

Weitere Informationen über Hiera findet man auf der [Puppet Hiera Webseite](https://puppet.com/docs/puppet/7/hiera_intro.html).

Innerhalb von Hiera können beliebige Daten hinterlegt werden.
Dies erlaubt es uns, Hiera auch für die Node Klassifizierung zu verwenden, indem wir einen bestimmten Key mit Hilfe der [Puppet lookup](https://puppet.com/docs/puppet/6/function.html#lookup) Funktion abfragen.
Bei der lookup Funktion können Default Werte angegeben werden:

- Name des Keys
- Daten Typ des Keys
- Merge Verhalten

### Hiera Array <a name="hiera_array"></a>

Die Klassifizierungen sollten über die verschiedenen Hiera Ebenen hinterlegt werden. (application1-dev, net-dmz, os-version, ...).
Normalerweise liefert Hiera den Wert des ersten Vorfinden eines Key zurück. Dieses Verhalten kann mit der Angabe des Merge Verhaltens überschrieben werden:

    lookup( {
      'name'          => 'classes',
      'value_type'    => Array,
      'default_value' => [],
      'merge'         => {
        'strategy' => 'unique',
      },
    } ).each | $c | {
      # Hinweis: Die Variable `$class` kann nicht verwendet werden,
      # weil es sich hierbei um ein reserviertes Wort handelt!
      include $c
    }

Innerhalb der Hiera Ebenen kann man den 'classes' Key hinterlegen:

    # common.yaml
    ---
    classes:
      - profile::base

    # os/CentOS.yaml
    ---
    classes:
      - profile::base::centos

    # stage/dev.yaml
    ---
    classes:
      - profile::accounts::dev

    # application/app01.yaml
    ---
    classes:
      - profile::webserver::nginx
      - profile::application::app01

Weiterhin kann das Merge Verhaltens auch innerhalb von Hiera gesetzt werden.

Das Merge Verhalten kann man in der common.yaml angeben:

    # common.yaml
    ---
    lookup_options:
      'classes':
        merge:
          strategy: 'unique'

Danach kann man den merge Key aus der lookup Funktion entfernen:

    # manifests/site.pp
    lookup( {
      'name'          => 'classes',
      'value_type'    => Array,
      'default_value' => []
    }).each | $c | {
      include $c
    }

Dadurch ist man nun in der Lage für ein System, welches komplett anders ist, ein anderes Merge Verhalten anzugeben.
z.B. sollen auf einem System zwar User angelegt werden, aber ohne die User aus der Common Ebene.

In diesem Fall setzt man Node spezifisch ein anderes Merge Verhalten:

    # node/hr_server.domain.tld.yaml
    ---
    lookup_options:
      'classes':
        merge:
          strategy: 'first'

    classes:
      - profile::base::hr
      - profile::accounts::hr

Hier muss man wissen, dass man auch alle anderen Klassifizierungen aus anderen Ebenen an dieser Stelle mit angeben muss.

### Hiera Hash <a name="hiera_hash"></a>

Eine weitere Möglichkeit mit mehr Optionen ist die Verwendung von Hashes in Hiera. Damit bekommt man die Möglichkeit, Klassifizierungen individuell neu zu setzen oder sogar herauszunehmen.

Dafür müssen 2 Einstellungen vorgenommen werden:

common.yaml: Wechsel der Merge Strategie von 'unique' auf 'deep'

    ---
    lookup_options:
      'classes':
        merge:
          strategy: 'deep'

site.pp: Wechsel des Daten Typen von Array zu Hash und iterieren über Schlüssel (key) und Wert (c)

    lookup( { 'name'          => 'classes',
              'value_type'    => Hash,
              'default_value' => {}
    }).each | $key, $c | {
      if $c != '' {
        include $c
      }
    }

Ab sofort können in Hiera Hashes verwendet werden:

    # common.yaml
    ---
    classes:
      base_class: 'profile::base'

    # os yaml
    ---
    classes:
      security_class: 'profile::base::centos'

    # stage yaml
    ---
    classes:
      accounts_class: 'profile::accounts::dev'

    # app yaml
    classes:
      webserver_class: 'profile::webserver::nginx'
      application_class: 'profile::application::app01'

Der key 'classes' ist ein Hash, dessen keys eindeutige Identifikationen sein müssen und der Wert des Keys der einzubindenden Klasse entspricht.
Die Keys (Identifikatoren) werden nur innerhalb von Hiera, nicht aber innerhalb von Puppet verwendet.

Diese Key-Wert Angaben erlaubt es nun, Klassifizierungen auch zu überschreiben:

    # node yaml
    ---
    classes:
      webserver_class: 'profile::webserver::tomcat'

Auf diesem Server wird anstelle des Standard Webserver 'profile::webserver::nginx' die Klasse 'profile::webserver::tomcat' verwendet.

Ausserem kann man mit Hashes auch eine Klassifizierung aus einer unteren Ebene wieder herausnehmen:

    ---
    classes:
      application_class: ''

Dies überschreibt den 'application_class' Key und setzt statt dessen eine leere Klasse. Im Puppet Code lassen wir leere Klassen aus. Optional kann man hier auch eine notice Function nutzen, um das auslassen der Klasse im Puppet Server Log zu protokollieren.

### Hiera Multiple Hashes <a name="hiera_multiple_hashes"></a>

Eine weitere Möglichkeit bietet das Unterteilen der Keys nach bestimmten Kriterien, wie z.B. Common, Betriebssystem und Anwendungen.

Im folgenden ein Beispiel für die Nutzung von Hiera Hash Keys basierend auf dem Kernel Fact:

    # common.yaml
    ---
    lookup_options:
      /.*_classes/:
        merge:
          strategy: 'deep'

    # manifests/site.pp
    $kernel_down = $facts['kernel'].downcase    
    lookup( { 'name'          => "${kernel_down}_classes",
              'value_type'    => Hash,
              'default_value' => {}
    }).each | $key, $c | {
      if $c != '' {
        include $c
      }
    }

Jetzt kann man Keys nach OS-spezifika angeben:

    linux_classes:
      hostname: 'profile::linux::hostname'
      repo: 'profile::linux::repo'
      sudo: 'profile::linux::sudo'
      ssh: 'profile::linux::ssh'
      mail: 'postfix'
      webshop: 'profile::application::webshop::nginx'

    windows_classes:
      hostname: 'profile::windows::hostname'
      hosts: 'profile::windows::hosts'
      features: 'profile::windows::features'
      time: 'profile::windows::time'
      users: 'profile::windows::ad_auth'
      webserver: 'iis'

Weiterhin kann man mit diesem Prinzip auf Klassen Reihenfolgen eingehen:

    # manifests/site.pp
    lookup( { 'name'          => "pre_classes",
              'value_type'    => Hash,
              'default_value' => {}
    }).each | $key, $c | {
      if $c != '' {
        class { $c:
          tags => 'pre',
        }
      }
    }

    # manifests/site.pp
    $kernel_down = $facts['kernel'].downcase
    lookup( { 'name'          => "${kernel_down}_classes",
              'value_type'    => Hash,
              'default_value' => {}
    }).each | $key, $c | {
      if $c != '' {
        include $c
        Class<| tags == 'pre' |> -> Class[$c]
      }
    }

Zu guter Letzt kann man auf Custom, External oder Trusted Facts für Applikationen eingehen.

## Zusammenfassung<a name="conclusion"></a>

Je nach Infrastruktur und den Anforderungen sollte man sich die beste Variante aussuchen.

Eine komplexere Infrastruktur braucht einen eleganteren Ansatz zur Node Klassifizierung.

Durch die Limitierung des Rollen Modells empfehlen wir die Nutzung des Hiera basierten Node Klassifizierungsverfahren.

Bei einer großen Anzahl von Variationen von Anwendungen und Betriebssystemen sollte man den Hiera Hash Ansatz wählen, in einfacheren Umgebungen reicht der Hiera Array Ansatz aus.

Die Option multiple Hashes zu nutzen, kann auch sinnvoll sein, wenn unterschiedliche Teams unterschiedliche Daten liefern müssen.
Mit dem Hash basierten Ansatz kann man das Management von Basis-Einstellungen von Anwendungen separieren.

Happy puppetizing,
Martin Alfke