Icinga2 Cluster mit Puppet

Icinga2 und Puppet

In dieser Anleitung wird ein Vorschlag unterbreitet, wie ein Icinga2 Cluster in Kombination mit Icingaweb2 und Puppet installiert betrieben werden kann.

Es empfiehlt sich erst einmal alles zu überfliegen, um sich einen Überblick zu verschaffen. Eventuell ist auch nicht das ganze Paket interessant, sondern nur Auszüge daraus.
Aufgrund der Länge werde ich die Anleitung etwas aufteilen, wenn alles soweit enthalten ist.

Zwei Konzepte

Der Autor (ich) hat so einige Zeit damit zugebracht zu schauen, was wie am Besten klappt. Es gibt zwei grundlegende Methoden, die ich kurz anreißen werden.

Puppet Ressource Export

Bei dieser Methode ist das zentrale Bindeglied die PuppetDB.
Bei jedem Aufruf des Puppet Agents, werden alle Fakten (facts) eingesammelt und in die PuppetDB geschrieben. Wenn diese Daten einmal vorhanden sind, kann man sie natürlich auch wieder abrufen.
Bei einer Icinga2 Installation kann es so aussehen, dass bei einer neuen Node (VM / physisch / Container) automatisch ein neuer Icinga2 Host erstellt und überwacht wird. Dank der Apply Regeln würde man recht schnell zu einem nahezu vollautomatischen Monitoring System kommen; immer unter der Voraussetzung, dass ein Puppet Agent darauf läuft.
Wird eine Node aus dem System entfernt (weil nicht mehr benötigt), kann sie auch aus der Überwachung verschwinden. Dafür benötigt es allerdings ein wenig Feintuning.
Was auf dem ersten Blick sehr verlockend erscheint, offenbart bei näherer Betrachtung allerdings einige Nachteile:

Es soll ein neuer Server mit in die Überwachung aufgenommen werden. Dazu muss natürlich erst einmal Puppet und Icinga eingerichtet worden sein, sodass beim initialen Aufruf des Puppet Agents der Icinga2 Agent auf dem neuen Server eingerichtet und gestartet wird. Danach sollte der Puppet Agent ein weiteres Mal gestartet werden, sodass die Fakten in der Datenbank auch wirklich alle vorhanden sind, auch die, die mit dem Icinga2 Puppet Modul mitkommen.
Damit auch der Icinga2 Master darüber Bescheid weiß, dass eine neue Node zum überwachen eingetroffen ist, muss auf dem Icinga2 Master ebenfalls der Puppet Agent aufgerufen werden. Dies hat zur Folge, dass das Puppet Icinga2 Modul diese neue Node in der Datenbank findet, eine passende Icinga2 Host Konfiguration erstellt und anschließend den Icinga2 Master neustartet, bzw. ein Reload durchführt.
Im Kern bedeutet dies, dass der Puppet Agent zwei Mal aufgerufen werden muss:

  1. Auf dem neuen zu überwachenden Host -> Daten in PuppetDB schreiben
  2. Auf dem Icinga2 Master -> Um neue Node aus der PuppetDB zu holen

Je nach Komplexität der Puppet Konfiguration, kann so ein Puppet Aufruf schon einmal länger dauern, erst Recht, wenn wir es nicht nur mit einer ein- oder zweistelligen Anzahl von Hosts zu tun haben.

Kompliziert wird es auch dann, wenn wir es nicht nur mit einem (logischen) Ort zu tun haben. Stichwort: DMZ / VPN / Rechenzentren.

Das gesamte Konstrukt baut darauf, dass es nur eine PuppetDB gibt und alle auf die gleiche Datenbank zugreifen. Wenn es aber mehrere Puppet Installationen gibt, bricht das Konzept. An dieser Stelle würde man zum Datenbank Spezialisten gehen und Konzepte entwickeln, um die fehlenden Informationen zu importieren.
Für kleinere und übersichtliche Installationen ist das Konzept großartig, für alles andere eher weniger.

Wer sich dafür interessiert, findet hier die nötigen Puppet Ideen.

Icingaweb2 + PuppetDB

Methode Zwei hat sich im Laufe der Zeit als wirklich sehr durchdachtes Konzept entpuppt. Die Aufgabe des Puppet Agent besteht nur noch aus zwei Aspekten:

  1. Icinga2 Agent installieren und einrichten (incl. Zertifikate)
  2. Fakten in die PuppetDB eintragen

