💾 Archived View for senioradmin.de › SimpleConfigManagement.gmi captured on 2021-12-04 at 18:04:22. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
Als Server alle noch einzeln physikalisch und Virtualisierung und Container nur akademische Themen waren, sah der Admin-Alltag so aus:
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
und
. 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.
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.
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.
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
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:
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 ...
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.
Dies ist sehr einfach, man legt einfach mehrere Textdateien mit Hostnamen an, diese können dann (z.b. `cat Inventoryfile`) an Efs2 übergeben werden.
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.
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"
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%.*} $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.