Simple Configuration Management
senioradminSimple Configuration Management
Was ist Konfigurationsmanagement?
Als Server alle noch einzeln physikalisch und Virtualisierung und Container nur akademische Themen waren, sah der Admin-Alltag so aus:
- Per SSH auf einen der wenigen Server einloggen
- Administrative Aufgaben erledigen
- Ausloggen
Dann kam die Virtualisierung und die Anzahl der (nun virtuellen) Hosts stieg stark an. Jeder dieser Hosts wollte auch noch gehegt und gepflegt werden. Das überlastete die Admins natürlich.
Es erschienen erste Tools, die es erlaubten Aufgaben parallel auszuführen, wie Parallel SSH und Cluster SSH. Damit konnten Admins Befehle gleichzeitig an mehrere Server senden. Das war schon eine Erleichterung, aber noch immer mussten die Befehle einzeln eingegeben und ausgeführt werden. Bei einem Fehler hatte dies nun erheblich größere Auswirkungen - ein Leerzeichen zu viel beim rm Befehl hat nun alle Server gelöscht, nicht nur einen.
Findige Admins haben ihre Standard-Aufgaben daher schon früh in ein Shellscript gegossen. Diese Skripte waren oft hochspezialisiert auf die jeweilige Aufgabe angepasst und oft wussten nur die Admins, die sie geschrieben hatten, wie sie aufzurufen waren und was sie taten.
Nun haben einige Leute gemerkt, dass es besser wäre, diese Skripte zu verallgemeinern und portabel zu machen. Die ersten Config-Management-Tools kamen auf dem Markt. Ein Pionier war CFEngine, welches bereits 1993 erschien. Aber richtig los ging es erst mit Anbruch des Virtualisierungszeitalters. 2005 erschien Puppet, 2009 Chef, 2011 Saltstack und 2012 Ansible.
Vom Admin zum DevOps
Mit diesen neuen Tools konnte so gut wie alles automatisiert werden. Admins haben ihre Aufgaben nun nicht mehr manuell ausgeführt, sondern geskriptet. Man kann den kompletten Lebenszyklus eines Servers in ein Skript gießen. Damit ähnelten Admins nun eher Entwicklern, sie wurden zum “DevOps”, die Server nun nicht mehr selbst aufsetzen und administrieren, sondern die gesamte Infrastruktur als Software programmierten, “Infrastructure as Code” (IaC) ist das Schlagwort, was seit einigen Jahren die Runde macht.
Die ersten Tools brachten für die Aufgaben ihre eigene Programmiersprache (Domain Specific Language) mit, im Fall von Puppet und Chef ist diese an Ruby angelehnt. Das war für viele kompliziert. Die beiden moderneren Tools, Ansible und Salt, nutzen daher YAML, eine Auszeichnungssprache, welche dafür gedacht ist, Daten in einfacher Form zu strukturieren.
Nun ist eine Auszeichnungssprache für Daten keine Skriptsprache und Ansible wendet allerlei Tricks an, um mit YAML logische Programmieraufgaben zu lösen. Einige behaupten gar, mit Ansible ist YAML turing-vollständig.
Etabliertes Konfigurationsmanagement
Software-Entwickler neigen dazu, Komplexität hinter Layern und Kapselungen zu verbergen. So wird die Übersicht behalten und man muss sich nicht mehr mit den Details hinter den gekapselten Funktionen beschäftigen. Admins auf der anderen Seite ist wichtig, dass Systeme stabil und zuverlässig laufen, daher werden Tools mit simpler Funktionalität bevorzugt.
Die etablierten Config-Management Systeme haben viele Standardfunktionen gekapselt. Dies kommt der Denkweise in der Softwareentwicklung entgegen: Man sucht in der API die entsprechende Funktion und muss diese dann mit den entsprechenden Parametern aufrufen. Um z. B. einen Host anzupingen muss folgendes aufgerufen werden.
# Salt (SSH)
salt-ssh -i 'host' test.ping
# Ansible
ansible host -m ping
In der Regel wird dazu außerdem ein Inventory benötigt, also ein Inventar. Dies ist eine Datei mit einer Liste der zu administrierenden Server.
Was beim Ping noch relativ einfach aussieht, kann recht schnell komplex werden. Als einfache Aufgabe sei hier mal die Installation eines Webservers genannt. Dieser soll konfiguriert werden und eine einfache HTML-Seite anzeigen. In Salt sieht dies so aus:
apache:
pkg.installed: []
service.running:
- watch:
- file: /etc/httpd/vhost.conf
- require:
- pkg: apache
/var/www/index.html:
file:
- managed
- source: salt://webserver/index.html
- require:
- pkg: apache
/etc/httpd/vhost.conf:
file.managed:
- source: salt://webserver/vhost.conf
und in Ansible
- hosts: host
tasks:
- name: Install Apache
package:
name=apache
state: present
- name: Copy index test page
copy:
src: "files/index.html"
dest: "/var/www/index.html"
- name: Copy vhost config
copy:
src: "files/vhost.conf
dest: "/etc/httpd/vhost.conf"
notify: Reload Apache
handlers:
- name: Reload Apache
service:
name: apache2
state: reloaded
Nicht ganz intuitiv. Wie würde man als klassischer Admin vorgehen? Doch etwa so:
ssh host pkg install apache; service apache start
scp webserver/index.html host:/var/www/index.html
scp webserver/vhost.conf host:/etc/httpd/vhost.conf
ssh host service apache reload
Diese Vorgehensweise ist für die klassische Administration einfach zu verstehen. Sie in eine für ein Config-Management-System angebrachte Weise zu formulieren ist aufwändig. Es muss herausgefunden werden, welches Modul für die jeweilige Funktion geeignet ist und wie die gewünschte Funktion aufgerufen wird und dann muss das ganze in eine syntaktisch korrekte Form gebracht werden. Ich garantiere, dass jeder mehr als einmal über Fehler stolpern wird, weil die Einrücktiefe in YAML nicht korrekt gesetzt war.
Simples Konfigurationsmanagement = Effizientes Konfigurationsmanagement
Zum Glück habe sich auch schon andere Menschen gedacht “das muss doch einfacher gehen”. Ich bin dann auf das Config-Management-Tool Efs2 gestoßen. Dieses findet man unter https://efs2.sh
Efs2 ist in Go geschrieben und besteht aus einem einzigen Binary. Zum schreiben der Tasks wird eine einfaches Format genutzt,welches an Dockerfiles angelehnt ist. Die Aufgabe “Webserver” von oben sähe in diesem Format wie folgt aus
# Efs2file
RUN pkg install apache; service apache start
PUT webserver/index.html /var/www/index.html 0440
PUT webserver/vhost.conf /etc/httpd/vhost.conf 0440
RUN service apache reload
Als klassischer Admin muss man da nicht viel umdenken. Aufgerufen wird dies mit dem Kommando efs2 host
, wobei multiple Hosts angegeben werden können. Ein Inventory
ist nicht nötig (aber mit einfachen Shellmitteln durchaus möglich). Die Verbindung wird über das SSH-Protokoll hergestellt, wobei natürlich
ein SSH-Schlüssel angegeben werden kann. Ein paralleles Ausführen ist möglich - und es ist wirklich schnell.
Insgesamt gibt es drei Kommandos:
- RUN - führt den Befehl auf dem Zielhost aus
- RUN SCRIPT - lädt das angegebene Script auf den Zielhost, führt es dort aus und löscht es dann wieder
- PUT - kopiert die angegebene Datei auf den Zielhost (mit Angabe der Berechtigungen in Octal)
Ich habe nicht mal eine Stunde gebraucht, um die Anweisungen für das Aufsetzen eines Debian-Servers inklusive einer Reihe von Diensten mit Konfiguration und Absicherung mit Paketfiltern und Blocklisten so zu automatisieren. In Ansible oder Salt hätte ich dafür ein vielfaches dieser Zeit benötigt - und ich wäre mir nicht sicher, ob das Ergebnis gut ist, denn wer weiß schon, ob man unter den hunderten verfügbaren Modulen und tausenden Funktionen das Richtige ausgewählt hat? Von dem Frust beim Kampf mit YAML ganz zu schweigen …
Grenzen und wie man sie ausdehnt
Ein ausgewachsenes Config-Management wie Ansible bietet sehr viele Möglichkeiten, die ein einfaches Tool wie Efs2 nicht bieten kann. Bei Ansible und Co steckt die Programmierlogik in den Taskbeschreibungen (Playbooks bzw. Statefiles). Diese werden oft deklarativ geschrieben, d.h. es wird ein Endzustand beschrieben, der erreicht werden soll. Welche Schritte dafür nötig sind, das soll das System selbst herausfinden.
Bei Efs2 muss man die Logik wieder selbst verwalten, z.B. in Skripte. Diese sind in ihrer Natur imperativ, d.h. es muss Schritt für Schritt beschrieben werden, wie das Ergebnis erreicht werden soll.
Beide Methoden, imperativ und deklarativ, haben Vor- und Nachteile. Der imperative Weg ist manchmal aufwändiger, aber auch flexibler. Deklarative Beschreibungen überlassen den Weg der Maschine, welche oft einen generischen Weg wählt, der nicht der effizienteste sein muss.
Idempotenz beschreibt, dass auch nach mehrfacher Ausführung immer das gleiche Ergebnis herauskommt und ist so was wie der heilige Gral des Konfigurationsmanagements. Diese wäre mit Efs2 automatisiert nur mit hohem Aufwand zu erreichen (aber auch bei den etablierten Systemen ist sie nicht immer gegeben).
Andere Dinge, die man von einem großen System vielleicht kennt, können relativ leicht nachgebildet werden.
Inventory
Dies ist sehr einfach, man legt einfach mehrere Textdateien mit Hostnamen an, diese können dann (z.b. cat Inventoryfile
) an Efs2 übergeben
werden.
Templates und Variablen
Ansible und Salt nutzen beide Jinja als Template-Engine. Damit können generische Vorlagen erstellt werden, die je nach Zielhost oder Hostgruppe angepasst werden können. Ansonsten bliebe nur das hardcoden.
Efs2 selbst nutzt zwar keine Template-Engine, aber gemäß dem Unix-Prinzip, dass jedes Tool genau eine Aufgabe erfüllen soll, spricht nichts dagegen, eine eigenständige Template-Engine zu nutzen. Im Netz bin ich auf ESH gestoßen, zu finden unter https://github.com/jirutka/esh
Dies ist eine einfache Shell-basierte Template-Engine, die man vor dem jeweiligen Aufruf von Efs2 starten kann.
Beispiel
Und hier nun ein Praxis-Beispiel. Gegeben sei eine Gruppe von Servern, die mit Paketfilterregeln (iptables) abgesichert werden sollen. Das Debian-Paket “iptables-persistent” dient dazu, die Regeln automatisch bei jedem booten zu laden. Der SSH-Port ist variabel, eine Gruppe von definierten Hosts soll unbeschränkten Zugriff haben.
Zunächst legen wir eine Datei “Vars” mit Variablen an.
# Vars
FWACCEPTIP=1.2.3.4
FWACCEPTIP=5.6.7.8
FWACCEPTIP=9.10.11.12
SSHPort=2222
Außerdem ein Inventoryfile “Inventory"
host1.example.com
host2.example.com
host3.example.com
Das Template für die Paketfilterregeln “rules.v4.esh"
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [18299:1945378]
-A INPUT -i lo -j ACCEPT
-A INPUT -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport <%= $(grep ^SSHPort= Vars|cut -d= -f2) %> -j ACCEPT
<% for i in `grep ^FWACCEPTIP= Vars|cut -d= -f2`; do -%>
<% echo "-A INPUT -s /32 -i eth0 -j ACCEPT" -%>
<% done -%>
-A INPUT -i eth0 -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A INPUT -i eth0 -j REJECT --reject-with icmp-port-unreachable
COMMIT
Das “E2fsfile"
RUN DEBIAN_FRONTEND=noninteractive apt-get -yq install iptables-persistent; mkdir -p /etc/iptables/
PUT rules.v4 /etc/iptables/rules.v4 0644
RUN iptables-restore < /etc/iptables/rules.v4
Und schließlich das Skript “runefs2.sh”, welches alles aufruft
#!/bin/bash
KEYFILE="/home/user/.ssh/id_ed25519"
SSHPORT=$(grep ^SSHPort= Vars|cut -d= -f2)
USR=root
if [ "$1" = "" ]; then
echo "Usage $0: inventory-file"
exit 1
fi
IFILE="$1"
if [ ! -f "$IFILE" ]; then
echo "Inventory file $IFILE not found"
exit 1
fi
# Processing Templates
for ESH in *.esh; do
esh -o $ESH
done
# Reading inventory in variable INV
INV=`cat $IFILE|tr '\n' ' '`
echo "Press RETURN for running Efs2 with the following inventory"; echo
echo "$INV"; echo
echo "Ctrl-C to abort"
read n
# Running efs2
efs2 --user=$USR --port=$SSHPORT -i $KEYFILE $INV
Alles was nun getan werden muss ist, die Datei “Vars” vor jedem Aufruf anzupassen, ein Inventory zu erstellen und dann runefs2.sh Inventoryfile
aufzurufen.