In diesem Beitrag lege ich dar, wie ich mein Puppet-Setup strukturiere. Ich orientiere mich dabei am Pattern der Profiles und Roles, habe es aber auf meine Bedürfnisse hin angepasst. Zum Einsatz kommt natürlich Git und es werden mehrere Repos gepflegt, die alle mit Hilfe von r10k dynamisch auf den Puppet Server gebracht werden, unter Beachtung von Environments etc.

Bisherige Beiträge in der Reihe:

Struktur

Wir fangen in der Praxis an und kommen dann zur Theorie anhand der Beispiele:

Ich habe ein Git-Repo puppet, in dem liegen, neben einem deploy-Script und einem Gemfile zwei weitere Git-Repositories als Submodule:

puppet
├── bootstrap
│   ├── bootstrap.pp
│   ├── common.sh
│   ├── deploykey
│   ├── deploykey.pub
│   ├── deploy_puppet.sh
│   └── puppetmaster.sh
├── environments
│   ├── data
│   ├── hiera.yaml
│   ├── manifests
│   └── Puppetfile
├── Gemfile
├── Gemfile.lock
├── pdeploy
└── profiles
    ├── data
    ├── files
    ├── hiera.yaml
    ├── manifests
    └── templates

Bootstrap

Hier pflege ich einige kleine Scripte und ein Puppet-Manifest, mit deren Hilfe ich eine neue VM oder einen neuen Container für die Verwendung von Puppet vorbereite.

Der Inhalt ist nicht weiter interessant, es wird im Grunde nur das zum jeweiligen Debian-Release (grep CODENAME /etc/lsb-release|cut -d'=' -f2) passende Paket von apt.puppetlabs.com heruntergeladen, installiert und nach einem generellen Systemupdate der Puppet Agent installiert.

Die Installation des Puppet Servers ist schon interessanter: zuerst passiert das gleiche wie auf einem Client, es wird aber auch r10k installiert, ein SSH-Schlüssel deploykey (mit lesendem Zugriff auf die Git-Repos) übertragen und mit r10k gleich das erste Mal das Puppet-Setup auf den Puppet Server heruntergeladen. Wir sehen später, wie das geht.

Profiles

In diesem Verzeichnis liegen Profile. Im Grunde handelt es sich dabei um ein Modul, in dem mehrere Manifeste liegen, eins pro Profil. Standardeinstellungen dafür werden in data/ mit Hiera hinterlegt, was natürlich mit der hiera.yaml konfiguriert werden kann.

Ein Profil beinhaltet, wie jedes gute Manifest, eine class. Diese wird als Wrapper für eine bestimmte Server-Funktion betrachtet. Im Beitrag zum Einrichten eines Mail-Relay für die VMs etwa kann man das Profil mailrelay betrachten: Die für die Funktion des Mail-Relay notwendigen Konfigurationsdaten werden ermittelt, die notwendigen Klassen aus anderen Modulen werden instanziert und mit den ermittelten Einstellungen konfiguriert. Dabei ist zu beachten, dass hier nicht etwa Postfix installiert oder seine Konfigurationsdateien in Templates zerlegt wurden, die dann befüllt werden! Das macht alles das Postfix-Modul, das ist eine andere Ebene der Implementierung. Ein Profil kümmert sich nicht darum, wie etwas installiert wird - man legt nur fest, was installiert wird und was daran konfiguriert wird, etwa anhand von Parametern und Defined Types. Das fügt der Sache eine Abstraktionsebene hinzu: Einer VM weist man also nicht zu, dass dort Postfix mit Konfigurationdatei /etc/postfix/main.cf mit Inhalt xzy installiert werden soll, sondern dass sie das Profil eines mailrelay erfüllen soll. Um die Umsetzung kümmert sich dann das Profil. Das Profil wiederum installiert auch nicht direkt das Paket postfix und schreibt in seine Konfigurationsdateien; vielmehr wird dort eine Klasse postfix instanziert, die das alles macht und dieser Klasse wird nur mitgeteilt, welche Konfigurationseinstellungen man wünscht. Die Klasse postfix aus dem gleichnamigen Modul setzt dann schlussendlich genau das um.