Um nun eine weitere Icinga2 Hostkonfiguration zu erstellen, wird das PuppetDB Modul vom Icingaweb2 Director verwendet. Es greift auf die PuppetDB (Postgresql) zu, und geht wie folgt vor:

  1. Extrahieren der Nodes und Fakten aus der PostgreSQL und in die eigene Datenbank überführen
  2. Daten nach definierten Regeln verändern, löschen oder neue hinzufügen
  3. Neue Icinga2 Hostkonfiguration erstellen und über die API an den Icinga2 Master übergeben

Alle Schritte lassen sich automatisieren, sodass auch hier am Ende eine vollautomatische Überwachung stattfinden kann.
Der Große Unterschied zu dem vorhergehenden Konzept liegt darin, dass nur einmalige Aufrufe des Puppet Agents notwendig sind und danach nur noch bei Änderungen. Auf dem Icinga2 Master wird kein Aufruf benötigt, da die Konfiguration vom Icingaweb2 Director übernommen wird.
Des weiteren kann das PuppetDB Modul vom Icingaweb2 Director nicht nur eine PuppetDB abfragen, sondern beliebig viele. Die einzige Voraussetzung ist, dass der Datenbank Zugriff vom Icingaweb2 auf die jeweilige Postgresql Datenbank (TCP) gestattet ist.

Der Autor hat mit der ersten Methode angefangen und aufgrund der (Netzwerk / PuppetDB) Restriktionen nach und nach Methode Zwei implementiert, welche auch heute noch so genutzt und ausgebaut wird.

Zutaten

Wir werden Methode Zwei verwenden, da diese am flexibelsten ist. Für den Anfang begnügen wir uns mit dem “einfachsten” Setup, ohne DMZ und Satelliten. Dazu benötigen wir folgendes:

Beginnen wir mir dem Icinga2 Master. Um die Tipparbeit ein wenig in Grenzen zu halten, gelten folgende Konventionen:

Icinga2 Master

Vorbereitung

Apache2

Für Icingaweb2 verwenden wir den Apache2 Webserver. Damit wir auch Hiera verwenden können, gibt es einige Hilfs- Manifests.

Zuerst müssen wir noch das Puppet Modul installieren:

$ puppet module install puppetlabs-apache --modulepath production/modules/

Nicht vergessen Git Bescheid zu geben.

SSL

Der Zugriff sollte ausschließlich per TLS erfolgen, daher entweder Zertifikate selbst erzeugen (z.B. per xca) oder LetsEncrypt verwenden.
Für dieses Szenario verwende ich letzteres und habe die Test VMs per IPv6 an das Welt-Weite-Netz angebunden. Als Client verwende ich gerne dehydrated.

Damit wir bei Puppet kein Henne-Ei Problem bekommen, ist im Hiera das Snakeoil Zertifikat angegeben.

# apt install dehydrated dehydrated-apache2

Wir installieren auch das Paket für Apache, welches einfach nur eine Konfiguration mitbringt. Diese wird dann im Hiera Abschnitt eingebunden.

Hier muss natürlich auch ein CNAME oder A existieren, für office-ffm-master.

office-ffm-master-01.4lin.net office-ffm-master.4lin.net

Sobald der Apache2 installiert und eingerichtet wurde, kann mittels dehydrated das korrekte Zertifikat bezogen werden.

# dehydrated --register --accept-terms ; dehydrated -c

Anschließend müssen um Hiera die Pfade für das SSL Zertifikat getauscht werden.

MariaDB

An dieser Stelle habe ich bereits aufgezeigt, wie ein MariaDB Galera Cluster erstellt werden kann.
Ich gehe davon aus, dass ein funktionierendes MariaDB vorhanden ist und folgende User und leere(!) Datenbanken erstellt worden sind:

root@office-ffm-db-01:# mysql
MariaDB [(none)]> CREATE DATABASE icinga2_ido_db CHARACTER SET utf8mb4;
MariaDB [(none)]> CREATE DATABASE icingaweb2_db CHARACTER SET utf8mb4;
MariaDB [(none)]> CREATE DATABASE icingaweb2_director_db CHARACTER SET utf8mb4;

