Icinga2 Puppet4
Inhaltsverzeichnis
- 1 Icinga2 und Puppet
- 2 Vim für Puppet
- 3 PuppetMaster
- 3.1 Puppet Server
- 3.2 PuppetDB
- 3.3 Hiera
- 3.4 Puppet Agent - Icinga Master
- 3.5 Rollen
- 3.6 Profile
- 3.7 Icinga-Master
Icinga2 und Puppet
Diese Anleitung wird einer meiner schlechtesten werden, soviel ist sicher. Es geht mehr darum Anderen das Gesuche zu ersparen und schneller zum Ergebnis zu kommen, insbesondere wenn man - wie ich - Programmierlegastheniker ist.
Ziel
Ziel ist es mittels Puppet(4) sowohl den Icinga2 Master als auch die Agents auszurollen. Hinzu kommt auch noch die Verwendung von Hiera - was eine sehr viel flexiblere Puppet Konfiguration ermöglicht - und der Zuhilfenahme von Profilen und Rollen. Gerade letzteres hat mich doch ganz schön Zeit gekostet, in Kombination mit Hiera.
Zutaten
Damit das alles am Ende klappt, benötigen wir:
- Mindestens drei Hosts
- Ein PuppetMaster Host
- Ein Icinga2 Master
- Ein Host für den Agent
- Genug Zeit
- Noch mehr Zeit
- Ein wenig Ahnung von Puppet
- Ein wenig Ahnung von Icinga2
- Immer noch Zeit
Klar, theoretisch würde es auch mit einem Host klappen, aber so peilt man es ein wenig besser. Für die Anleitung habe ich mir drei LXC Container (Danke ProxMox) erstellt und mittels Standard Debian 8.0 befüllen lassen. Danach kommen noch ein paar Standard Werkzeuge drauf, wie Screen und Co. Es empfiehlt sich außerdem ein Cluster SSH zu verwenden, da die ersten Schritte überall gleich sind. Für Linux gibt es "mssh", für OSX csshX und Windows .... bestimmt auch etwas.
Konzept
Das Konzept sieht so aus, dass jedem Host eine(!) Rolle (Webserver / Mailserver / Jabber ..) zugewiesen wird, welche aus mehreren Profilen bestehen kann. Ein Beispiel:
- role::webserver::nginx
- common -> Enthält Standardpakete (screen/htop/fail2ban/ntpd...) / Konfigurationen (NTP Server / screenrc ..)
- profile::webserver -> Kann z.B. für passende Firewallregeln sorgen ...
- profile::webserver::nginx -> Wir wollen den Nginx als Webserver
- profile::webserver::nginx::php -> Alles was für PHP5 nötig ist
Das ließe sich noch alles viel granulärer definieren, aber für den Anfang ...
Um auf das eigentliche Thema -- Icinga2 Master -- zurückzukommen, benötigen wir eine weitere Rolle:
- monitoring::monitor::master:
Dort werden alle Profile enthalten sein, die notwendig sind, um einen vollständigen Icinga2 Master zu haben. Für die zu überwachenden Hosts benötigen wir keine Rolle, da der Icinga2 Agent (aka Host) über ein passendes Profil eingebunden wird.
Dann fangen wir mal zuerst mit dem PuppetMaster an.
Vim für Puppet
Bevor es losgeht, noch schnell dafür sorgen, dass wir die Puppet Manifests und YAML Dateien vernünftig editieren können:
root@puppet-master ~ # mkdir -p /opt/puppetlabs/environments root@puppet-master ~ # chown puppet: /opt/puppetlabs/environments root@puppet-master ~ # usermod -s /bin/bash puppet root@puppet-master ~ # su - puppet puppet@puppet-master ~ $ mkdir /opt/puppetlabs/environments/.vim puppet@puppet-master ~ $ ln -s /opt/puppetlabs/environments/.vim puppet@puppet-master ~ $ mkdir -p ~/.vim/autoload ~/.vim/bundle; puppet@puppet-master ~ $ curl -L -Sso ~/.vim/autoload/pathogen.vim https://raw.github.com/tpope/vim-pathogen/master/autoload/pathogen.vim puppet@puppet-master ~ $ cat <<EOF > ~/.vim/vimrc call pathogen#infect() syntax on filetype plugin indent on EOF puppet@puppet-master ~ $ cd ~/.vim/bundle puppet@puppet-master ~/.vim/bundle $ git clone https://github.com/rodjek/vim-puppet.git puppet@puppet-master ~/.vim/bundle $ git clone https://github.com/godlygeek/tabular.git puppet@puppet-master ~/.vim/bundle $ git clone https://github.com/hjpbarcelos/vim-yaml |
Theoretisch könnte man auch einfach das $HOME von dem User "puppet" nach /opt/puppetlabs/environments/ ändern, aber das könnte unter Umständen andere Probleme nach sich ziehen, daher die Kombination mit Softlinks.
PuppetMaster
Ein wichtiger Grund weshalb wir gleich auf Puppet4 setzen ist der, dass sich insbesondere bei Hiera einiges getan hat, vor allem würde es Minimum ein Puppet 3.7 (glaube ich) benötigen, damit die essentiellen Dinge funktionieren.
Da bei Debian Jessie Puppet 3.7 enthalten ist, verwenden wir die Puppetlabs Pakete. Wir benötigen hierfür das PC1 Repository
root@puppet-master ~ # wget http://apt.puppetlabs.com/puppetlabs-release-pc1-jessie.deb; dpkg -i puppetlabs-release-pc1-jessie.deb && apt-get update |
Puppet Server
Für den Puppetserver benötigt es nicht sonderlich viel:
root@puppet-master ~ # aptitude install puppetserver |
Des weiteren muss DNS funktionieren, damit die IP Adressen jeweils auflösbar sind. Wer keinen DNS Server aufsetzen möchte oder kann, greift am Besten zu dnsmasq und pflegt die /etc/hosts.
Kommt nun die Basiskonfiguration, die alles notwendige enthält:
- /etc/puppetlabs/puppet/puppet.conf:
[master] vardir = /opt/puppetlabs/server/data/puppetserver logdir = /var/log/puppetlabs/puppetserver rundir = /var/run/puppetlabs/puppetserver pidfile = /var/run/puppetlabs/puppetserver/puppetserver.pid codedir = /etc/puppetlabs/code # storeconfigs = true # storeconfigs_backend = puppetdb hiera_config = /etc/puppetlabs/puppet/hiera.yaml environmentpath = /opt/puppetlabs/environments [main] logdir = /var/log/puppetlabs/puppet ssldir = /opt/puppetlabs/puppet/ssl rundir = /var/run/puppetlabs environmentpath = /opt/puppetlabs/environments # http_proxy_host = 1.2.3.4 # http_proxy_port = 3128 [agent] report = true ca_server = puppet-master.4lin.net server = puppet-master.4lin.net environment = test |
- /etc/default/puppetserver:
In dieser Datei wird unter anderem definiert, wie viel Ram sich der Java Prozess nehmen darf. Standard sind 256MB.
[...] JAVA_ARGS="-Xms2g -Xmx2g -XX:MaxPermSize=256m" [...] |
Auf meiner Test Instanz habe ich den Wert auf "128m" gesetzt und die VM selbst hat 4GB Ram, um den Puppetserver erfolgreich starten zu können.
Im Anschluss kann der Puppetserver gestartet werden. Der Dienst benötigt meist etwas Zeit, bis dieser gestartet ist. Also Geduld haben und ein Blick in die Logs werfen.
Eventuell kann es sein, dass der Puppetserver sich kein zweites Mal starten lässt, wegen fehlender Berechtigungen für das SSL Verzeichnis und dem User "puppet". Das lässt sich schnell korrigieren:
root@puppet-master ~ # chown puppet: -R /opt/puppetlabs/puppet/ssl/c* root@puppet-master ~ # chown puppet: -R /opt/puppetlabs/puppet/ssl/p* |
PuppetDB
Als nächstes benötigen wir PostgreSQL für die Puppetdatenbank "puppetdb". Der Puppetserver dient nur dafür um die Ressourcen bereitzustellen, die dann von dem Puppet-Agent abgerufen wird. Die Eigenschaften (Facts) des Hosts die der Agent auswertet, wird nirgends zentral gespeichert. Genau das ist die Aufgabe vom "puppetdb". Alle Facts werden vom Puppetserver an Puppetdb weitergegeben und liegen dann dort zentral vor. Damit ist es dann möglich mittels "@@" auf diese Informationen von jedem Puppet-Agent zu zugreifen. Leider unterstützt PuppetDB lediglich HSQL (nicht mehr unterstützt -> veraltet) und Postgresql. MySQL ist damit außen vor.
root@puppet-master ~ # aptitude install postgresql postgresql-contrib |
Folgt die Basiskonfiguration für PostgreSQL:
root@puppet-master ~ # su - postgres postgres@puppet-master ~ $ createuser -DRSP puppetdb postgres@puppet-master ~ $ createdb -E UTF8 -O puppetdb puppetdb postgres@puppet-master ~ $ exit |
Gibt es eine Fehlermeldung...:
createdb: database creation failed: ERROR: new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII) HINT: Use the same encoding as in the template database, or use template0 as template.
... muss zuvor das Template1 mit UTF8 erstellt werden:
root@puppet-master ~ # su - postgres postgres@puppet-master ~ $ psql postgres=# UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1'; postgres=# DROP DATABASE template1; postgres=# CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE'; postgres=# UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1'; postgres=# \c template1 You are now connected to database "template1" as user "postgres". template1=# VACUUM FREEZE; template1-# \q |
Danach kann die Datenbank "puppetdb" wie oben gezeigt, erstellt werden. Nach der Datenbank wird als nächstes eine Erweiterung hinzugefügt, welche sich im jeweiligen "postgresql-contrib-x.y" Paket verbirgt:
root@puppet-master ~ # su - postgres postgres@puppet-master ~ $ psql puppetdb -c 'create extension pg_trgm' postgres@puppet-master ~ $ exit |
Fehlt noch die Authentifizierung:
- /etc/postgresql/9.4/main/pg_hba.conf:
# Debian Standard local all postgres peer local all all peer host all all 127.0.0.1/32 md5 host all all ::1/128 md5 # Neu local all all md5 |
Die oberen Zeilen sind bereits vorhanden, die "local" Zeile wurde hinzugefügt.
root@puppet-master ~ # service postgresql restart root@puppet-master ~ # psql -h localhost puppetdb puppetdb Password for user puppetdb: psql (9.4.10) SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off) Type "help" for help. puppetdb=> \q |
Damit ist der PostgreSQL Teil erledigt, folgt PuppetDB selbst:
root@puppet-master ~ # aptitude install puppetdb puppetdb-termini |
Bei der Ersteinrichtung werden die SSL Zertifikate die zuvor von der Installation von Puppetserver bzw. puppet-agent erstellt worden sind, nach /etc/puppetlabs/puppetdb/ssl kopiert. Wurde zuerst puppetdb installiert oder eine neue CA aufgesetzt, kann das Setup mittels puppetdb ssl-setup erneut aufgerufen werden.
PuppetDB befüllt die Datenbank automatisch, es müssen lediglich die Daten für die Authentifizierung hinterlegt werden. Diese befinden sich in /etc/puppetlabs/puppetdb/database.ini
- /etc/puppetlabs/puppetdb/database.ini:
[database] classname = org.postgresql.Driver subprotocol = postgresql subname = //localhost:5432/puppetdb username = puppetdb password = neuroses-clap-latchet-manasses gc-interval = 60 log-slow-statements = 10 |
root@puppet-master ~ # service puppetdb start |
Der Start dauert auch hier ein wenig ...
Nun kommt der letzte Teil für PuppetDB. Wir müssen dem Puppetserver noch mitteilen, dass die Daten an PuppetDB übergeben werden sollen. Dazu müssen die passenden Zeilen in der puppet.conf hinterlegt werden. Wurde die Konfiguration von "oben" verwendet, reicht es die '#' zu entfernen:
- /etc/puppetlabs/puppet/puppet.conf:
[...] [Master] storeconfigs = true storeconfigs_backend = puppetdb |
Wird nun der Puppetserver neugestartet, werden alle Tabelle angelegt und mit ersten Informationen gefüttert. Das reicht allerdings noch nicht ganz, sondern wir benötigen dann noch die puppetdb.conf, welche in der Regel noch nicht existiert:
- /etc/puppetlabs/puppet/puppetdb.conf:
[main] server_urls = https://puppet-master.4lin.net:8081 |
Hiera
Normalerweise würde man nun damit beginnen, die Manifests für Module (Apt/Webserver...) anzulegen um diese dann für Nodes zu verwenden. Das Problem: schon bei nur wenigen Nodes (aka Hosts) würden früher oder später Zeilen doppelt geschrieben werden müssen, weil es Ausnahmen gibt. Zum Beispiel soll der Webserver den internen NTP Server verwenden, der NTP Server selbst jedoch von pool.ntp.org. Das wäre an dieser Stelle noch "einfach" zu lösen, doch je länger eine Puppet Infrastruktur "lebt", desto komplizierter wird es, Ausnahmen zu erstellen, ohne ganze Ketten von Profilen, Modulen und Nodes umzuschreiben. Diesem Problem soll Hiera entgegenwirken, in dem Redundanzen komplett vermieden werden, oder zumindest stark reduziert werden können. Es werden nur noch Module gepflegt mit Eigenschaften, die Attribute kommen jedoch aus separaten aus einer anderen Quelle. Das Schöne: In einer perfekten Hiera Welt benötigt es keine Node Manifeste und für Abweichungen nur eine Zeile. Genug geschwafelt.
hiera.yaml
Zuerst muss definiert werden, wo sich die hiera.yaml Datei befindet. Das wiederum steht in der puppet.conf
- /etc/puppetlabs/puppet/puppet.conf:
... [Master] ... hiera_config = /etc/puppetlabs/puppet/hiera.yaml |
Der Inhalt dieser Datei kann wie im folgendem Aussehen:
- /etc/puppetlabs/puppet/hiera.yaml:
--- :merge_behavior: deeper :backends: - yaml - eyaml - json :yaml: :datadir: "/opt/puppetlabs/environments/%{::environment}/hieradata" :json: :datadir: "/opt/puppetlabs/environments/%{::environment}/hieradata" :eyaml: :datadir: "/opt/puppetlabs/environments/%{::environment}/hieradata" :pkcs7_private_key: "/opt/puppetlabs/environments/%{::environment}/hieradata/keys/private_key.pkcs7.pem" :pkcs7_private_key: "/opt/puppetlabs/environments/%{::environment}/hieradata/keys/public_key.pkcs7.pem" :hierarchy: - "role/%{::role}" - "node/%{::fqdn}" - "profile/%{::profile}" - "virtual/%{::is_virtual}" - "common" - "monitoring" |
Dies ist bereits eine Variante, wie wir sie für Icinga2 benötigen. Die monitoring(.yaml) am Ende enthält Werte, die in verschiedenen Modulen wiederverwendet werden. Es gibt eine Vielzahl von weiteren Kombinationen, die für dieses Beispiel zu weit weg führen würden. Es sind in jedem Fall zwei Dinge zu beachten:
- Es wird hauptsächlich Yaml verwendet, was bedeutet, dass auf die Einrückung geachtet wird, also auf die Anzahl der Leerzeichen bzw. Tabulator Stops.
- %{::fqdn} ist der Verweis auf ein Fakt, welches beim Aufruf des Puppet Agents auf dem Host bekannt sein muss:
root@puppet-master ~ # facter -p fqdn puppet-master.4lin.net |
Fakten die nicht vorhanden sind, müssen zuvor erstellt und auf den Nodes eingerichtet werden:
root@puppet-master ~ # facter -p role |
Eine weitere Besonderheit ist "Eyaml". Eyaml bietet die Möglichkeit sensitive Informationen via Gnupg zu verschlüsseln. Was verschlüsselt werden soll, ist dabei unerheblich. Es können einfach nur Passwörter sein, oder auch Zertifikate.
root@puppet-master ~ # aptitude install hiera-eyaml root@puppet-master ~ # su - puppet puppet@puppet-master ~ $ puppetserver gem install hiera-eyaml |
In diesem Beispiel, haben wir drei Umgebungen:
- live - Für die produktiven Server (kann man auch production nennen)
- dev - Für Server in der Entwicklung
- test - Für Testzwecke
Für den Anfang konzentrieren wir uns auf "test". Für Eyaml erstellen wir also passende Ordner und legen dort die Schlüssel ab:
root@puppet-master ~ # su - puppet puppet@puppet-master ~ $ cd /opt/puppetlabs/environments/ puppet@puppet-master /opt/puppetlabs/environments $ mkdir -p test/hieradata && cd test/hieradata puppet@puppet-master /opt/puppetlabs/environments/test/hieradata $ eyaml createkeys puppet@puppet-master /opt/puppetlabs/environments/test/hieradata $ chmod -R 0500 keys/ puppet@puppet-master /opt/puppetlabs/environments/test/hieradata $ chmod 0400 keys/*.pem |
Damit haben wir die Schlüssel für eyaml erstellt und Dank der hiera.yaml weiß Puppet auch, wo sie zu finden sind.
Ein kurzer Test:
root@puppet-master ~ # su - puppet puppet@puppet-master /opt/puppetlabs/server/data/puppetserver/puppet/test/hieradata $ eyaml encrypt -s 'Spinat ist grün' |
Eyaml spuckt dann zwei Variationen vom verschlüsselten Text aus. Hat es nicht geklappt, liegt es sehr wahrscheinlich daran, dass man sich im falschem Verzeichnis befindet, sodass die Schlüssel nicht gefunden werden.
Ein weiteres Beispiel:
root@puppet-master ~ # su - puppet puppet@puppet-master /opt/puppetlabs/server/data/puppetserver/puppet/test/hieradata $ eyaml edit common.eyaml |
--- klartext: kann man lesen spinat::ist:: DEC::PKCS7[grün]! |
Wurde der Text gespeichert und der Editor beendet, wird der Inhalt von spinat::ist:: verschlüsselt, der obere Teil jedoch nicht. Einfach mal einen Blick in die Datei mit "cat" werfen ;-). Ein wichtiger Hinweis an dieser Stelle: Eyaml fügt IDs ein, zum Beispiel: DEC(1). Diese darf man nicht kopieren, sondern löscht sie beim Kopieren der Zeilen:
- FALSCH:
spinat::ist:: DEC(1)::PKCS7[grün]! spargel::ist:: DEC(1)::PKCS7[weiß]!
- RICHTIG:
spinat::ist:: DEC(1)::PKCS7[grün]! spargel::ist:: DEC::PKCS7[weiß]!
Eyaml fügt die IDs dann automatisch ein.
Basis
Für die ersten Schritte verwenden wir die common.yaml und lassen die /etc/apt/sources.list von Puppet verwalten. Dazu installieren wir das puppetlabs-apt Modul und erstellen die Grundstruktur. Damit die Übersicht nicht zu sehr leidet, lasse ich den Arbeitspfad ($PWD) in den Shells weg. Der Hauptarbeitsordner ist: /opt/puppetlabs/server/data/puppetserver/puppet/test/
puppet@puppet-master ~ # puppet module install puppetlabs-apt |
Als nächstes muss die Umgebung (environment) "test" vorbereitet werden:
- /opt/puppetlabs/server/data/puppetserver/puppet/test/environment.conf:
modulepath = /opt/puppetlabs/environments/test/modules:/opt/puppetlabs/server/data/puppetserver/.puppetlabs/etc/code/modules |
Die environment.conf kann unter anderem enthalten, in welchen Pfaden nach Modulen gesucht werden soll. Dabei lässt sich ebenfalls eine Hierarchie aufbauen, sodass von test auch auf Module in dev oder live zugegriffen werden kann. Bei älteren Puppet Version (<3.8) muss unter anderem "parser = future" enthalten sein, da andernfalls verschiedene neue Puppet Aufrufe nicht klappen. Beim aktuellen Puppet ist das der neue Standard.
- /opt/puppetlabs/server/data/puppetserver/puppet/test/manifests/site.pp:
hiera_include('classes','') node default { } |
Da wir auf Node Manifests verzichten, lassen wir über die site.pp die Klassen via Hiera einbinden. Damit lässt sich zum Beispiel das Puppet Modul über die Klasse 'apt' aufrufen.
Und nun folgt das erste Beispiel über die common.yaml
- /opt/puppetlabs/server/data/puppetserver/puppet/test/hieradata/common.yaml:
--- classes: - 'apt' apt::purge: sources.list.d: true sources.list: true apt::sources: 'debian_stable': comment: 'This is the Debian stable mirror' location: 'http://ftp.debian.org/debian' release: 'jessie' repos: 'main contrib non-free' include: src: false deb: true 'debian_security': comment: 'This is the Debian stable security mirror' location: 'http://security.debian.org' release: 'jessie/updates' repos: 'main contrib' include: src: false deb: true 'debian_updates': comment: 'This is the Debian stable update mirror' location: 'http://ftp.debian.org/debian' release: 'jessie-updates' repos: 'main contrib' include: src: false deb: true 'puppetlabs': location: 'http://apt.puppetlabs.com' repos: 'PC1' release: 'jessie' key: id: '6F6B15509CF8E59E6E469F327F438280EF8D349F' server: 'pgp.mit.edu' |
Die common.yaml wird als (vor-)letztes aufgerufen und bedarf keiner weiteren facts. Im späteren Verlauf würde man die Apt Quellen nicht unbedingt in der common.yaml pflegen, sondern eher unter hieradata/os/debian.yaml (Dank {os/%{::family} wäre das möglich) . In unserem Fall lässt sich aber so zügig testen, ob Puppet an sich funktioniert.
Auch nochmal der Hinweis an dieser stelle: Es ist unbedingt darauf zu achten, dass die Yaml Syntax eingehalten wird.
Beim Aufruf von puppet agent -t sollten zwei Dinge passieren:
- Puppet löscht den Inhalt von sources.list und alles in sources.list.d/
- Puppet legt für jedes Repository eine Datei mit passendem Namen an
puppet agent -t Info: Using configured environment 'test' Info: Retrieving pluginfacts Info: Retrieving plugin Info: Loading facts Info: Caching catalog for puppet-master.4lin.net Info: Applying configuration version '1489700295' Notice: /Stage[main]/Apt/File[sources.list]/content: --- /etc/apt/sources.list 2017-03-16 21:38:03.353839833 +0000 +++ /tmp/puppet-file20170316-7253-muxbvl 2017-03-16 21:38:17.169393864 +0000 @@ -1,21 +1 @@ # Repos managed by puppet. -# Puppetlabs PC1 jessie Repository [...] |
Es wird die Umgebung "test" verwendet, da dies für den Puppet Agent so in der /etc/puppetlabs/puppet/puppet.conf definiert wurde. Später sollte man auf dem Puppet Server natürlich auf "live" oder "production" übergehen.
Sollte etwas nicht klappen, so empfehle ich immer Syntax Fehler gezielt einzustreuen, um zu sehen, ob Puppet die Dateien überhaupt aufruft. Es reicht eine Klammer zu entfernen, in der site.pp oder dann danach in der common.yaml ein Hochkomma zu entfernen. Stolpert dann der Puppet Agent über diese Syntaxfehler, ist automatisch klar, dass der Fehler nicht an der Umgebung liegt, sondern eher an den Klassen, bzw. deren Verwendung.
Hat alles geklappt, so kann man nun die zweite VM/Container Instanz hochfahren und dort den Puppet Agent einrichten.
Puppet Agent - Icinga Master
Hier wird noch nicht so viel passieren, nur der Puppet Agent muss eingerichtet werden. Dazu benötigen wir:
- Ein(e) VM/LXC Container
- Puppet Apt Quellen für PC1
- /etc/apt/sources.list:
deb http://apt.puppetlabs.com jessie PC1 |
# aptitude update # aptitude install puppetlabs-release-pc1 lsb-release # aptitude update # aptitude install puppet-agent |
Das Paket puppetlabs-release-pc1 benötigen wir, damit der Apt Schlüssel für das Apt Repository vorhanden ist und Apt nicht meckert. Außerdem wird die $PATH Variable um /opt/puppetlabs/bin/ erweitert. lsb-release ist nicht immer installiert, wird aber vom Puppet Agent benötigt.
Ist der Puppet Agent auf dem System, sollte man sich neu einloggen, damit wir die neue $PATH Variable bekommen und gesetzt wird.
Damit kann es an das Konfigurieren gehen:
- /etc/puppetlabs/puppet/puppet.conf:
[agent] report = true ca_server = puppet-master.4lin.net server = puppet-master.4lin.net environment = test |
Das genügt bereits für den Anfang. Wichtig ist noch, dass der FQDN stimmt, andernfalls würde gleich das Zertifikat falsch erzeugt werden. Die /etc/resolv.conf sollte daher geprüft werden.
root@icinga-master-01 ~ # puppet agent -t --noop Info: Creating a new SSL key for icinga-master-01.4lin.net Info: Caching certificate for ca Info: csr_attributes file loading from /etc/puppetlabs/puppet/csr_attributes.yaml Info: Creating a new SSL certificate request for icinga-master-01.4lin.net Info: Certificate Request fingerprint (SHA256): 0B:FA:13:97:A1:A5:FC:EA:8E:93:31:9E:53:4E:7E:48:52:78:A6:16:50:95:2F:9B:7D:11:09:33:3F:50:96:18 Info: Caching certificate for ca Exiting; no certificate found and waitforcert is disabled |
Auf der Puppet-master Seite, müssen wir das Zertifikat nun unterschreiben lassen:
- Auf Puppet Master:
root@puppet-master ~ # puppet cert sign icinga-master-01.4lin.net Signing Certificate Request for: "icinga-master-01.4lin.net" (SHA256) 0B:FA:13:97:A1:A5:FC:EA:8E:93:31:9E:53:4E:7E:48:52:78:A6:16:50:95:2F:9B:7D:11:09:33:3F:50:96:18 Notice: Signed certificate request for icinga-master-01.4lin.net Notice: Removing file Puppet::SSL::CertificateRequest icinga-master-01.4lin.net at '/opt/puppetlabs/puppet/ssl/ca/requests/icinga-master-01.4lin.net.pem' root@puppet-master ~ # puppet cert list --all + "icinga-master.4lin.net" (SHA256) DB:45:97:7E:71:2E:CA:2B:EE:F3:F9:65:AB:45:52:BD:BB:D9:F6:EA:52:3A:E2:7A:52:8C:B5:34:0D:B1:6F:A2 + "puppet-master.4lin.net" (SHA256) B9:AA:DE:37:43:91:AA:B9:9F:1E:6E:1B:5C:29:8A:BA:9D:E6:2F:3D:C3:79:0F:69:77:67:FF:32:9D:67:24:35 (alt names: "DNS:puppet", "DNS:puppet-master.4lin.net") |
Damit können wir nun wieder auf icinga-master-01 den Agent erneut starten:
- Auf Icinga Master:
root@icinga-master-01 ~ # puppet agent -t Info: Caching certificate for icinga-master-01.4lin.net Info: Caching certificate for icinga-master-01.4lin.net Info: Using configured environment 'test' Info: Retrieving pluginfacts Info: Retrieving plugin Info: Loading facts Info: Caching catalog for icinga-master-01.4lin.net Info: Applying configuration version '1489774490' Notice: /Stage[main]/Apt/Apt::Source[debian_stable]/Apt::Setting[list-debian_stable]/File[/etc/apt/sources.list.d/debian_stable.list]/ensure: defined content as '{md5}7b431f7079129d235f07fee6d842172a' [...] |
Damit haben wir nun sowohl einen Puppet Master und eine Node die wir mir dem Puppet Agent Konfigurieren lassen können. Dadurch dass wir nur die common.yaml aktuell verwenden, werden auch auf dieser VM nur die Apt Quellen angetastet.
Rollen
Wie oben bereits angedeutet, wollen wir Rollen und Profile (1:n) verwenden. Da es das "Fact" "role" nicht gibt ($ facter -p role), müssen wir dieses Faktum erzeugen.
Dazu erstellen wir eine "Modulstruktur" unterhalb von test/ die das Faktum "role" auf die Nodes bringt. Das klappt allerdings nur, wenn wir die Funktion "pluginsync = true" auf beiden Seiten aktivieren.
pluginsync = true sorgt ab der aktuellsten Version (4.x) / Puppet Agent 1.9.3 für: Warning: Setting 'pluginsync' is deprecated. Ticket: https://tickets.puppetlabs.com/browse/PUP-5708. Sobald ich die Alternative kenne, werden die Seiten angepasst.
- Puppet-Master: /etc/puppetlabs/puppet/puppet.conf:
- Icinga-Master-01: /etc/puppetlabs/puppet/puppet.conf:
[main] [...] pluginsync = true |
Danach den Puppetserver Dienst neustarten:
root@puppet-master ~ # service puppetserver restart |
Auf dem icinga-master reicht es den Agent einfach nur aufzurufen, aber das kann später erfolgen.
Als nächsten ersten wir die "role" Modulstruktur:
puppet@puppet-master ~ test/ $ mkdir -p modules/role/lib/facter/ puppet@puppet-master ~ test/ $ mkdir -p modules/role/manifests |
Und legen im lib Ordner zwei Dateien an:
- Icinga-Master: test/modules/role/lib/facter/role.rb:
# https://rnelson0.com/2014/07/14/intro-to-roles-and-profiles-with-puppet-and-hiera/ # https://rnelson0.com/2014/07/14/intro-to-roles-and-profiles-with-puppet-and-hiera/ # ([a-z]+)[0-9]+, i.e. www01 or logger22 have a role of www or logger if Facter.value(:hostname) =~ /^([a-z]+)[0-9]+$/ Facter.add('role') do setcode do $1 end end # ([a-z]+), i.e. www or logger have a role of www or logger elsif Facter.value(:hostname) =~ /^([a-z]+)$/ Facter.add('role') do setcode do $1 end end # ([a-z]+), i.e. www-01 or logger-01 have a role of www or logger elsif Facter.value(:hostname) =~ /^([a-z]+)-[0-9]+$/ Facter.add('role') do setcode do $1 end end # ([a-z]+), i.e. mon-master-01 or logger-slave-02 have a role of www-master or logger-slave elsif Facter.value(:hostname) =~ /^([a-z]+)-([a-z]+)-[0-9]+$/ Facter.add('role') do setcode do $1+'-'+$2 end end # Set to hostname if no patterns match else Facter.add('role') do setcode do 'default' end end end |
Diese Version ist gegenüber der Originalversion etwas erweitert worden, damit auch Namen klappen wie "www-01", oder auch "icinga-master-01". Aus dem Hostname wird dann die Rolle "www", oder "icinga-master".
- Icinga-Master: test/modules/role/lib/facter/puppet_classes.rb:
classes_file = '/opt/puppetlabs/puppet/cache/state/classes.txt' classes_hash = {} modules_array = [] File.foreach(classes_file) do |l| modules_array << l.chomp.gsub(/::.*/, '') end modules_array = modules_array.sort.uniq modules_array.each do |i| classes_array = [] classes_array << i File.foreach(classes_file) do |l| classes_array << l.chomp if l =~ /^#{i}/ classes_array = classes_array.sort.uniq end classes_hash[i] = classes_array end Facter.add( :puppet_modules) do confine :kernel => 'Linux' setcode do modules_array.sort.uniq.join(', ').to_s end end Facter.add( :puppet_classes) do confine :kernel => 'Linux' setcode do classes_hash.map { |_k, v| v }.sort.uniq.join(', ').to_s end end |
Diese Datei kann dafür genutzt werden, aus den zugeordneten / verwendeten Puppet Klassen die beiden neuen Fakten "puppet_classes" und "puppet_modules" zu erstellen, die sich dann für die Profile nutzen lassen. Wird also einer Node die Klasse "nginx" zu gewiesen, kann daraus das Profil "webserver" abgeleitet werden. Eventuell muss bei der "puppet_classes.rb" der Pfad zu der "classes.txt" Datei angepasst werden. Dazu einfach mittels "find" im Puppet Ordner suchen lassen.
Profile
Der Profilanteil dieser Anleitung lässt sich mit wenigen Sätzen erklären:
Es geht lediglich darum, Puppet Manifests zu erstellen, die alles notwendige enthalten, um aus einer Node einen Webserver zu erstellen. Die Kunst der Profile besteht darin, soweit wie möglich zu abstrahieren, sodass die Profile keine starren Zuordnungen enthalten. Es gibt aber auch Abseits Gründe, warum Profile Sinn ergeben: nicht jedes Puppet Modul bietet die Möglichkeit alles mittels Hiera zu definieren, sondern Profile bieten dann die Schnittstelle zwischen Hiera und dem Modul. Ein sehr bekanntes Beispiel ist das Apache Modul, bei dem VirtualHost zwar in Hiera definiert werden, aber erst die entsprechenden Manifest Zeilen es in die Tat umsetzen. Dazu dann später mehr.
Icinga-Master
Um mir die Tipparbeit zu erleichtern, gelten folgende Konventionen:
- Puppet Dateien werden immer als User "puppet" editiert
- Ein sehr großer Teil entstammt einem fertigen Testsystem, daher wird vieles per Copy/Paste übernommen, sofern sinnvoll.
- Ich versuche die Profile so aufzulisten, sodass der Puppet Agent nach jedem Profil Abschnitt sauber durchlaufen kann.
- Manifest/Konfigurations Dateien enthalten immer einen Kopf, damit klar wird, durch welches Modul eine Veränderung vorgenommen wurde
- Dateien sollten definitiv per Git gepflegt werden
- Der Ort ist, wenn nicht anders definiert: /opt/puppetlabs/environments/test/
- Die Unterordner sind:
- hieradata/ -> Alles was mit Yaml zu tun hat
- modules/ -> Wo alle eigenen Module liegen
- modules/role -> Rollenmanifests
- modules/profile -> Profilmanifests
- Es gibt weniger zu lesen, sondern mehr zu tippen / kopieren :-)
- Es ist nicht perfekt, aber nachvollziehbar und Anregungen nehme ich sehr sehr gern an
- Der Icinga-master-01 bildet sozusagen alles ab, was es im Moment gibt, aber eventuell gibt es sinnvollere Wege ;-)
Rolle/Profile
- Der Icinga-Master hat die Rolle: role::icinga2_server
- Die Profile sind:
- profile::base
- profile::mysql::server
- profile::mysql::client
- profile::icinga2::server
- profile::apache2::server
- profile::apache2::php
- profile::icinga2::web
- profile::graphite::graphite
- profile::grafana::grafana
- profile::influxdb::influxdb
- profile::monitoring
Profile base
- Enthält alles, was auf allen Nodes konfiguriert werden soll
- Puppet Modul für Sudo und NTP installieren:
# puppet module install saz-sudo # puppet module install puppetlabs-ntp # puppet module install puppetlabs-concat # puppet module install camptocamp-augeas # puppet module install herculesteam-augeasproviders_shellvar |
- modules/profile/manifests/base.pp:
# source modules/profile/manifests/base.pp # == Class: profile::base class profile::base { contain 'profile::base::apt' contain 'profile::base::sudo' class { '::ntp': servers => [ 'ptbtime1.ptb.de', 'ptbtime2.ptb.de', '1.rhel.pool.ntp.org'], } @@host { $::fqdn: ensure => present, ip => $::ipaddress, host_aliases => $::hostname, } file { '/backup': ensure => directory, owner => root, group => root, mode => '0750', } package { ['git', 'htop', 'screen' ]: ensure => $ensure, } Host <<| |>> } |
- modules/profile/manifests/base/apt.pp:
# source modules/profile/manifests/base/apt.pp # == Class: profile::base::apt class profile::base::apt { # include base aptitude class include ::apt package { ['apt-transport-https' ]: ensure => $ensure, } # get keys from hiera and create them $keys = hiera_hash('profile::apt::keys', undef) if ($keys) { create_resources('apt::key', $keys) } # get repos from hiera and create them $repositories = hiera_hash('profile::apt::repositories', undef) if ($repositories) { create_resources('apt::source', $repositories) } } |
- modules/profile/manifests/base/sudo.pp:
# source modules/profile/manifests/base/sudo.pp # == Class: profile::base::sudo class profile::base::sudo { include ::sudo include ::sudo::configs } |
- hieradata/common.yaml:
--- classes: - 'profile::base' - 'profile::base::sudo' ########### Apt settings ############# apt::purge: sources.list.d: true sources.list: true ########### Apt sources ############# apt::sources: 'debian_stable': comment: 'hieradata/common.yaml' location: 'http://ftp.debian.org/debian' release: 'jessie' repos: 'main contrib non-free' include: src: false deb: true 'debian_security': comment: 'hieradata/common.yaml' location: 'http://security.debian.org' release: 'jessie/updates' repos: 'main contrib' include: src: false deb: true 'debian_updates': comment: 'hieradata/common.yaml' location: 'http://ftp.debian.org/debian' release: 'jessie-updates' repos: 'main contrib' include: src: false deb: true 'debian_backports': comment: 'hieradata/common.yaml' location: 'http://httpredir.debian.org/debian' release: 'jessie-backports' repos: 'main contrib' include: src: false deb: true 'puppetlabs': comment: 'hieradata/common.yaml' location: 'http://apt.puppetlabs.com' repos: 'PC1' release: 'jessie' key: id: '6F6B15509CF8E59E6E469F327F438280EF8D349F' server: 'pgp.mit.edu' 'debmon': comment: 'hieradata/common.yaml' location: 'http://debmon.org/debmon' release: 'debmon-jessie' repos: 'main' include: src: false deb: true key: id: '7E55BD75930BB3674BFD6582DC0EE15A29D662D2' source: 'http://debmon.org/debmon/repo.key' 'icinga': comment: 'hieradata/common.yaml' location: 'http://packages.icinga.org/debian' release: 'icinga-jessie' repos: 'main' key: id: 'F51A91A5EE001AA5D77D53C4C6E319C334410682' source: 'http://packages.icinga.com/icinga.key' include: src: false deb: true |
- hieradata/monitoring.yaml:
--- ########### Sudo settings ############# sudo::configs: 'nagios': 'content' : "%nagios ALL=(ALL) NOPASSWD: ALL" 'priority' : 10 ############ Icinga2 common vars ############ icinga_vars: os: Linux client_endpoint: "%{::fqdn}" domain: "%{::domain}" hostname: "%{::hostname}" address: "%{::ipaddress}" cores: "%{::processorcount}" virtual_machine: "%{::is_virtual}" distro: "%{::operatingsystem}" disks: 'disk /': disk_partitions: '/' ############ Icinga2 common object settings ############ icinga2::object::host: "%{::fqdn}": address: "%{::ipaddress}" |
Mysql Server
Alles rund um den MySQL / Mariadb
# puppet module install puppetlabs-mysql |
- modules/profile/manifests/mysql/base.pp:
# source modules/profile/manifests/mysql/base.pp # == Class: profile::mysql::server::base class profile::mysql::server { } |
- modules/profile/manifests/mysql/server.pp:
# source modules/profile/manifests/mysql/server.pp # == Class: profile::mysql::server::base class profile::mysql::server { include ::mysql::server include ::mysql::server::backup } |
Backup / root Passwort
Alles Passwörter werden verschlüsselt über eyaml abgelegt. Wichtig ist die Endung "*.eyaml" und sollte nicht mit der *.yaml verwechselt werden:
$ cd hieradata/ $ touch role/icinga-master.eyaml $ eyaml edit role/icinga-master.eyaml |
- role/icinga-master.eyaml:
mysql::server::root_password: DEC::PKCS7[dbrootsecret]! mysql::server::backup::backuppassword: DEC::PKCS7[dbbackupsecret]! |
Beim Aufruf vom Puppet Agent werden sowohl die Werte aus der Yaml, als auch auch der Eyaml Datei zusammengeführt.
Im Falle vom MySQL Puppet Modul werden die Daten auch in /root/.my.cnf abgelegt.
MySQL Client
- modules/profile/manifests/mysql/client.pp:
# source modules/profile/manifests/mysql/client.pp # == Class: profile::mysql::server::client class profile::mysql::client { include ::mysql::client } |
Profile der Rolle zuordnen
Nun sind wir an dem Punkt, an dem wir anfangen die Profile der Rolle zuzuordnen. Dazu erstellen wir das Manifest "modules/role/manifests/icinga2_server.pp" und fügen die Profile hinzu. Ich habe schon alle Profile hinzugefügt die noch kommen, aber noch auskommentiert. Lediglich profile::base und MySQL (allerdings nehmen wir MariaDB 10.x) ist soweit benutzbar. Wichtig wäre noch zu wissen, dass die profile::monitoring automatisch über Hiera eingebunden wird, wie dies bei der common der Fall ist. Daher legen wir dort Werte ab, die für mehrere Module genutzt werden (können).
- modules/role/manifests/icinga2_server.pp:
# Role icinga2_master class role::icinga2_server { contain profile::base contain profile::mysql::server contain profile::mysql::client # contain profile::icinga2::master # contain profile::apache2::server # contain profile::apache2::php # contain profile::icinga2::web # contain profile::graphite::graphite # contain profile::grafana::grafana # contain profile::influxdb::influxdb } |
Wie bereits angedeutet Werte, die Modul übergreifend sind. Auch hier habe ich wieder kräftig kopiert und es sind einige Werte enthalten, die in dieser Anleitung (noch) keinen Nutzen haben.
- hieradata/monitoring.yaml:
--- ########################## ### general settings ########################### 'monitoring::domain': '4lin.net' 'monitoring::admin_email': "monitor@%{hiera('monitoring::domain')}" ######################### ## Firewall settings ######################## 'monitoring::ip_whitelist': - "%{hiera('monitoring::icinga::ipaddress')}" - "%{hiera('monitoring::mysql::ipaddress')}" - "%{hiera('monitoring::influxdb::ipaddress')}" - "%{hiera('monitoring::puppet::ipaddress')}" ######################### ## SMTP Settings ######################### 'monitoring::smtp::fqdn': "mx-01.%{hiera('monitoring::domain')}" 'monitoring::smtp::ssl_type': 'STARTTLS' 'monitoring::smtp::port': 25 ########################## ## icinga related settings ########################## 'monitoring::icinga::fqdn': "mon.%{hiera('monitoring::domain')}" 'monitoring::icinga::mysql_db': 'icinga2_ido_db' 'monitoring::icinga::mysql_user': 'icinga2_ido_db' 'monitoring::icinga::ipaddress': '192.168.43.13' 'monitoring::icinga::influxdb::db' : 'icinga2' 'monitoring::icinga::influxdb::user' : 'icinga2' ########################## ## icingaweb2 related settings ########################## 'monitoring::icingaweb2::mysql_db': 'icingaweb2_db' 'monitoring::icingaweb2::mysql_user': 'icingaweb2_db' 'monitoring::icingaweb2_director::mysql_db': 'icingaweb2_director_db' 'monitoring::icingaweb2_director::mysql_user': 'icingaweb2_director_db' ############################## #### InfluxDB Details ################################ 'monitoring::influxdb::ip': "graph.%{hiera('monitoring::domain')}" ############################## ### Grafana Details ############################### 'monitoring::grafana::db::host': "%{hiera('monitoring::mysql::ipaddress')}" 'monitoring::grafana::mysql_db': 'grafana_db' 'monitoring::grafana::mysql_user': 'grafana_db' 'monitoring::grafana::user': 'admin' 'monitoring::grafana::url': "graph.%{hiera('monitoring::domain')}" 'monitoring::grafana::influxdb::user': 'admin' 'monitoring::grafana::influxdb::password': 'admin' 'monitoring::grafana::port': 3000 'monitoring::grafana::elasticsearchurl': "127.0.0.1" 'monitoring::grafana::influxdb::ipaddress': "graph.%{hiera('monitoring::domain')}" 'monitoring::grafana::influxdbdatabase': "icinga2" 'monitoring::grafana::install_method': 'repo' 'monitoring::grafana::version': '4.2.0' |
In der hieradata/role/icinga-master.yaml kommt nun alles zusammen. Da sie bei mir sehr umfangreich geworden ist, wäre es nicht verkehrt einzelne Bestandteile auszulagern.
- hieradata/role/icinga-master.yaml:
# Icinga2 Master Monitoring Node --- classes: - role::icinga2_server ############# MySQL base settings ############# mysql::server::restart : 'true' mysql::server::backup::backupuser : 'dbbackup' mysql::server::backup::backupdir : '/backup/mysql' mysql::server::backup::backupcompress : 'true' mysql::server::backup::backuprotate : 90 mysql::server::backup::file_per_database : 'true' mysql::server::backup::time : ['*', '00'] mysql::server::package_name : 'mariadb-server-10.0' mysql::client::package_name : 'mariadb-client-10.0' ############# MySQL override options ############# mysql::server::override_options : 'mysqld': bind-address : '0.0.0.0' server-id : 1 auto-increment-increment : 2 auto-increment-offset : 1 ############# MySQL user ############# mysql::server::users: '@localhost': ensure : 'absent' '@mysql': ensure : 'absent' 'root@127.0.0.1': ensure : 'present' ############# MySQL databases ############# mysql::server::databases: test: ensure : 'absent' |
Damit haben wir einen funktionierenden MySQL Server, mit MariaDB als Backend beim Ausführen des Puppet Agents auf icinga-master-01.
Dann kann es ja mit den anderen Profilen weitergehen :-)
Profil Icinga2
Nachfolgende Zeilen entstammen zu einem sehr großen Anteil von hier: https://github.com/Icinga/puppet-icinga2/tree/master/examples/example3 Ohne diese enorme Hilfe, hätte ich noch ewig benötigt, alles zusammen zu bekommen. Daher mein Dank an https://github.com/kwisatz !
Das icinga2-puppet Modul besorgen wir uns über Puppet:
# puppet module install puppet-icinga2 |
Profil Icinga2 Server
Dieses Profil sorgt für alles Grundlegende, was benötigt wird. Es empfiehlt sich die README durchzulesen. Es wurde lediglich um einige Zeilen erweitert, dazu unten mehr:
- modules/profile/manifests/icinga2/server.pp:
class profile::icinga2::server ( $ido_db_name = hiera('monitoring::icinga::mysql_db') $ido_db_user = hiera('monitoring::icinga::mysql_user') $ido_db_pass = hiera('monitoring::icinga::mysql_password') $ido_db_host = hiera('monitoring::mysql::ipaddress') ) { user { 'nagios': groups => ssl-cert } mysql::db { "$ido_db_name": user => "$ido_db_user", password => "$ido_db_pass", host => "$ido_db_host", grant => ['ALL'], require => Package['mysql-client'], } class { '::icinga2': } icinga2::object::zone { 'global-templates': global => true, } icinga2::object::zone { 'director-global': global => true, } file { 'icinga2_global_templates': path => '/etc/icinga2/zones.d/global-templates', ensure => directory, purge => true, recurse => true, }-> File <<| ensure != 'directory' and tag == 'icinga2::scripts::file' |>> { } # Collect all hosts into their respective directories. file { 'icinga2_masterzone': path => '/etc/icinga2/zones.d/master', ensure => directory, purge => true, recurse => true, }-> file { 'icinga2_hosts': path => '/etc/icinga2/conf.d/hosts', ensure => directory, purge => true, recurse => true, }-> Icinga2::Object::Host <<| |>> { } # Export master zone and endpoint for all agents to collect @@icinga2::object::zone { 'master': endpoints => [ "$::fqdn", ], } @@icinga2::object::endpoint { "$::fqdn": host => "$::ipaddress_eth0", } # Collect and realize all agent zones and endpoints Icinga2::Object::Endpoint <<| |>> { } Icinga2::Object::Zone <<| |>> { } # Collect services and notifications exported on agent nodes # (and not created by the Apply Rules included below) file { 'icinga2_services': path => '/etc/icinga2/conf.d/services', ensure => directory, purge => true, recurse => true, }-> Icinga2::Object::Service <<| |>> { } file { 'icinga2_notifications': path => '/etc/icinga2/conf.d/notifications', ensure => directory, purge => true, recurse => true, }-> Icinga2::Object::Notification <<| |>> { } # Collect check and notification commands that are not created by Apply file { 'icinga2_commands': path => '/etc/icinga2/conf.d/commands', ensure => directory, purge => true, recurse => true, }-> Icinga2::Object::Checkcommand <<| |>> { }-> Icinga2::Object::NotificationCommand <<| |>> { } # Define apply rules that # contain profile::icinga2::applyrules # Create API users from Hiera # $myIcinga2ApiUser = hiera('icinga2::object::apiuser', {}) # create_resources( 'icinga2::object::apiuser', $myIcinga2ApiUser) # Create Icinga hosts from Hiera # $myIcinga2Hosts = hiera('icinga2::object::host', {}) # create_resources( 'icinga2::object::host', $myIcinga2Hosts) # Create Icinga hostgroups from Hiera # $myIcinga2HostGroup = hiera('icinga2::object::hostgroup', {}) # create_resources( 'icinga2::object::hostgroup', $myIcinga2HostGroup) # Create Icinga servicegroups from Hiera # $myIcinga2ServiceGroup = hiera('icinga2::object::servicegroup', {}) # create_resources( 'icinga2::object::servicegroup', $myIcinga2ServiceGroup) # Note: these manifests are not included in this example # contain profile::icinga2::hostgroups # contain profile::icinga2::users # contain profile::icinga2::timeperiods # contain profile::icinga2::notifications # contain profile::icinga2::checkcommands } |
Die Zeilen mit "$my*" sind eingefügt worden, um in Hiera Ressourcen definieren zu können, für die es keine Modul Hiera spezifischen Objekte gibt. Mittels $myIcinga2Hosts können so für den Icinga-Master Hosts erzeugt werden, die keinen Icinga-Agent haben, z.B: weil sie nur per SNMP erreichbar sind. Dies kann man zwar auch anderweitig lösen, z.B. über die Einbindung weitere Profile, aber in diesem Fall habe ich mich für diesen Weg entschieden. Im Augenblick sind sie noch nicht aktiv, das kommt später.
An dieser Stelle sollte noch kein Puppet Agent ausgeführt werden, denn es fehlt noch so einiges.
- hieradata/role/icinga-master.yaml:
############# Icinga2 settings ############# icinga2::constants: NodeName: "%{::fqdn}" ZoneName: 'master' # Change TicketSalt !! TicketSalt: '1234567890ß0945766325458937237545934583489502387450854903758972347809348230974wezriuwzeriuwrfoo' icinga2::features: - 'idomysql' - 'api' - 'checker' - 'mainlog' - 'statusdata' - 'command' icinga2::manage_database: true icinga2::restart_cmd: 'service icinga2 reload' icinga2::plugins: - 'nscp' - 'plugins' - 'plugins-contrib' - 'windows-plugins' - 'manubulon' icinga2::feature::idomysql::database: "%{hiera('monitoring::icinga::mysql_db')}" icinga2::feature::idomysql::user: "%{hiera('monitoring::icinga::mysql_user')}" icinga2::feature::idomysql::host: "%{hiera('monitoring::mysql::ipaddress')}" icinga2::feature::idomysql::import_schema: true icinga2::feature::api::accept_commands: true icinga2::feature::api::endpoints: {} icinga2::feature::api::zones: {} |
- Datenbankberechtigung:
- hieradata/role/icinga-master.yaml:
mysql::server::grants: "icinga2_ido_db@%/icinga2_ido_db.*": ensure : 'present' privileges : ['ALL'] table : 'icinga2_ido_db.*' user : 'icinga2_ido_db@%' |
- Datenbank anlegen:
- hieradata/role/icinga-master.yaml:
mysql::server::databases: test: ensure : 'absent' icinga2_ido_db: ensure : 'present' |
- Datenbank User anlegen:
Die User Passwörter (nicht das MySql root Passwort!) können leider nicht als Klartext in Hiera abgelegt werden, wie es in einem Manifest der Fall wäre. Das Attribut "password" existiert da leider nicht. Daher gäbe es zwei Möglichkeiten den Benutzer zu erstellen:
- Attribute password_hash: Hierbei wird zuvor ein Passwort-Hash erstellt, welches dann als Hiera Attribut eingegeben werden kann. Der Hash sollte dann dennoch über Eyaml verschlüsselt gespeichert werden.
- User im Manifest Profil: Eine andere Methode wäre es, den Datenbank User im Profil -- z.B. icinga2_server --- erzeugen zu lassen. Dann könnte das Passwort über Hiera bezogen werden, als Klartext. Das Passwort hätte dann gleich erneut verwendet werden können.
Die Ablage wäre dann natürlich wieder in Eyaml.
Die Erste Methode hat den Nachteil, dass das Passwort zweimal abgelegt werden muss; einmal als Hash, einmal als Klartext. Das Klartextpasswort benötigen für den Icinga2 Daemon. Ein weiterer Umstand ist, dass der komplette User mysql::server::users: Teil in Eyaml liegen muss.
Wie auch immer, ich empfinde die erste Methode als angenehmer, da sie mir flexibler erscheint.
- MySQL Hash erzeugen:
root@icinga-master-01 ~ # mysql -e 'select password("dbidosecret")' +-------------------------------------------+ | password("dbidosecret") | +-------------------------------------------+ | *994EDBCADFBB7973CC6B1346421BE0FE842F4C7E | +-------------------------------------------+ |
- EYAML:
- hieradata/role/icinga-master.eyaml:
############# MySQL user ############# mysql::server::users: '@localhost': ensure : 'absent' '@mysql': ensure : 'absent' 'root@127.0.0.1': ensure : 'present' 'icinga2_ido_db@%': ensure : 'present' password_hash : DEC::PKCS7[*994EDBCADFBB7973CC6B1346421BE0FE842F4C7E]! ############# Icinga secrets ############# icinga2::feature::idomysql::password: DEC::PKCS7[dbidosecret]! |
Ab diesem Punkt kann der Puppet Agent auf "icinga-master-01" ausgeführt werden und am Ende läuft ein Icinga2 der per MySQL seine Daten an die Datenbank überreicht. Wenn etwas nicht klappen sollte, einfach einen Blick in die Logs werfen.
Icinga2 Api User
Bevor es sich lohnt das Icingaweb2 anzugehen, sollte zuerst der API Teil aktiviert werden. Das ist in diesem Fall sehr einfach:
Die bereits vorhandenen $myIcinga2ApiUser Zeilen aktivieren:
- modules/profile/manifests/icinga2/server.pp:
... $myIcinga2ApiUser = hiera('icinga2::object::apiuser', {}) create_resources( 'icinga2::object::apiuser', $myIcinga2ApiUser) ... |
- role/icinga-master.yaml:
... icinga2::features: - 'api' ... ############# Icinga2 API User ############# icinga2::object::apiuser: 'root': target: '/etc/icinga2/conf.d/api-user.conf' apiuser_name: 'root' password: "%{hiera('icinga2::object::apiuser::root::password')}" permissions: - '*' |
- EYAML:
- role/icinga-master.eyaml:
icinga2::object::apiuser::root::password: DEC::PKCS7[icingaapirootsecret]! |
Damit wird die Datei /etc/icinga2/conf.d/api-user.conf erstellt mit dem ersten User Root und dem passendem Passwort aus der EYAML Datei.
Profil Apache2 Server
Zeit um einen Webserver zu installieren.
- Puppet Modul für Apache installieren:
# puppet module install puppetlabs-apache |
- modules/role/manifests/icinga2_server.pp:
... contain profile::apache2::server ... |
- modules/profile/manifests/apache2/server.pp:
# source modules/profile/manifests/apache2/server.pp # == Class: profile::apache2::server class profile::apache2::server { class { 'apache': default_vhost => false, } $myApache2Vhosts = hiera('apache::vhost', {}) create_resources('apache::vhost', $myApache2Vhosts) class { 'apache::mod::alias': } class { 'apache::mod::rewrite': } class { 'apache::mod::env': } class { 'apache::mod::headers': } class { 'apache::mod::setenvif': } class { 'apache::mod::deflate': } } |
Auch hier zu beachten, die Zeilen von $myApache2Vhosts, welche sich aus der Hiera Datei alles passende holen.
- hieradata/role/icinga-master.yaml:
... ############# Apache2 settings ############# apache::default_mods: false apache::mpm_module: prefork apache::vhost: "%{::fqdn}": servername: "%{::fqdn}" serveraliases: - 'icinga-master.%{::domain}' serveradmin: 'webmaster@%{::domain}' port: 80 docroot: '/var/www' redirect_status: 'permanent' redirect_dest: "https://%{::fqdn}/" "%{::fqdn}_ssl": servername: "%{::fqdn}" serveraliases: - 'icinga-master.%{::domain}' serveradmin: 'webmaster@%{::domain}' ssl: true ssl_cert: '/etc/ssl/certs/ssl-cert-snakeoil.pem' ssl_key: '/etc/ssl/private/ssl-cert-snakeoil.key' port: 443 docroot: '/var/www' |
Nach einem Puppet Agent durchlauf ist ein Apache2 installiert, der von Port 80 auf 443 (SSL) umleitet. Genutzt werden die TestSSL Zertifikate, die dann später durch LetsEncrypt ersetzt werden sollten.
Profil Apache2 PHP
Für Icingaweb2 benötigen wir PHP5, als aktivieren wir das passende Profil.
- modules/role/manifests/icinga2_server.pp:
... contain profile::apache2::php ... |
- modules/profile/manifests/apache2/php.pp:
# source modules/profile/manifests/apache2/php.pp # == Class: profile::apache2::php class profile::apache2::php { contain ::apache::mod::php package { php5-curl: ensure => installed, } } |
Ein erneuter Puppet Agent Lauf bringt PHP5 auf den Server und aktiviert es auch sogleich.
Profil Icinga2 Web
Nun kommt Icingaweb2. Dieses Profil ist wieder ein wenig aufwendiger. Die Basis kommt von hier: https://github.com/Icinga/puppet-icingaweb2#real-world-example
- Puppet Module für Ini Dateien und Repos:
# puppet module install puppetlabs-inifile # puppet module install puppetlabs-vcsrepo |
- Puppet Modul für Icingaweb2:
$ cd modules/ $ git clone https://github.com/Icinga/puppet-icingaweb2 icingaweb2 |
- Profil selbst:
# source modules/profile/manifests/icinga2/web.pp # == Class: profile::icinga2::web class profile::icinga2::web() { package { git: ensure => installed, } # Icinga2 results saved ido $ido_db_name = hiera('icinga2::feature::idomysql::database') $ido_db_user = hiera('icinga2::feature::idomysql::user') $ido_db_pass = hiera('icinga2::feature::idomysql::password') $ido_db_host = hiera('icinga2::feature::idomysql::host') $web_db_name = hiera('icingaweb2::db_name') $web_db_user = hiera('icingaweb2::db_user') $web_db_pass = hiera('icingaweb2::db_password') $web_db_host = hiera('icingaweb2::db_host') class { '::icingaweb2': initialize => true, install_method => 'package', manage_apache_vhost => true, # Icinga2 DB ido_db_name => $ido_db_name, ido_db_pass => $ido_db_pass, ido_db_user => $ido_db_user, # Icingaweb2 DB web_db_name => $web_db_name, web_db_pass => $web_db_pass, web_db_user => $web_db_user, require => Class['::mysql::server'], } -> # class { '::icingaweb2::mod::graphite': # install_method => 'git', # require => Package['git'], # } -> # augeas { 'php.ini': context => '/files/etc/php.ini/PHP', changes => ['set date.timezone Europe/Berlin',], } contain ::icingaweb2::mod::monitoring # contain ::icingaweb2::mod::graphite # contain ::icingaweb2::mod::director # contain ::icinga2::feature::graphite # contain ::icinga2::feature::influxdb # For director module # $myResourceDatabase = hiera('icingaweb2::config::resource_database', {}) #create_resources( 'icingaweb2::config::resource_database', $myResourceDatabase) } |
- hieradata/role/icinga-master.yaml:
... ############## MySQL grants ############# mysql::server::databases: ... 'icingaweb2_db@%/icingaweb2_db.*': ensure : 'present' privileges : ['ALL'] table : 'icingaweb2_db.*' user : 'icingaweb2_db@%' |
- hieradata/role/icinga-master.yaml:
############# MySQL databases ############# mysql::server::databases: ... icingaweb2_db: ensure : 'present' charset : 'utf8' |
root@icinga-master-01 ~ # mysql -e 'select password("dbicingawebsecret")' +-------------------------------------------+ | password("dbicingawebsecret") | +-------------------------------------------+ | *1FC0E6B8BE974EB5617E25EFE49902AB9441205F | +-------------------------------------------+ |
- EYAML
- hieradata/role/icinga-master.eyaml:
############# MySQL user ############# ... mysql::server::users: ... 'icingaweb2_db@%': ensure : 'present' password_hash : DEC::PKCS7[*1FC0E6B8BE974EB5617E25EFE49902AB9441205F]! ############# Icinga secrets ############# ... icingaweb2::db_pass: DEC::PKCS7[dbicingawebsecret]! |
Damit haben wir nun ein funktionierendes Icingaweb2, welches dann unter https://icinga-master-01/icingaweb2/ erreichbar ist. Die Standardzugangsdaten sind wie gehabt:
- User: icingaadmin
- Password: icinga
Profil Icinga2 Graphite
Um nun ein paar Performance Graphen in unserem Icingaweb2 zu erhalten, gibt es unterschiedliche Methoden. Die Variante die nun kommt, würde ich mittlerweile als "veraltet" ansehen, da Grafana auf dem Vormarsch ist und es erste Anläufe gibt, die Bilder in Icingaweb2 zu bekommen. Wie auch immer, ich führe es dennoch auf.
Ein paar Worte zur Apache Konfiguration:
Leider funktioniert Graphite nur als VirtualHost und nicht als Unterordner von einem vorhandenem VirtualHost, wie zum Beispiel icinga-master-01.4lin.net/graphite. Daher gibt es einen neuen VirtualHost graph.4lin.net mit den gleichen Kenndaten, lediglich um WSGI erweitert. Man muss also daran denken (oder Puppet), den DNS Namen auf die gleiche IP Adresse zu legen (oder CNAME).
Das Modul habe ich mir selbst zusammen geschustert, da mir alle Puppet Module für Graphite/Carbon einfach zu aufwendig waren. Zu selten muss etwas geändert werden ....
- modules/role/manifests/icinga2_server.pp:
... contain profile::graphite::graphite ... |
- modules/profile/manifests/graphite/graphite.pp:
# source modules/profile/manifests/graphite/init.pp # == Class: profile::graphite::graphite class profile::graphite::graphite { contain ::apache::mod::wsgi contain ::graphite contain ::icingaweb2::mod::graphite } |
Nicht über den doppelten Namen wundern. Man sollte es eigentlich mal in profile::graphite::basic umbenennen ...
- modules/graphite/manifests/init.pp:
class graphite { ## Hiera Lookups # Graphite $storage_dir = hiera('graphite::storage_dir', '/var/lib/graphite') $secret_key = hiera('graphite::secret_key', 'S3CrEt_k3Y') $whisper_dir = hiera('graphite::whisper_dir', '/var/lib/graphite/whisper') $log_dir = hiera('graphite::log_dir','/var/log/graphite') $index_file = hiera('graphite::index_file', '/var/lib/graphite/search_index') $engine = hiera('graphite::engine','django.db.backends.mysql') $time_zone = hiera('graphite::time_zone','Europe/Berlin') $web_port = hiera('graphite::web::port') $db_name = hiera('graphite::db_name', 'graphite_db') $db_user = hiera('graphite::db_user', 'graphite_db') $db_pass = hiera('graphite::db_pass', 'secret') $db_host = hiera('graphite::db_host', 'localhost') # Carbon $carbon_data_dir = hiera('graphite::carbon::data_dir', '/var/lib/graphite/whisper') $carbon_storage_dir = hiera('graphite::carbon::storage_dir', '/var/lib/graphite/whisper') $carbon_log_dir = hiera('graphite::carbon::log_dir', '/var/log/graphite') $icingaweb_base_url = hiera('icingaweb2::mod::graphite::graphite_base_url') # Packages $packages = [ 'python-mysqldb', 'graphite-carbon', 'python-django', 'python-django-tagging', 'python-cairo', 'python-tz', 'python-pyparsing', 'python-memcache', 'python-rrdtool', 'graphite-web', ] package { $packages: ensure => latest, } # Alternative zu service Deklaration unten # shellvar { 'graphite-carbon_default': # ensure => present, # target => '/etc/default/graphite-carbon', # variable => 'CARBON_CACHE_ENABLED', # value => 'true', # comment => 'We want graphite-carbon to run', # } ::mysql::db { $db_name: user => $db_user, password => $db_pass, host => $db_host, grant => ['ALL'], } file { '/etc/graphite/local_settings.py': notify => Service['carbon-cache'], content => template('graphite/local_settings_py.erb'), owner => root, group => www-data, mode => '0640', require => Package['graphite-carbon'], } file { '/etc/carbon/storage-schemas.conf': notify => Service['carbon-cache'], content => template('graphite/carbon_storage_schemas_conf.erb'), owner => root, group => root, mode => '0640', require => Package['graphite-carbon'], } file { '/etc/carbon/carbon.conf': notify => Service['carbon-cache'], content => template('graphite/carbon_conf.erb'), owner => root, group => root, mode => '0640', require => Package['graphite-carbon'], } file { '/etc/icingaweb2/modules/graphite/config.ini': content => template('graphite/icingaweb2_mod_graphite_config_ini.erb'), owner => root, group => root, mode => '0644', } exec { 'graphite-manage syncdb --noinput': cwd => '/var/tmp', path => ['/usr/bin', '/usr/sbin/',], creates => hiera('graphite::index_file'), require => Package['graphite-carbon'], } service { "carbon-cache": # provider => systemd, ensure => running, enable => true, require => Package['graphite-carbon'], } } |
- modules/graphite/templates/local_settings_py.erb:
## Managed by Puppet ## # modules/graphite/templates/local_settings_py.erb SECRET_KEY = <%= @secret_key %> TIME_ZONE = <%= @time_zone %> LOG_RENDERING_PERFORMANCE = True LOG_CACHE_PERFORMANCE = True LOG_METRIC_ACCESS = True GRAPHITE_ROOT = '/usr/share/graphite-web' CONF_DIR = '/etc/graphite' STORAGE_DIR = '<%= @storage_dir %>' CONTENT_DIR = '/usr/share/graphite-web/static' WHISPER_DIR = '<%= @whisper_dir %>' LOG_DIR = '<%= @log_dir %>' INDEX_FILE = '<%= @index_file %>' DATABASES = { 'default': { 'NAME': '<%= @db_name %>', 'ENGINE': '<%= @engine %>', 'USER': '<%= @db_user %>', 'PASSWORD': '<%= @db_pass %>', 'HOST': '<%= @db_host %>', 'PORT': '3306' } } |
- modules/graphite/templates/carbon_storage_schemas_conf.erb:
# Managed by Puppet # modules/graphite/templates/carbon_storage_schemas_conf.erb # [name] # pattern = regex # retentions = timePerPoint:timeToStore, timePerPoint:timeToStore, ... # Carbon's internal metrics. This entry should match what is specified in # CARBON_METRIC_PREFIX and CARBON_METRIC_INTERVAL settings [carbon] pattern = ^carbon\. retentions = 60:90d [default_1min_for_1day] pattern = .* retentions = 60s:1d [icinga_internals] pattern = ^icinga2\..*\.(max_check_attempts|reachable|current_attempt|execution_time|latency|state|state_type) retentions = 5m:7d [icinga_default] pattern = ^icinga2\. retentions = 1m:2d,5m:10d,30m:90d,360m:4y |
- modules/graphite/templates/carbon_conf.erb:
# Managed py Puppet # # modules/graphite/templates/carbon_conf.erb [cache] STORAGE_DIR = <%= @carbon_storage_dir %> CONF_DIR = /etc/carbon/ LOG_DIR = <%= @carbon_log_dir %> PID_DIR = /var/run/ LOCAL_DATA_DIR = <%= @carbon_data_dir %> ENABLE_LOGROTATION = True USER = _graphite MAX_CACHE_SIZE = inf MAX_UPDATES_PER_SECOND = 500 MAX_CREATES_PER_MINUTE = 50 LINE_RECEIVER_INTERFACE = 0.0.0.0 LINE_RECEIVER_PORT = 2003 ENABLE_UDP_LISTENER = False UDP_RECEIVER_INTERFACE = 0.0.0.0 UDP_RECEIVER_PORT = 2003 PICKLE_RECEIVER_INTERFACE = 0.0.0.0 PICKLE_RECEIVER_PORT = 2004 LOG_LISTENER_CONNECTIONS = True USE_INSECURE_UNPICKLER = False CACHE_QUERY_INTERFACE = 0.0.0.0 CACHE_QUERY_PORT = 7002 USE_FLOW_CONTROL = True LOG_UPDATES = False LOG_CACHE_HITS = False LOG_CACHE_QUEUE_SORTS = True CACHE_WRITE_STRATEGY = sorted WHISPER_AUTOFLUSH = False WHISPER_FALLOCATE_CREATE = True [relay] LINE_RECEIVER_INTERFACE = 0.0.0.0 LINE_RECEIVER_PORT = 2013 PICKLE_RECEIVER_INTERFACE = 0.0.0.0 PICKLE_RECEIVER_PORT = 2014 LOG_LISTENER_CONNECTIONS = True RELAY_METHOD = rules REPLICATION_FACTOR = 1 DESTINATIONS = 127.0.0.1:2004 MAX_DATAPOINTS_PER_MESSAGE = 500 MAX_QUEUE_SIZE = 10000 USE_FLOW_CONTROL = True [aggregator] LINE_RECEIVER_INTERFACE = 0.0.0.0 LINE_RECEIVER_PORT = 2023 PICKLE_RECEIVER_INTERFACE = 0.0.0.0 PICKLE_RECEIVER_PORT = 2024 LOG_LISTENER_CONNECTIONS = True FORWARD_ALL = True DESTINATIONS = 127.0.0.1:2004 REPLICATION_FACTOR = 1 MAX_QUEUE_SIZE = 10000 USE_FLOW_CONTROL = True MAX_DATAPOINTS_PER_MESSAGE = 500 MAX_AGGREGATION_INTERVALS = 5 |
- modules/graphite/templates/icingaweb2_mod_graphite_config_ini.erb:
; Managed by Puppet ; modules/graphite/templates/icingaweb2_mod_graphite_config_ini.erb [graphite] metric_prefix = icinga2 base_url = <%= @icingaweb_base_url %> legacy_mode = false ;service_name_template = "icinga2.$host.name$.services.$service.name$.$service.check_command$.perfdata.$metric$.value" ;host_name_template = "icinga2.$host.name$.host.$host.check_command$.perfdata.$metric$.value" ;graphite_args_template = "&target=$target$&source=0&width=300&height=120&hideAxes=true&lineWidth=2&hideLegend=true&colorList=$colorList$&areaMode=$areaMode$&areaAlpha=$areaAlpha$" graphite_iframe_w = 800px graphite_iframe_h = 700px graphite_area_mode = all graphite_area_alpha = 0.1 graphite_summarize_interval = 10min graphite_color_list = 049BAF,EE1D00,04B06E,0446B0,871E10,CB315D,B06904,B0049C host_name_template = icinga2.$host.name$.host.$host.check_command$.perfdata.$metric$.value service_name_template = icinga2.$host.name$.services.$service.name$.$service.check_command$.perfdata.$metric$.value graphite_args_template = &target=$target$&source=0&width=300&height=120&hideAxes=false&lineWidth=2&hideLegend=true&colorList=049BAF&bgcolor=white&fgcolor=black graphite_large_args_template = &target=$target$&source=0&width=800&height=700&colorList=049BAF&lineMode=connected&bgcolor=white&fgcolor=black |
Nach den vielen Templates, kommt nun die Hiera Konfiguration:
root@icinga-master-01 ~ # mysql -e 'select password("dbgraphitesecret")' +-------------------------------------------+ | password("dbgraphitesecret") | +-------------------------------------------+ | *467829391D367414291A5E8000D52F24C9CBCBDC | +-------------------------------------------+ |
- EYAML:
- hieradata/role/icinga-master.eyaml:
... mysql::server::users: ... 'graphite_db@%': ensure : 'present' password_hash : DEC::PKCS7[*467829391D367414291A5E8000D52F24C9CBCBDC]! # Graphite graphite::db_pass: DEC::PKCS7[dbgraphitesecret]! graphite::secret_key: DEC::PKCS7['S3Cr3T']! |
- hieradata/role/icinga-master.yaml:
############# Icinga2 settings ############# ... icinga2::features: ... - 'graphite' ... icinga2::feature::graphite::host: "graph.4lin.net" icinga2::feature::graphite::enable_send_thresholds: true icinga2::feature::graphite::enable_send_metadata: true ... ############# Icingaweb2 settings ############# ... icingaweb2::mod::graphite::graphite_base_url: 'https://graph.4lin.net/render?' ############# Graphite settings ############# graphite::db_name: 'graphite_db' graphite::db_user: 'graphite_db' graphite::db_host: 'localhost' graphite::time_zone: '"Europe/Berlin"' graphite::storage_dir: '/var/lib/graphite/whisper' graphite::whisper_dir: '/var/lib/graphite/whisper' graphite::log_dir: '/var/log/graphite' graphite::index_file: '/var/lib/graphite/search_index' graphite::engine: 'django.db.backends.mysql' graphite::web::port: '3000' graphite::carbon::data_dir: '/var/lib/graphite/whisper' graphite::carbon::storage_dir: '/var/lib/graphite' graphite::carbon::log_dir: '/var/log/carbon' ... ############# Apache2 settings ############# ... "graph.%{::domain}": servername: "graph.%{::domain}" serveraliases: - "graph-01.%{::domain}" serveradmin: "webmaster@%{::domain}" port: 80 docroot: '/var/www' redirect_status: 'permanent' redirect_dest: "https://graph.%{::domain}/" "graph.%{::domain}_ssl": servername: "graph.%{::domain}" serveraliases: - "graph-01.%{::domain}" serveradmin: "webmaster@%{::domain}" ssl: true ssl_cert: '/etc/ssl/certs/ssl-cert-snakeoil.pem' ssl_key: '/etc/ssl/private/ssl-cert-snakeoil.key' port: '443' docroot: '/var/www' aliases: - alias: '/content/' path: '/usr/share/graphite-web/static/' directories: - provider: 'location' path: '/content/' sethandler: 'None' # Note: %%{} is required for escaping custom_fragment: | WSGIProcessGroup _graphite WSGIDaemonProcess _graphite processes=5 threads=5 display-name='%%{}{GROUP}' inactivity-timeout=120 user=_graphite group=_graphite WSGIImportScript /usr/share/graphite-web/graphite.wsgi process-group=_graphite application-group=%%{}{GLOBAL} WSGIScriptAlias / /usr/share/graphite-web/graphite.wsgi |
Damit haben wir nun Graphite am Start und nachdem Icinga2 neugestartet wurde (falls noch nicht geschehen), dauert es auch nicht lang, bis in dem Ordner /var/lib/graphite/whisper/ "icinga2" auftaucht. Damit sind dann die Werte auch über die Webseite darstellbar. Dank Puppet wurde auch alles konfiguriert, um die Graphen auch in icingaweb2 einzubinden.
Icinga2 Checks - Basics
Bevor wir den Webserver einbinden, möchte ich kurz auf das Konstrukt eingehen, wie Icinga2 mit Puppet einhergeht. Das Puppet Manifest legt folgende Ordner an:
- /etc/icinga2/zones.d/global-templates/ -> Dateien dort werden auf allen Icinga2 Instanzen verteilt
- /etc/icinga2/zones.d/master/ -> Dateien die nur den Icinga-Master betreffen
- /etc/icinga2/conf.d/commands/ -> Eigene Checkkommandos
- /etc/icinga2/conf.d/services/ -> Services
- /etc/icinga2/conf.d/notifications -> Benachrichtigungen
- /etc/icinga2/conf.d/hosts -> Hosts
Wird ein neuer Host zu Puppet hinzugefügt und hat das entsprechende Icinga Profil eingebunden, so wird außer Icinga2 zu installieren auch die Information an die PuppetDB weitergegeben. Wird danach der Puppet Agent auf dem Icinga Master aufgerufen, holt sich dieser wiederum alles aus der Datenbank, um einen neuen Host zu Icinga2 hinzuzufügen. Puppet muss in diesem Fall daher zweimal aufgerufen werden:
- Auf dem neu zu überwachenden Host
- Auf dem Icinga2 Master
Das Gleiche gilt auch bei Änderungen an den Icinga2 Variablen wie zum Beispiel host.var.foo, die keinerlei Änderungen an dem Host selbst durchführen. Da die Variable dem Host zugewiesen wird, kann der Icinga2 Master noch nichts davon wissen, erst nachdem der Puppet Agent diese Information in der zentralen Datenbank abgelegt hat.
Icinga2 Checks - Checkcommands
In dieser Datei werden eigene Checkcommands untergebracht. Das Beispiel hier entstammt meiner erste Testumgebung und soll lediglich aufzeigen, wie so etwas aussehen kann. Es gibt dabei zwei Besonderheiten:
- Es gibt statische Checks, die lediglich auf dem Icinga2 Master zum Einsatz kommen, wie zum Beispiel check_nwc_health mit samt sämtlicher Perl Module die notwendig sind.
- Checks die in der Datenbank abgelegt werden, sind leicht an dem "@@" zu erkennen. Diese Checks können damit auch von anderen Puppet Instanzen bezogen werden, da sie exportiert werden.
Programme/Tools etc. die für einen Checkkommando benötigt werden, habe ich in dem Ordner modules/icinga_checks/files/checks/ abgelegt, wenn es dafür keine Debian Pakete gibt. Dort finden sich z.B. check_iostat, oder auch check_nwc_health. Die exportieren Checkkommandos finden sich dann in zones.d/global-templates/ wieder.
- modules/profile/manifests/icinga2/server.pp:
... # contain profile::icinga2::notifications contain profile::icinga2::checkcommands ... |
- modules/profile/manifests/icinga2/checkcommands.pp:
# class profile::icinga2::checkcommands # modules/profile/manifests/icinga2/checkcommands.pp class profile::icinga2::checkcommands { # installing packages $checkdeps = [ 'libnet-snmp-perl','libcrypt-hcesha-perl', 'libcrypt-des-perl','libdigest-hmac-perl','libcrypt-rijndael-perl', 'sysstat','smartmontools','libconfig-json-perl','libredis-perl' ] package { $checkdeps: ensure => installed, } file { '/usr/lib/nagios/plugins/check_nwc_health': ensure => file, mode => '755', owner => 'root', group => 'root', source => [ "puppet:///modules/icinga_checks/checks/check_nwc_health", ], } file { '/usr/lib/nagios/plugins/check_iostat': ensure => file, mode => '755', owner => 'root', group => 'root', source => [ "puppet:///modules/icinga_checks/checks/check_iostat", ], require => Package['sysstat'], } file {'/usr/lib/nagios/plugins/check_icmp': ensure => file, mode => '4755', owner => 'root', group => 'root', } file {'/usr/lib/nagios/plugins/check_ping': ensure => file, mode => '4755', owner => 'root', group => 'root', } file {'/usr/lib/nagios/plugins/contrib': ensure => directory, mode => '755', owner => 'root', group => 'root', } @@icinga2::object::checkcommand { 'smart_attributes': import => [ 'plugin-check-command', ], command => [ 'PluginDir + /contrib/check_smart_attributes/check_smart_attributes', ], arguments => { '-d' => '$smart_device$', '-dbj' => '$smart_dbj$', '-ucfgj' => '$smart_ucfgj$', '-nosudo' => '$smart_nosudo$', }, vars => { 'smart_device' => '/dev/sda', 'smart_dbj' => '/usr/lib/nagios/plugins/contrib/check_smart_attributes/check_smartdb.json', }, target => '/etc/icinga2/zones.d/global-templates/check-smart-attributes-command.conf', } # Ceph Checks @@icinga2::object::checkcommand { 'ceph_health': import => [ 'plugin-check-command', ], command => [ 'PluginDir + /check_ceph_health', ], arguments => { '-e' => '$ceph_bin$', '-c' => '$ceph_conf$', '-m' => '$ceph_mon_address$', '-i' => '$ceph_client_id$', '-n' => '$ceph_client_name$', '-k' => '$ceph_client_keyring$', '-w' => '$ceph_whitelist_health_regex$', '-d' => '$ceph_health_detail$', }, vars => { 'ceph_bin' => '/usr/bin/ceph', 'ceph_conf' => '/etc/ceph/ceph.conf', 'ceph_client_id' => 'nagios', 'ceph_client_keyring' => '/var/lib/nagios/ceph-nagios.keyring', }, target => '/etc/icinga2/zones.d/global-templates/check-ceph-health-command.conf', } @@icinga2::object::checkcommand { 'ceph_mon': import => [ 'plugin-check-command', ], command => [ 'PluginDir + /check_ceph_mon', ], arguments => { '-e' => '$ceph_bin$', '-c' => '$ceph_conf$', '-m' => '$ceph_mon_address$', '-I' => '$ceph_mon_id$', '-i' => '$ceph_client_id$', '-k' => '$ceph_client_keyring$', }, vars => { 'ceph_bin' => '/usr/bin/ceph', 'ceph_conf' => '/etc/ceph/ceph.conf', 'ceph_client_id' => 'nagios', 'ceph_client_keyring' => '/var/lib/nagios/ceph-nagios.keyring', }, target => '/etc/icinga2/zones.d/global-templates/check-ceph-mon-command.conf', } @@icinga2::object::checkcommand { 'ceph_osd': import => [ 'plugin-check-command', ], command => [ 'PluginDir + /check_ceph_osd', ], arguments => { '-e' => '$ceph_bin$', '-c' => '$ceph_conf$', '-m' => '$ceph_mon_address$', '-I' => '$ceph_osd_id$', '-H' => '$ceph_osd_host$', '-i' => '$ceph_client_id$', '-k' => '$ceph_client_keyring$', '-o' => '$ceph_osd_out$', }, vars => { 'ceph_bin' => '/usr/bin/ceph', 'ceph_conf' => '/etc/ceph/ceph.conf', 'ceph_client_id' => 'nagios', 'ceph_client_keyring' => '/var/lib/nagios/ceph-nagios.keyring', 'ceph_osd_host' => '$address$', }, target => '/etc/icinga2/zones.d/global-templates/check-ceph-osd-command.conf', } @@icinga2::object::checkcommand { 'ceph_rgw': import => [ 'plugin-check-command', ], command => [ 'PluginDir + /check_ceph_rgw', ], arguments => { '-c' => '$ceph_conf$', '-e' => '$radosgw_admin_bin$', '-d' => '$ceph_perf_detail$', '-B' => '$ceph_perf_byte$', '-i' => '$ceph_client_id$', }, vars => { 'radosgw_admin_bin' => '/usr/bin/radosgw-admin', 'ceph_conf' => '/etc/ceph/ceph.conf', }, target => '/etc/icinga2/zones.d/global-templates/check-ceph-rgw-command.conf', } @@icinga2::object::checkcommand { 'ceph_df': import => [ 'plugin-check-command', ], command => [ 'PluginDir + /check_ceph_df', ], arguments => { '-e' => '$ceph_bin$', '-c' => '$ceph_conf$', '-m' => '$ceph_mon_address$', '-i' => '$ceph_client_id$', '-n' => '$ceph_client_name$', '-k' => '$ceph_client_keyring$', '-d' => '$ceph_pool_detail$', '-W' => '$ceph_pool_warning$', '-C' => '$ceph_pool_critical$', }, vars => { 'ceph_bin' => '/usr/bin/ceph', 'ceph_conf' => '/etc/ceph/ceph.conf', 'ceph_client_id' => 'nagios', 'ceph_client_keyring' => '/var/lib/nagios/ceph-nagios.keyring', 'ceph_pool_warning' => '85', 'ceph_pool_critical' => '95', }, target => '/etc/icinga2/zones.d/global-templates/check-ceph-df-command.conf', } } |
Icinga2 Checks - Services / applyrules
Im Gegensatz zu der offiziellen icinga2-puppet Anleitung, wird für die Services nicht notwendigerweise auf auf Puppet spezifische Konstrukte (icinga2::object::service) verwendet. Der Autor von Example3 hat als Grund dafür angegeben, dass mit dem icinga2-puppet einfach noch nicht alles möglich ist, was Icinga selbst kann, mit seinen diversen Macros und Variablen. Daher werden die services / applyrules einfach per Datei verteilt, allerdings auch exportiert.
Die Dateien dazu liegen bei mir unter: modules/icinga_checks/files/services/
- modules/profile/manifests/icinga2/server.pp:
... # Define apply rules that contain profile::icinga2::applyrules ... |
- modules/profile/manifests/icinga2/applyrules.pp:
# classes profile::icinga2::applyrules # modules/profile/manifests/icinga2/applyrules.pp class profile::icinga2::applyrules { # We attempt to export them with the respective services where possible. # However, that only works if the service is unique on the infrastructure and would # not lead to duplicate resources. # # All multi-use (apply) services are defined here. # # We do not use "icinga2::object::service" but files with the "icinga2::config::file" tag. See the # example's README on why this is the case. file { '/etc/icinga2/conf.d/services/check_nwc_health.conf': ensure => file, owner => nagios, group => nagios, tag => 'icinga2::config::file', source => [ "puppet:///modules/icinga_checks/services/service_check_nwc_health.conf", ], } file { '/etc/icinga2/conf.d/services/check_ping.conf': ensure => file, owner => nagios, group => nagios, tag => 'icinga2::config::exported', source => [ "puppet:///modules/icinga_checks/services/service_check_ping.conf", ], } file { '/etc/icinga2/conf.d/services/check_iostat.conf': ensure => file, owner => nagios, group => nagios, tag => 'icinga2::config::exported', source => [ "puppet:///modules/icinga_checks/services/service_check_iostat.conf", ], } file { '/etc/icinga2/conf.d/services/service_check_ntp.conf': ensure => file, owner => nagios, group => nagios, tag => 'icinga2::config::exported', source => [ "puppet:///modules/icinga_checks/services/service_check_ntp.conf", ], } # Collect any files exported and tagged elsewhere (can be created inside # services or master zone) # We need to use a different tag then icinga itself (icinga2::config::file) # or the agent will try to collect any resources tagged so on himself. File <<| ensure != 'directory' and tag == 'icinga2::config::exported' |>> { require => [ File['icinga2_masterzone'], File['icinga2_services'], ], } } |
- modules/icinga_checks/files/services/service_check_ping.conf:
# Managed by Puppet # modules/icinga_checks/files/services/service_check_ping.conf apply Service "ping4" { import "generic-service" check_command = "ping4" assign where host.address ignore where host.vars.noping4 } apply Service "ping6" { import "generic-service" check_command = "ping6" assign where host.address6 ignore where host.vars.noping6 } |
- modules/icinga_checks/files/services/service_check_iostat.conf:
# Mangaged by puppet # modules/icinga_checks/files/services/service_check_iostat.conf apply Service for (blockdevice => config in host.vars.blockdevices) { import "generic-service" check_command = "iostat" vars += config display_name = "iostat " + blockdevice vars.instance = blockdevice if (host.name != NodeName) { command_endpoint = host.name } assign where host.vars.os == "Linux" ignore where host.vars.noagent } |
- modules/icinga_checks/files/services/service_check_ntp.conf:
# Mangaged by puppet # modules/icinga_checks/files/services/service_check_ntp.conf apply Service "ntp-time" { import "generic-service" check_command = "ntp_time" if (host.name != NodeName) { command_endpoint = host.name } assign where host.vars.distro == "Debian" ignore where host.vars.noagent } |
- modules/icinga_checks/files/services/service_check_nwc_health.conf:
# Mangaged by puppet # modules/icinga_checks/files/services/service_check_nwc_health.conf apply Service "if-" for (interface_name => interface_config in host.vars.interfaces) { import "generic-service" display_name = "IF-" + interface_name enable_notifications = 0 notes = interface_config.description check_command = "nwc_health" vars.instance = interface_name vars.nwc_health_mode = "interface-usage" vars.nwc_health_name = interface_name vars.nwc_health_timeout = 120 check_interval = 300 retry_interval = 300 vars += interface_config } apply Service "cpu" { import "generic-service" display_name = "CPU-Load" enable_notifications = 0 check_command = "nwc_health" vars.nwc_health_mode = "cpu-load" vars.nwc_health_timeout = 120 assign where host.vars.load == "snmp" } apply Service "memory" { import "generic-service" display_name = "memory usage" enable_notifications = 0 check_command = "nwc_health" vars.nwc_health_mode = "memory-usage" vars.nwc_health_timeout = 120 assign where host.vars.mem == "snmp" } apply Service "interface-available" { import "generic-service" display_name = "Interfaces available" enable_notifications = 0 check_interval = 28800 retry_interval = 30000 check_command = "nwc_health" vars.nwc_health_timeout = 120 vars.nwc_health_mode = "interface-availability" assign where host.vars.intcheck == "snmp" } apply Service "uptime" { import "generic-service" display_name = "uptime" enable_notifications = 0 check_command = "nwc_health" vars.nwc_health_mode = "uptime" vars.nwc_health_timeout = 120 assign where host.vars.uptime == "snmp" } |
Sollte ein neustart von Icinga im übrigen fehlschlagen, könnte es daran liegen, dass Check oder Services bereits vorhanden sind. Dies trifft zum Beispiel auf check_ping zu, der bereits in der icinga2/conf.d/services.conf steck. Dann entweder die Datei unschädlich machen, oder nur den Inhalt auskommentieren.
Icinga2 Checks - Hosts ohne Icinga2
Sind Hosts zu prüfen, auf denen kein Icinga2 laufen kann, genügen folgende Zeilen:
- modules/profile/manifests/icinga2/server.pp:
... $myIcinga2Hosts = hiera('icinga2::object::host', {}) create_resources( 'icinga2::object::host', $myIcinga2Hosts) ... |
Damit lassen sich nun Hosts in Hiera erstellen, die dem Icinga Master zugewiesen werden:
- hieradata/node/icinga-master-01.4lin.net.yaml:
# Only host specified things --- icinga2::object::host: www.heise.de: target: '/etc/icinga2/zones.d/master/www.heise.de.conf' display_name: 'Heise.de' address: '193.99.144.80' notes: 'Heise Verlag' notes_url: 'https://heise.de' check_command: 'hostalive' vars: os: 'None' http_vhosts: 'www.heise.de': http_vhost: 'www.heise.de' http_ssl: 'true' |
Dadurch dass in der hiera.yaml definiert wurde, den Ordner node/ nach <fqdn>.yaml zu durchsuchen (::fqdn), findet Puppet nun unsere neu angelegt Datei und kann somit die Icingakonfiguration erstellen, die dann unter zones.d/master/www.heise.de.conf zu finden ist:
# This file is managed by Puppet. DO NOT EDIT. object Host "www.heise.de" { address = "193.99.144.80" display_name = "Heise.de" check_command = "hostalive" notes = "Heise Verlag" notes_url = "https://heise.de" vars.os = "None" vars.http_vhosts["www.heise.de"] = { http_vhost = "www.heise.de" http_ssl = true } } |
In diesem Fall, kann man mittels des check_http und einer passenden Applyrule sämtliche Vhost prüfen :-) Auf die gleiche Weise können SNMP Hosts etc. eingebunden werden.
Icinga2 Checks - Hosts mit Icinga2
Kommen wir zu unserem Webserver. Jeder Host der mittels Icinga Agent überwacht werden soll, muss die Rolle monitorednode eingebunden haben. Des Weiteren ist es wichtig zu wissen, dass für die Verschlüsselung zwischen den Icinga2 Instanzen die Zertifikate von Puppet verwendet werden.
Das nachfolgende Beispiel beruht wieder zu fast 100% auf Example3 :-)
Beachten: Da ich nun schon zweimal darauf reingefallen bin, hier ein Hinweis: Am Ende der agent.pp findet ihr die Zeile "Icinga2::Object::Endpoint". Dort muss euer Icinga2 Master Hostname stehen, oder ein Äquivalent. Passt der Eintrag nicht, so wird auf der Agent Seite in der icinga2/zone.conf der Endpoint fehlen. Damit startet Icinga natürlich nicht, da er nicht weiß, wie er den Master erreichen kann.
- Alles was für den Agent notwendig ist:
- modules/profile/manifests/icinga2/agent.pp:
class profile::icinga2::agent { # By default, the icinga module only installs monitoring-plugins-base ensure_packages([ 'monitoring-plugins-standard', 'nagios-plugins-contrib', 'libmonitoring-plugin-perl', 'sysstat', ], { install_options => ['--no-install-recommends'], }) # Options valid for all agents, thus defined inside the manifest class { '::icinga2': manage_repo => false, confd => false, features => [ 'checker','mainlog' ], } # Leave this here or put it in a yaml file common # to icinga agent nodes only. class { '::icinga2::feature::api': pki => 'puppet', accept_config => true, accept_commands => true, endpoints => {}, zones => {}, } icinga2::object::zone { 'global-templates': global => true, } # All nodes export resources for icinga monitoring # The vars (set in the various nodes hiera files) are used to Apply Services # to these hosts. (See profile::icinga::server) @@::icinga2::object::host { $::fqdn: display_name => $::fqdn, address => $::ipaddress, check_command => 'hostalive', vars => hiera_hash('icinga_vars', {}), target => "/etc/icinga2/zones.d/master/${::fqdn}.conf" } # Create virtual resources for this agent node @@::icinga2::object::endpoint { "$::fqdn": host => "$::ipaddress", } @@::icinga2::object::zone { "$::fqdn": endpoints => [ "$::fqdn", ], parent => 'master', } # Collect and realize info about self and master, but no other nodes. Icinga2::Object::Endpoint <<| title == $::fqdn or title == 'icinga-master-01.4lin.net' |>> { } Icinga2::Object::Zone <<| title == $::fqdn or title == 'master' |>> { } # ::icinga2::pki::puppet } |
- Die Rolle monitorednode die das Profil icinga::agent einbindet
- modules/role/manifests/monitorednode.pp:
class role::monitorednode { contain profile::icinga2::agent } |
- Unsere Rolle für den Webserver
- modules/role/manifests/webserver.pp:
# class role::webserver # modules/role/manifests/webserver.pp class role::webserver inherits role::monitorednode { contain profile::nginx::server } |
- Wir wollen Nginx
- Fehlende Module nachinstallieren:
root@puppet-master # puppet module install puppet-nginx root@puppet-master # puppet module install puppet-collectd |
- modules/profile/manifests/nginx/server.pp:
# class profile::nginx::server # modules/profile/manifests/nginx/server.pp class profile::nginx::server { # Needed for Nginx Repo package { ['apt-transport-https']: ensure => $ensure, } # This profile can be used by many nodes and thus the node configuration is # in the hiera file for the respective node! class { '::nginx': manage_repo => true, package_source => 'nginx-stable' }-> class { '::collectd::plugin::nginx': url => 'http://localhost:8433/nginx-status', } # Icinga: install check into PluginContribDir # (PluginContribDir could be a fact "icinga2 variable get PluginContribDir", # but for that to work, puppet would probably have to run twice…) file { '/usr/lib/nagios/plugins/check_nginx_status.pl': ensure => file, mode => '+x', source => [ 'puppet:///modules/1024/icinga/plugins/check_nginx_status.pl', ], require => Package['monitoring-plugins-standard'], } } |
- Zum Schluss die Hiera Node
- hieradata/node/web-01.4lin.net.yaml:
--- classes: - 'role::webserver' icinga_vars: client_endpoint: "%{::fqdn}" role: http-server vhosts: web.4lin.net: uri: '/ping' checks: 'nginx-status': nginx_status_host_address: '127.0.0.1' nginx_status_port: '8433' nginx_status_url: '/nginx-status' nginx_status_warn: '100,50,100' nginx_status_critical: '200,100,200' } |
Im Grunde was es das nun. Nun kann man den Puppet Agent auf web-01.4lin.net genauso einrichten, wie bereits schon beim icinga-master-01.4lin.net. Also zuerst das PC1 Repository von Puppetlabs einbinden, das passende Paket installiere und die puppet.conf anpassen. Natürlich muss dann noch das Zertifikat unterschrieben werden, bevor der richtige Puppetlauf starten kann. Am Ende des Durchlaufs kann der Puppet Agent auf dem Icinga Master gestartet werden, der darauf hin ein passendes Icinga2 Host Objekt erzeugt.
Profil InfluxDB
Weil Graphite eben keine Schönheit ist, wollen wir stattdessen Grafana. Um Grafana in vollem Umfang genießen zu können, benötigen wir dafür InfluxDB, welches von Icinga2 nativ gefüttert werden kann.
- modules/role/manifests/icinga2_server.pp:
... contain profile::influxdb::influxdb |
- modules/profile/manifests/influxdb/influxdb.pp:
# Class profile::influxdb::influxdb # modules/profile/manifests/influxdb/influxdb.pp class profile::influxdb::influxdb { class { 'influxdb::server': meta_bind_address => "${::fqdn}:8088", meta_http_bind_address => "${::fqdn}:8091", http_bind_address => "${::fqdn}:8086", } } |
- Puppet Module für Influx:
root@puppet-master # puppet module install golja-influxdb |
- hieradata/role/icinga-master.yaml:
... ############# Influx settings ############# influxdb::params:http_pprof_enabled: true ... ############# Icinga2 settings ############# ... icinga2::features: - 'influxdb' ... icinga2::feature::influxdb::enable_send_thresholds: true icinga2::feature::influxdb::enable_send_metadata: true icinga2::feature::influxdb::host: "graph.%{::domain}" icinga2::feature::influxdb::host_tags: fqdn: '$host.name$' hostname: '$host.vars.hostname$' domain: '$host.vars.domain$' icinga2::feature::influxdb::service_tags: fqdn: "$host.name$" hostname: '$host.vars.hostname$' domain: "$host.vars.domain$" service: "$service.name$" instance: "$service.vars.instance$" |