Wäre für den Betrieb des mailrelay etwa noch ein MySQL-Server notwendig, könnte man im Profil mailrelay auch dafür eine Klasse aus einem Modul instanzieren und zum Beispiel Zugangsdaten dafür über eine gemeinsam verwendete Variable an die Module postfix und mysql übergeben, die sich dann um die jeweils für sie relevanten Details der Implementierung kümmern.

Unverkennbarer Vorteil dieser Systematik ist natürlich die Wiederverwendbarkeit der verschiedenen Schichten. Das Modul letsencrypt (aus dem Beitrag zur Einrichtung von SSL etwa kann sowohl im Profil sslmaster als auch im Profil ssl (für Clients) verwendet werden, mit unterschiedlichen Setups. Genauso können Profile unterschiedlich konfiguriert und auf verschiedenen Hosts daher mit verschiedenen Einstellungen genutzt werden.

Environments

Die Struktur dieses Repositories ist etwas komplexer. Jedes in meinem Puppet-Setup vorhandene Environment wird dort in einem eigenen Branch gepflegt. Nachdem es bei Puppet normalerweise kein Environment master gibt sondern production, habe ich das entsprechend auch im Repository so abgebildet.

Es gibt eine Puppetfile, in die ich alle verwendeten Module eintrage:

mod 'profiles',
  :git    => '[email protected]:puppet/profiles.git',
  :branch => 'master'

mod 'puppetlabs-firewall', '1.9.0'
mod 'puppetlabs-postgresql', '5.1.0'
# [...]

Man sieht hier zwei Dinge: Ich lasse mir das profiles-Repository von meinem Git-Server holen. Dazu muss auf dem Puppet Server ein SSH-Key liegen, der zumindest lesenden Zugriff auf das Repo hat. Deswegen vorhin der deploykey. Zweitens rufe ich verschiedene Module anhand ihres Namens und (optional) eine Version aus der Puppet Forge ab. r10k kann damit umgehen und holt mir den Code automatisch von dort. Ich muss also nirgends in meinem Setup Check-Outs der verwendeten 3rd-Party-Module pflegen. Das ist ein Vorteil; zu meinem Nachteil kann ich so aber keine lokalen Forks pflegen, bei denen etwa kleine Änderungen am Code gemacht wurden.

Weiterhin gibt es hier meine zentrale hiera.yaml, welche die Konfiguration der Nodes und Dienste bereitstellt. Wie man so eine Datei aufbauen kann, sieht man beispielhaft im Beitrag zu verschlüsselten Einträgen in Hiera mit eyaml. Dort wird auch die ganze Konfiguration der Profile vorgenommen. Beispielsweise wird profiles::mailrelay::relayhost auf die Adresse meines SMTP-Relayhosts im Internet gesetzt. Erst jetzt! Von hier geht diese Konfiguration an das Profil mailrelay, das wiederum schickt es weiter an die Klasse postfix und die trägt es schlussendlich an die richtigen Stellen ein. Wie gesagt: Das steigert die Wiederverwenbarkeit des Codes.

Die Zuweisung von Profilen an VMs oder Container findet hier auch statt, nicht nur die Fein-Konfiguration der Profile. Ein Server kann dabei mehrere Profile zugewiesen bekommen:

# Datei environments/data/nodes/puppet.domain.tld
classes:
  - puppetdb
  - puppetdb::master::config
  - profiles::sslmaster
  - profiles::mail

Am Beispiel meines Puppet Servers sieht man, dass man Profile einfach durch neue Elemente im Array classes zuweisen kann. Ich habe außerdem noch ein Profil baseline, in dem ich allgemeine Dinge wie Generierung von locales, Installation von vim, etc machen lasse, die auf allen Systemen angewandt wird. Diese Klasse weise ich dann einfach in der common.yaml zu. Hier greifen alle normalen Mechaniken von Hiera. Damit das dann klappt, braucht man noch ein kurzes Manifest:

# Datei environments/manifests/site.pp
lookup('classes', Array[String], 'unique').include

Hier werden alle Arrays mit Namen classes zusammengetragen und mit include geladen. Nachdem das Manifest auf jedem Zielhost einzeln realisiert wird, werden natürlich nur diejenigen Klassen geladen, die auch auf den Host gehören.

r10k

Damit r10k funktioniert, muss es initial aufgesetzt werden. Das ist bei mir Teil des deploy_puppet.sh-Scripts und des dazugehörigen Puppet-Manifests und erstreckt sich im Grunde auf die drei Ressourcen im Manifest:

sshkey { 'git.domain.tld':
  ensure => present,
  type   => 'ssh-ed25519',
  key    => 'AB...XYZ',
}

file { 'deploykey':
  ensure => present,
  path   => '/root/.ssh/id_rsa',
  source => '/tmp/deploykey',
  owner  => 'root',
  group  => 'root',
  mode   => '0600',
}

class { 'r10k':
  remote   => '[email protected]:puppet/environments.git',
  provider => 'puppet_gem',
}

und vier Zeilen im Script:

scp deploykey puppet.domain.tld:/tmp
scp bootstrap.pp puppet.domain.tld:/tmp
ssh puppet.domain.tld 'sudo /opt/puppetlabs/bin/puppet module install puppet/r10k'
ssh puppet.domain.tld 'sudo /opt/puppetlabs/bin/puppet apply /tmp/bootstrap.pp'

Workflow

Hat man dieses Setup erfolgreich aufgesetzt, kann man Änderungne an Profilen oder an der Konfiguration in den dazugehörigen Repositories auf der Workstation vornehmen und bei Bedarf an den Git-Server schicken. Ist der Push angekommen, muss noch ein Pull auf dem Puppet Server stattfinden, damit der Code auch dort ankommt. Dazu genügt:

sudo r10k deploy environment -pv

Damit wird das vorgegebene Repository environments ausgecheckt und die darin liegende Puppetfile gelesen. Die dort definierten Referenzen auf externe Repos (darunter profiles!) werden auch umgesetzt und der Code wird in das Verzeichnis /etc/puppetlabs/code/environments/<namedesenvironments> ausgecheckt. Für das Environment production ist also alles schon mal im Gange. Wie machen wir jetzt andere Environments?

Git branches als environments

Um ein neues Environment anzulegen, schafft man einfach einen neuen Branch des Repositories environments:

git branch sslsetup
git checkout sslsetup
# jetzt Änderungen vornehmen, dann:
git commit
git push origin sslsetup

Wenn der Branch auch remote auf dem Git-Server besteht, wird r10k ihn ebenfalls auschecken, nämlich nach /etc/puppetlabs/code/environments/sslsetup

  • ebenfalls mit den Inhalten seiner eigenen Puppetfile und was dazugehört. So können also auch unterschiedliche Versionsnummern von Modulen je nach Branch verwendet werden.

Wo sind jetzt hier die Rollen aus dem Pattern?

Um ehrlich zu sein habe ich in meinem Setup bisher noch keine Rollen verwendet. Das liegt zum einen daran, dass es derzeit noch organisch wächst, nachdem mein virtualisiertes Netzwerk zuallererst eine Lern- und Testlabor ist und zum anderen bisher einfach noch keine Installationen gemacht wurden, bei denen mehr als ein Server von einem bestimmten Typ (Rolle) gebraucht wird. Ich habe also etwa noch nicht zwölf gleiche Hosts, die alle die gleiche Kombination aus Profilen tragen. Die Zuordnung Nodes zu Rolle wäre also 1:1, da kann ich sie mir auch sparen. Bei späterem Bedarf wird hier dann eben refaktoriert.