MariaDB [mysql]> GRANT SELECT INSERT UPDATE DELETE DROP CREATE VIEW CREATE INDEX EXECUTE ALTER REFERENCES ON icinga2_ido_db.* TO 'icinga2_ido_db'@'192.168.1.%' IDENTIFIED BY 'secret';

MariaDB [mysql]> GRANT SELECT INSERT UPDATE DELETE DROP CREATE VIEW CREATE INDEX EXECUTE ALTER REFERENCES ON icingaweb2_db.* TO 'icingaweb2_db'@'192.168.1.%' IDENTIFIED BY 'secret';

MariaDB [mysql]> GRANT SELECT INSERT UPDATE DELETE DROP CREATE VIEW CREATE INDEX EXECUTE ALTER REFERENCES ON icingaweb2_director_db.* TO 'icingaweb2_director_db'@'192.168.1.%' IDENTIFIED BY 'secret';

Man kann zwar auch von Puppet die Datenbank anlegen lassen, aber dafür müsste man noch das MySQL Puppet aufnehmen, was ich mir an dieser Stelle spare.

Icinga2 Puppet Modul

$ puppet module install icinga-icinga2 --modulepath environments/production/modules/
$ puppet module install icinga-icingaweb2 --modulepath environments/production/
$ puppet module install camptocamp-systemd --modulepath environments/production/modules/
$ git add environments/production/modules/
$ git commit -m "Add Icinga2 module and dependencies" environments/production/modules/

Manifests

Wir schmeißen im Grunde die Standardkonfiguration weg und erzeugen sie neu. Die Datenbank Daten entnehmen wir Hiera sowie diverse andere Parameter.

Master

Inhalt zeigen

Die Apply Regeln werden als Dateien gepflegt, da dies sehr viel einfacher und pflegeleichter ist, als sie zu exportieren.
Ein Auszug:

Inhalt zeigen

Die Dateien liegen in einem anderen Ordner und kann bei Bedarf natürlich geändert werden.
Alle “reinen” (also nicht Puppet Dateien) Icinga2 Dateien, Plugins, Scripte und Co. landen bei mir in modules/icinga2_checks/

$ mkdir -p environments/dev/modules/icinga2_files/files/{commands,plugins,plugins_agent,scripts,applyrules,templates}

Beispiel für service_check_apt.conf:

# Managed by Puppet
# modules/icinga2_checks/files/applyrules/service_check_apt.conf

apply Service "apt" {
  import "generic-service"
  check_command = "custom-apt"
    if (host.name != NodeName) {
        command_endpoint = host.name
    }
  assign where host.vars.distro == "Debian"
  enable_notifications = false
  ignore where host.vars.noagent
  if (host.vars.distro_name == "stretch") {
           vars.apt_list = true
     }
}

Beispiel für service_check_linux_base.conf:

Inhalt zeigen

Hier hinterlegen wir die Kommandos. Historisch bedingt tatsächlich als Puppet Objekte. Auch hier wäre die ÜBerlegung wert, sie als reine Icinga2 Dateien zu hinterlegen. Diese Datei ist sehr umfangreich und sollte auf jeden Fall entschlackt werden. Allerdings hilft es dem einen oder anderen anhand der Syntax zu entnehmen, wie ich etwas umgesetzt habe.

Inhalt zeigen

Dieses Manifest trägt eigentlich einen falschen Namen. Es sorgt eigentlich nur für die Umgebung und hinterlegt Passwörter oder dass benötigte Pakete auf dem System sind, um bestimmte Tools für die Benachrichtigung verwenden zu können.

# Notifications scripts and passwords
class profile::icinga2::notifications (
  $domain =  hiera('monitoring::domain'),
){

  $templates = '/etc/icinga2/zones.d/global-templates'
  
  file { '/etc/icinga2/scripts':
    ensure  =>  directory,
    mode    =>  '0755',
    owner   =>  'root',
    group   =>  'nagios',
    source  =>  [
      'puppet:///modules/icinga2_files/scripts',
    ],
    recurse =>  true,
  }
}

Hier werden nur die Templates hinterlegt, aber als reguläre Dateien:

# Mostly all templates for import
class profile::icinga2::templates {

  $global_templates = '/etc/icinga2/zones.d/global-templates'
  $templates = "${global_templates}/templates.d"

  file { "${global_templates}/templates.d":
    ensure => directory,
    owner  => 'nagios',
    group  => 'nagios',
    mode   => '0750',
    purge  => true,
    force  => true,
  }

  -> file { "${templates}/host-templates.conf":
    ensure =>  file,
    owner  =>  nagios,
    group  =>  nagios,
    tag    =>  'icinga2::config::file',
    source =>  [
      'puppet:///modules/icinga2_files/templates/host-templates.conf',
    ],
  }

  -> file { "${templates}/service-templates.conf":
    ensure =>  file,
    owner  =>  nagios,
    group  =>  nagios,
    tag    =>  'icinga2::config::file',
    source =>  [
      'puppet:///modules/icinga2_files/templates/service-templates.conf',
    ],
  }
}

Plugins

In den Foren wird oft gefragt, ob es nicht möglich wäre die Plugins per Icinga2 zu verteilen. Das geht natürlich nicht und würde den Code nur aufblasen, zumal es in sehr vielen Fällen ohnehin quatsch wäre. Bei vielen Plugins werden sehr oft weitere Pakete benötigt, die dann nach wie vor fehlen würden. Daher überlässt man das besser anderen.
Dieses Manifest unterscheidet im Grunde drei Typen:

  1. Icinga2 Agent oder Master
  2. Virtueller Host / Container
  3. Physischer Host

In meinem Fall bekommt der Master ein anderes Plugin Verzeichnis, als der Agent, weil zum Beispiel die IBM oder VMware Ordner auf den Clients nicht benötigt werden. Aber auch hier wäre noch sehr viel Luft zum optimieren. Da habe ich mir jetzt nicht soviel Mühe gegeben :-)

Inhalt zeigen
Sudo

Da wir an sehr vielen Stellen sudo benötigen, hinterlegen wir eine passende Konfiguration. Das Puppet saz-sudo Modul sorgt dafür, dass nur syntaktisch korrekte Sudo Dateien hinterlegt werden.

Agent

Beim Agent wurde am meisten gegrübelt, da die Anforderungen sich stetig änderten. Anfangs konnte der Master alle Agents direkt erreichen, dann kamen unterschiedliche Zonen sowie Satelliten hinzu. Am Ende bekam ein Satellit sogar einen weiteren Satelliten zugeordnet, der einen nochmaligen Umbau nach sich zog.

Satellite Aufbau

Inhalt zeigen

Icingaweb2

Icingaweb2 wird ebenfalls auf dem Master installiert, daher haben wir auch dafür ein passendes Manifest.

Inhalt zeigen

Hiera

Damit haben wir die wichtigsten Manifests zusammen. Gehen wir zum Hiera Teil über.
Ein erheblicher Anteil kann nun über Hiera definiert werden. Fangen wir beim Master an und gehen dann Monproxy und Agent.

Master

Es gibt drei Orte die zum Einsatz kommen:

  1. hieradata/common.eyaml
    • Für allgemeine Parameter die für Master und Agents gleichermaßen gültig sind
  2. hieradata/role/master.yaml
    • Parameter die nur die Master betreffen
  3. hieradata/node/office-ffm-master-01.4lin.net.eyaml
    • Parameter die nur speziell diese Node betreffen.

Fangen wir wir mit der hieradata/common.eyaml an. Alles mit secret muss natürlich durch eigene Kennwörter getauscht werden. Die monitoring Schlüssel sind sozusagen übergeordnet, da sie nicht nur bei Icinga2 zum Einsatz kommen, sondern auch noch für z.B. Grafana oder ähnliches. Ist aber reine Geschmackssache.

Inhalt zeigen

Die hieradata/role/master.yaml beinhaltet alles für die Master Nodes: * Icinga2 selbst - Zonen - Endpoints - Plugins * Apache2 für Icingaweb2 - PHP * Icingaweb2

Inhalt zeigen

Erster Puppetlauf

Hat man es bis zu diesem Punkt geschafft, kann man mittels puppet agent -t --noop einen ersten Testlauf starten und nach Syntaxfehlern Ausschau halten. Erfahrungsgemäß gibt es jede Menge davon. Des weiteren kann (und wird) es nötig sein, den Lauf mehrfach zu starten, da das ganze Konstrukt zu komplex ist, als dass es auf Anhieb durchläuft.

