Von Welchen, die auszogen das Fürchten zu verlernen

Frank
05.07.2019 0 7:12 min

Wie bei den meisten anderen Unternehmen gehört es auch bei uns zu den täglichen Aufgaben unsere Server mit aktuellen Softwareversionen zu versorgen und so Sicherheitslücken zu schließen, bevor sie ausgenutzt werden können.

Der Zustand vorher

Unser Umgang damit sah lange Zeit folgendermaßen aus:

  1. Ein Automatismus ermittelt aus dem RSS Feed für Debian GNU/Linux DSAs aktuelle Paketversionen und prüft, welche Binärpakete vom im Feed adressierten Sourcepaket abgeleitet sind. Nun prüft der Automatismus, auf welchen Servern diese Binärpakete installiert sind und erzeugt aus all diesen Informationen eine Anfrage in unserem Ticketsystem.
  2. Das Ticket wird inhaltlich von einem Menschen geprüft und es wird entschieden, ob das Update eingespielt wird.
  3. Zur Installation der Updates existiert ein weiteres Script, das hilft die zu aktualisierenden Pakete zu installieren.
  4. Wir prüfen, welcher Dienst einen Neustart braucht und führen diesen durch.

Wie geht das besser?

Das hat lange ganz gut funktioniert, passte aber nicht besonders zu unserer Strategie manuelle Arbeit zu vermeiden und durch Mechanismen zu ersetzen, die ein deterministisches Ergebnis hinterlassen. In Debian GNU/Linux gibt es ein Paket mit dem vielversprechenden Namen ‘unattended-upgrades’, aber:

  1. Das Neustarten von Diensten ist in diesem Fall ‘Paketsache’, das heisst, dass der Maintainer seinem Paket den Restart als erwünschtes Verhalten konfiguriert haben muss.
  2. Bei Kernelupdates kann zwar automatisch neu gestartet werden, weil in diesem Fall die Datei ‘/var/run/reboot-required’ angelegt wird, die wiederum vom Debian-eigenen unattanded-upgrades Mechanismus beachtet werden und einen Neustart auslösen kann, aber bei microcode updates, die in der jüngeren Vergangenheit häufig notwendig waren, um Sicherheitslücken in CPUs zu patchen wird diese Datei z.B. nicht angelegt obwohl ein Neustart notwendig wäre, um den Code auch zu laden.
  3. Aktualisiert man nur eine Bibliothek sorgt nichts dafür, dass Dienste neu gestartet werden, die die alte Version der Bibliothek geladen haben.

Hätten wir weiter gesucht hätten wir sicherlich noch weitere Varianten von ‘der Dienst müsste eigentlich neu gestartet werden’ gefunden, aber die oben zitierten Funde haben ausgereicht, uns dazu zu bringen etwas Besseres zu suchen.

Der Plan

Wir waren sehr schnell an dem Punkt zu erkennen, dass ein Neustart eines Systems den eindeutigsten Zustand erzeugt, waren uns aber auch bewusst, dass wir vor und nach einem Neustart auf unterschiedlichen Klassen von Systemen auch unterschiedliche Dinge zu tun haben. Wir haben uns entschieden, dass das dennoch der Weg ist, den wir verfolgen wollen.

Die Details

Bei der Betrachtung unterschiedlicher Systeme haben wir schließlich folgende Zeitpunkte ermittelt, zu denen Dinge passieren müssen:

  1. Vor dem Update.
  2. Nach dem Update.
  3. Nach dem Reboot.

Die Paketaktualisierung selbst wird per cron gestartet und führt folgendes aus:

  1. Anweisungen, die vor dem Update ausgeführt werden müssen.
  2. Die Aktualisierung selbst.
  3. Anweisungen, die nach der Aktualisierung, aber vor dem Neustart ausgeführt werden müssen.
  4. Den Neustart der Maschine

Die Anweisungen liegen in Shell Skripten in Ordnern, die namentlich den drei oben genannten Zeitpunkten entsprechen, die Namen der Skripte sind außerdem mit einem numerischen Prefix versehen, das uns erlaubt die Reihenfolge des Aufrufs zu bestimmen.

Um die Skripte aufzurufen, die nach dem Reboot ausgeführt werden müssen verwenden wir den @reboot Mechanismus von cron, wir führen so alle Dateien aus, die im Ordner für Dinge, die nach dem Neustart erledigt werden müssen liegen und ausführbar sind.

Jedes dieser Skripte entfernt im letzten Schritt sein Executable Flag, dieses Flag muss nämlich vorher von einem Script gesetzt werden, das vor dem Neustart erfolgreich ausgeführt wurde. Dieser Mechanismus wurde etabliert um zu gewährleisten, dass beispielsweise ein manuell aus der Verteilung eines Loadbalancers genommener Dienst nach einem Neustart der Maschine nicht automatisch wieder angemeldet wird, die Scripte sollen nur dann ausgeführt werden, wenn eine Maschine durch die automatischen Upgrades neu gestartet wurde.

Was also sind das für Dinge die vor oder nach einem Neustart erledigt werden müssen?