Ein Erfolgt ist es, wenn der Login auf Icingaweb2 bereits klappt, auch wenn noch keine Hosts sichtbar sind, da die Standard Konfiguration von Puppet entfernt wird. Stattdessen nutzen wir das Director Modul und Puppet um neue Hosts hinzuzufügen.

Director PuppetDB

Nach dem Puppet durchgelaufen ist, können wir den PuppetDB Host hinzufügen und auf die dort enthaltenen Ressourcen zugreifen.

PuppetDB Anbindung

Wurde die erste Synchronisierung durchgeführt, dürften wie in diesem Fall zwei Nodes bereits in der Vorschau auftauchen. Das allein hilft noch nicht, den wir müssen dem Modul noch sagen, wie es damit verfahren soll.

Director Vorarbeit

Bevor die ersten Hosts im Icinga2 auftauchen und überwacht werden können, sind noch einige Schritte zuvor notwendig:

  1. Erstellen von eigenen Datenfeldern
    • wir benötigen host.vars.os = Linux, damit die Apply Regeln greifen
    • wir benötigen host.vars.client_endpoint
    • werden über den Director später automatisch befüllt
  2. Host Templates erstellen
    • Generic Host (oder generic-host) -> Für ICMP oder Hostalive Hostcheck
    • Director Host -> Bindet Generic Host ein, zusätzlich wird aktive Agent Verbindung gesetzt (Endpoint / Zone )
    • Datenfeld “os” hinzufügen
  3. generic-service Template erstellen
    • es muss dieser Name sein, da in vielen Apply Regeln auf diesen Namen zurückgegriffen wird
    • Dient zum Setzen von Standardwerten

Datenfelder

Fangen wir mit dem Datenfeld an. Wir erstellen eine Datenliste mit unterschiedlichen Betriebssystemen. Das ist besser als einfach nur ein String, wegen unterschiedlicher Schreibweisen:

Director Datenliste

Die Liste selbst habe ich “Operation Systems” genannt. Als Schlüssel und Bezeichnung habe ich das Gleiche genommen.

Es folgt als nächstes das Datenfeld “os”, welches sich auf diese Liste bezieht:

Director Datenfeld os

Folgt als letzten das Feld host.vars.client_endpoint. Das sorgt in den Apply Regeln dafür, dass der Check auf wirklich auf dem gewünschten Host ausgeführt wird:

Director Datenfeld client_endpoint

Das waren für den Anfang die wichtigsten Felder.

Templates

Nun benötigen wir zwei Host Templates und ein Service Template. Das erste Host Template “Generic Host” (oder falls lieber “generic-host”) hat lediglich den Hostcheck gesetzt. In meinem Fall verwende ich lieber das Check Kommando “ICMP”, statt dem “Hostalive”.

Director Generic Host Template

Wurde das Template erstellt, muss auch das Feld “os” und “client_endpoint” hinzugefügt werden.

Director Generic Host Field

Das zweite Host Template “Director Host” sorgt dafür, dass beim Import durch den Director aus dem Puppet heraus, auch die notwendigen Zonen und Endpoints erstellt werden. Dafür muss lediglich unter den “Icinga Agenten- und Zoneneinstellungen Einstellungen” die Parameter dafür aktiviert werden:

Director Host Template

Das Service Template “generic-service” wird wie das Host Template erstellt, aber es bedarf da aktuell keine weiteren Parameter, für dieses HowTo. Einen Screenshot erspare ich mir an dieser Stelle.

Sync Regeln

Nun kommt der fummligste Abschnitt: das Erstellen der Synchronisationsregeln.

Grob gesagt geht es darum, dem Director zu sagen, welches Puppet Fact (ip / hostname / role / blockdevice …) zu welchem Feld im Icinga2 Host Object gehören soll.

Puppet Fact Icinga Host Object
fact.ip host.address
certname host.display_name
fact.fqdn host.vars.fqdn
fact.role host.vars.role

Jedes gewünschte Feld im Icinga Host Object muss zuvor auch im Director unter Datenfelder erstellt werden (meistens als String), damit es dann in der Regel zugeordnet werden kann.
Der nächste wichtige Punkt ist der, dass die neuen Datenfelder im Host Template (z.B Generic Host oder Director Host) hinzugefügt wurden.

Hier werde ich sechs Regeln exemplarisch aufzeigen.

Zum erstellen des Regelsets im Director müsst ihr wie im Screenshot dargestellt auf Icinga-Director gehen, dann Automatisierung, Syncronisationsregel. Dort wird ein neuer Regelsatz erstellt:

Director Role Set

Hier habe ich den Regelsatz “Master PuppetDB Sync” genannt und als Objekttyp “Host”, Aktualisierungsrichtlinie “Zusammenführen” und Bereinigen mit “Ja” angegeben.

Wurde der Satz erstellt, kommen die Regeln. Wählt den neu erstellten Regelsatz aus und dann direkt auf “Eigenschaften”. An dieser Stelle wird nun dem Director gesagt, was wir gerne wo hätten. Als Beispiel habe diese gewählt:

Director Datenfeld client_endpoint

Puppet Fact Icinga Host Object
icinga2_zone zone
certname display_name
facts.ipaddress address
certname object_name
facts.kernel vars.os
Director Host import
cername vars.client_endpoint

Die Möglichkeit einem Host Object Templates mit auf dem Weg zu geben, ist besonders hervorzuheben. In diesem Beispiel wird dem zu erstellendem Icinga Host Object das Template “Director Host” mitgegeben. In Kombination mit dem Filter kann ich sagen, dass der Host “office-ffm-srv-puppet” – welcher den Hostname “srv” hat – zum Beispiel ein Template “SRV Host” mitbekommt. Von dieser Funktion machen wir reichlich Gebrauch, um gleichartige Hosts zu erschlagen.
Ein fertiger Host vom Director sieht dann zum Beispiel so aus:

zones.d/master/hosts.conf

object Host "office-ffm-srv-puppet.4lin.net" {
    display_name = "office-ffm-srv-puppet.4lin.net"
    address = "192.168.1.33"
    check_command = "icmp"
    vars.client_endpoint = "office-ffm-srv-puppet.4lin.net"
    vars.os = "Linux"
}

zones.d/master/agent_endpoints.conf

object Endpoint "office-ffm-srv-puppet.4lin.net" {
    host = "192.168.1.33"
    log_duration = 0s
}

zones.d/master/agent_zones.conf

object Zone "office-ffm-srv-puppet.4lin.net" {
    parent = "master"
    endpoints = [ "office-ffm-srv-puppet.4lin.net" ]
}

Fügen wir noch das Fact “role” hinzu:

Director add role fact

Das Feld “role” habe ich natürlich vorher schon hinzugefügt. Damit können wir am Ende dann Apply Regeln erstellen, die nach host.vars.role == “foo” schauen :-)

Nehmen wir noch einen Import hinzu ! Dafür habe ich ein weiteres Template erstellt, das “DB Host” heißt und möchte, dass jedes Icinga Host Object dieses Template erhält, welches als certname=*db* enthält:

Director import DB host

Ist das auch geschafft, kann man im ersten Karteireiter den Sync anstoßen und sollte dann folgendes Ergebnis erhalten:

zones.d/master/hosts.conf

object Host "office-ffm-db-01.4lin.net" {
    import "Director Host"
    import "DB Host"

    display_name = "office-ffm-db-01.4lin.net"
    address = "192.168.1.30"
    vars.client_endpoint = "office-ffm-db-01.4lin.net"
    vars.os = "Linux"
    vars.role = "db"
}

zones.d/master/agent_endpoints.conf

object Endpoint "office-ffm-db-01.4lin.net" {
    host = "192.168.1.30"
    log_duration = 0s
}

zones.d/master/agent_zones.conf

object Zone "office-ffm-db-01.4lin.net" {
    parent = "master"
    endpoints = [ "office-ffm-db-01.4lin.net" ]
}

Und das ist das geniale, denn mit dieser Funktion könnte ihr nahezu alles in ein Host Objekt packen lassen. Wir übergeben damit sogar Festplattentypen und Mountpoints, um dann mit entsprechenden Apply Regeln darauf zu reagieren.

Danksagung

An dieser Stelle gilt mein Dank vor allem der Netways Truppe Ich muss zugeben, dass ich ein Teil meines Gehaltes wegen ihrer Arbeit erhalte :-) Da sind besonders zu nennen:

Dann wäre da noch Marianne M.Spiller, mit deren Hilfe ich überhaupt erst den Director kennen und nutzen gelernt habe.

Dann kämen natürlich noch die unzähligen Menschen von Puppet selbst und deren Module. Die wichtigsten für mich sind da:

Links