Beispiele für Dinge die vor einem Neustart zu tun sind:

  • Abmeldung eines Dienstes von seinem Loadbalancer
  • Das zu Ende führen laufender Gespräche bei gleichzeitiger nicht-Annahme von neuen Anrufen

Daraus ergibt sich auch gleich, was in diesen Fällen nach einem Neustart getan werden muss:

  • Wiederanmelden eines Dienstes, wenn er sich vorher an seinem Loadbalancer abgemeldet hat
  • Die Bereitschaft neue Gespräche anzunehmen signalisieren

Detaillierte Details

Grundsätzlich werden automatische Updates nur an den Tagen Montag bis Donnerstag durchgeführt, um der Rufbereitschaft möglichst wenige Alarme am Wochenende zu bescheren.

Wir führen die Updates abhängig von der Anzahl der Backends an unterschiedlichen Tagen aus, gibt es zwei Backends so werden die Updates von Backend A an ungeraden Wochentagen, die von Backend B an geraden Tagen durchgeführt. (Unix/Linux nummerieren die Wochentage durch, beginnend mit Sonntag als Tag 0)

Bei Diensten mit mehr Backends führen wir in den meisten Fällen upgrades nur auf einem dieser Backends pro Tag durch.

Als Zeitpunkt für die Installation von Software Upgrades haben wir zunächst zwei unterschiedliche Zeitfenster vorgesehen, tagsüber und nachts, das wurde später noch um die Möglichkeit erweitert den Zeitpunkt beliebig zu setzen. Die beiden im ersten Schritt definierten Zeitfenster sind jeweils eine Stunde lang, die Minute der Ausführung wird beim initialen Anlegen des Cronjobs zufällig, aber deterministisch bestimmt.

Im Verlauf der Durchführung gelerntes

Wir dachten so wären wir fein raus, mussten aber lernen, dass unser Dienst nicht der einzige ist, der einen anderen Dienst stoppen kann, unser Deployment kann einen Dienst zu jedem Zeitpunkt neu ausrollen und diesen damit zumindest kurz stoppen.

Wenn wir auf einem Dienst mit zwei Backends nun Updates installieren und gleichzeitig eine neue Version des hier lebenden Dienstes auf dem zweiten Backend ausgerollt wird ist der Dienst ohne Backend.

Um das zu verhindern haben wir eine einfache Webseite angelegt, auf der sowohl die automatischen Updates als auch unser Deployment prüfen, ob hier ein Lockfile vorliegt, das besagt dass ein anderer Mechanismus den Dienst gleich stoppen wird. Liegt diese Datei vor, brechen Deployment bzw. automatische Updates ab, liegt sie nicht vor so wird sie angelegt.

Reporting, Monitoring und Fehlerhandling

Jeder Host, der ein automatisches Update durchführt meldet den bevorstehenden Neustart in einem Slack Channel. Das erledigt ein systemd Service, der ein Shellskript ausführt, wenn er gestoppt wird. Das Shellscript kann erkennen, ob der Neustart durch automatische Upgrades ausgelöst wurde und meldet diesen dann in folgender Form:

Server Reboot Notification [03:24 Uhr]
stretch Server $hostname.$domain.$tld ($Aufgabe der Maschine) rebootet nach unattended-upgrades
und zwar um 2019-06-19 03:24:46.
Dabei wurden folgende Pakete aktualisiert:
 dbus libdbus-1-3 linux-image-4.9.0-9-amd64 linux-libc-dev

Tritt bei unserem Prozess ein Fehler auf wird dieser in einem anderen Channel gemeldet, in einer idealen Welt gibt es hier nie was zu lesen, falls aber doch erhält man hier einen Hinweis welches Skript nicht erfolgreich ausgeführt werden konnte und den Namen der Logdatei, in der Details zum aufgetretenen Fehler zu finden sind:

unattended-upgrades error notification [12:56 Uhr]
*pre hook 10-disable-asyncd-for-unattended-upgrades.sh failed on $hostname.$domain.$tld*
10-disable-$Dienst-for-unattended-upgrades.sh failed with return code 1. Log is in /var/log/unattended-upgrades/pre-upgrade-hook/10-disable-$Dienst-for-unattended-upgrades.sh.log. Exiting /usr/local/sbin/sipgate-unattended-upgrade.sh.

In einer weiteren Nachricht weist die Maschine darauf hin, dass alle Vorbereitungen zum automatischen Upgrade wieder rückgängig gemacht wurden:

*$hostname.$domain.$tld*
Running post-boot hooks to re-enable all the things.

Auch hier bedienen wir uns des weiter oben skizzierten Mechanismus, im Fehlerfall werden ebenfalls alle ausführbaren Dateien im Ordner mit den Skripten für nach dem Reboot abgearbeitet, da diese nur im Erfolgsfall ausführbar gemacht werden sind sie die Quelle der Information, welche Dinge wieder zurückgenommen werden müssen.

Ablaufplan

Weil ein Bild ja bekanntlich mehr sagt als 1000 wohlgewählte Worte hier der Ablaufplan unseres automatischen Upgrades:

Keine Kommentare


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert