Nix & NixOS
Nix & NixOS
Lesezeit: Acht Minuten
Ein Problem so alt wie die Softwareentwicklung selbst: Nach einem Update funktioniert *nix* mehr.
Dieses Problem ist Ihnen sicher schon einmal begegnet.
Sei es ein einfaches Grafiktreiber-Update, welches dazu führt, dass die eigene Maschine nicht mehr grafisch bootet oder ein Update beliebiger Anwender-Software, nach welchem jene nicht mehr wie gewohnt funktioniert.
Selbst bei gründlichster Testung steht jede Software vor der zentralen Frage: Funktioniert die Software beim Anwender?
In diesem Blog-Beitrag betrachten wir, wie ein revolutionärer, deklarativer Ansatz von Nix und NixOS diese Frage überflüssig macht und warum betriebliche Informationssysteme davon profitieren können.
Öko-System: Nix ist nicht gleich Nix
Wer schon einmal mit Nix in Berührung gekommen ist, der denkt an einen Paket-Manager. Das ist auch vollkommen richtig, denn der Paket-Manager Nix ist der zentrale Bestandteil des Nix-Öko-Systems. Zusätzlich gibt es aber auch Nix: eine domänenspezifische Sprache zur Beschreibung von Build-Instruktionen. Zur Differenzierung bezeichnen wir diese im Nachfolgenden auch mit dem etwas unüblicheren Namen der Nix Expression Language. Aufbauend auf dem Paket-Manager Nix gibt es noch eine Linux-Distribution NixOS, welche die funktionale Idee des Paket-Managers auf System-Ebene übernimmt. Erwähnenswert, aber in diesem Beitrag nicht weiter betrachtet ist Nixpkgs: das Paket-Repository für Nix-Pakete.
Nix ist deklarativ – alles ist deklarativ
Nix Expression Language
Die Nix Expression Language ist eine touring-vollständige domänenspezifische Sprache zur Beschreibung der Erstellung von Paketen. Angelehnt an die funktionale Programmiersprache Haskell ist diese ebenfalls deklarativ und rein funktional – aber dynamisch typisiert.
Betrachten wir das Universum als Menge von Paketen P (z.B. gcc
), Konfigurationen C (z.B. Flaggen für gcc
) und Daten D (z.B. Quellcode-Dateien). Ein Nix-Paket ist dann Funktionswert einer Transformation t:2P×C→P. Jedes Paket hängt also von bestimmten Eingaben (Dependencies) ab und produziert eine Ausgabe (Derivation – Nix-Name für ein Paket).
Aufgrund der rein funktionalen Natur von Nix ist nicht verwunderlich, dass allgemeiner alle Nix-Ausdrücke Bäume sind. Blätter sind Konfigurationen C oder Daten D und Knoten sind Transformationen t:2P×C0,1×2D→P∣C∣D. Da identische Teilbäume aus Performance-Gründen allerdings untereinander geteilt werden (engl. sharing), sind die Ausdrücke in Wirklichkeit kreisfreie, gerichtete Graphen (engl. directed acyclic graph – kurz DAG).
Aufgeschrieben werden Nix-Ausdrücke in Dateien mit Endung .nix
.
Nix
Gebaut werden die Pakete in einer dedizierten System-Umgebung, dem sogenannten Nix-Store. Unter Linux entspricht dieser dem Pfad /nix/store
im Root-Dateisystem und unter MacOS sogar einer eigenen System-Partition. Im Folgenden betrachten wir ausschließlich die Verwendung von Nix unter Linux. Der Speicher-Ort eines Pakets wird durch die Addition des Hashes eines Pakets zum Pfad des Nix-Stores bestimmt. Ein Paket p wird mit einer Hash-Funktion h:P→N also unter /nix/store/h(p)
gespeichert.
Wie die Expression Language ist auch der Nix-Store rein funktional, was bedeutet, dass alle Pakete unveränderlich (engl. immutable) sind. Zusammen mit der Hash-Funktion gilt folgende Eigenschaft:
∀p1,p2∈P:(h(p1)≠h′(p1′)∧p2=t(P,c)∧p1∈P)→Rebuild(p2,h′)
Ändert sich eine Dependency eines Paketes p, dann muss dieses neu gebaut werden. Das neu gebaute Paket p′ erhält dann einen neuen Hash und somit eine neue Speicherstelle. Das alte Paket p an der alten Stelle bleibt dabei dennoch erhalten, sodass der Nix-Store referentielle Transparenz aufweist. Diese Eigenschaft garantiert zusammen mit den ausschließlich puren Funktionen der Nix Expression Language, dass Pakete tatsächlich Funktionswerte mathematischer Funktionen sind, also ausschließlich von ihren Eingaben abhängen und somit vollständig reproduzierbar sind.
NixOS
Nix implementiert pures und rein funktionales Paket-Management. Diese Art von Management kann allerdings auch auf System-Ebene gehoben werden. Genau das macht NixOS, eine Linux-Distribution auf Basis des Paket-Managers Nix. So wie bei Nix jedes Paket durch eine Build-Instruktion in einer .nix
-Datei beschrieben werden kann, kann auch ein ganzes Betriebssystem in einer einzigen .nix
-Datei beschrieben werden. Betrachten wir beispielhaft folgendes Betriebssystem, auf welchem sowohl ein OpenSSH- als auch ein Apache-Server laufen:
Abbildung: Beispielhafte und vereinfachte System-Konfiguration in NixOS
Wie auch bei Nix wird hier deutlich, dass jedes System ein DAG aus Paketen, Konfigurationen und Daten ist. Zusätzlich kommen hier noch Skripte hinzu, um ausführbare Dienste zu ermöglichen.
Vorteile von NixOS in betrieblichen Informationssystemen
Durch diese deklarative und rein funktionale Natur von Nix und NixOS ergeben sich einige Vorteile, die insbesondere in betrieblichen Informationssystemen von Nutzen sind. Im Folgenden betrachten wir einige davon.
Sichere Updates durch Rollbacks
Systemupdates werden durch Änderungen der Konfigurationsdatei durchgeführt. Um die Änderungen zu übernehmen, muss das System mit nixos-rebuild
neu gebaut werden. Da die Konfiguration der entsprechenden Paketversionen durch eine Konfigurationsdatei beschrieben ist, kann jede „Generation“ des Systems in einer Historie festgehalten werden. Sollten Aktualisierungen zu Problemen oder Instabilität führen, kann mithilfe von nixos-rebuild --rollback switch
auf eine ältere Generation zurückgewechselt und die vorherige Konfiguration wiederhergestellt werden. Außerdem besteht die Möglichkeit neue Konfigurationen mit nixos-rebuild build-vm
vorher in einer VM auszutesten. Damit kann Downtime in produktiven Umgebungen durch fehlerhafte Updates minimiert werden.
Um Rollbacks zu gewährleisten, speichert NixOS standardmäßig alle vorherigen Systemzustände. Dies kann mitunter zu einem hohen Speicherverbrauch führen, was insbesondere in ressourcenbegrenzten Umgebungen wie der Cloud problematisch sein kann.
Skalierbarkeit und Automatisierung
Ähnlich wie klassische Docker-basierte Systeme wie beispielsweise GitHub Actions, bietet das auf Nix basierende Tool Hydra eine Umgebung zum Ausführen von automatisierten Pipelines. Dabei wird vollständig auf Docker verzichtet, sodass stattdessen ausführbare System-Images direkt mit Nix-Konfigurationen deklariert werden.
Alternativ ist es auch möglich, NixOS in einer Docker-Umgebung zu verwenden, um die Vorteile beider Systeme zu kombinieren. Damit können deklarative Umgebungen, die sich garantiert immer gleich verhalten in weit verbreiteten Systemen eingesetzt werden, in denen NixOS nicht direkt unterstützt wird. Ein Beispiel dafür sind Cloud-Applikationen, die Docker in Verbindung mit Kubernetes zum Skalieren verwenden.
Für die Verwendung unter Amazons EC2 können virtuelle System-Images direkt aus Nix-Konfigurationen heraus gebaut werden. Der Befehl nix-build
nimmt dafür als Argument einerseits die Nix-Konfigurationsdatei und andererseits das bereitgestellte EC2 Base-Image für die passende Architektur.
Sicherheit
Betriebliche Informationssysteme können durch den Einsatz deklarativer Konfigurationen zuverlässiger höheren Sicherheitsstandards entsprechen. In einer deklarativen Umgebung wird der gewünschte Endzustand explizit beschrieben und das System automatisch so konfiguriert, dass dieser erreicht wird. Dadurch werden unbeabsichtigte Nebenwirkungen oder nicht dokumentierte Zustände, wie sie bei klassischen imperativen Ansätzen (direkte Befehle, manuelle Konfiguration) auftreten können, vermieden.
Außerdem können Sicherheitsprüfungen in einer einzelnen Umgebung durchgeführt werden und auf alle anderen Umgebungen übertragen werden, auf denen diese Konfiguration zum Einsatz kommt. Durch die zustandslose Konfiguration kann somit garantiert werden, dass alle Umgebungen den gleichen Sicherheitsstandards entsprechen.
Weiterhin ist es möglich, durch die Versionierung von Systemzuständen direkt nachzuvollziehen, wer was wann wie geändert hat und welche Auswirkungen diese Änderungen hatten. So können Breaking-Changes, oder Änderungen an sicherheitskritischen Komponenten direkt isoliert und nachvollzogen werden.
Alle Änderungen, die am System vorgenommen werden, sind zudem atomar. Jede Änderung wird also vollständig übernommen oder gar nicht. In klassischen Systemen kann es dazu kommen, dass bei fehlgeschlagenen Installationen das System in einem halbfertigen, potenziell unsicheren Zustand zurückgelassen wird. Diese Zustände werden hier vermieden, womit die damit verbundenen Sicherheitslücken nicht auftreten können.
Anwendungsbeispiel Pinterest
Ein prominentes Beispiel, bei welchem NixOS erfolgreich in einem betrieblichen System zum Einsatz kommt, stellte das Pinterest Engineering Team in einem Blog-Beitrag vor. Konkret ging es um die Pipelines zum automatisierten Bauen und Testen der Pinterest iOS-App. Die Ausgangssituation, die viele Probleme verursachte, sah wie folgt aus:
- Groovy und Bash Skripte für Jenkins Pipelines waren überall im Repository verteilt und schwer zu warten
- Es gab es eine große Menge an unversioniertem Code in Form von Jenkins-Konfigurationen
- Zahlreiche Beschwerden von Entwicklern über die CI Umgebung
- „My build passes locally, but fails on CI.“
- „It’s difficult to reproduce what’s running on CI on my machine.“
- „It’s difficult to add new pipelines or build new machines.“
- „Builds are not fast enough.“
- „It’s difficult to tell why my build failed.“
Aus diesen Problemen heraus, entschied sich Pinterest’s Engineering Team unter anderem dazu, zu Nix-basiertem Deployment zu wechseln. Konkret bedeutete das die Vereinheitlichung von Dev und CI Umgebungen. Anstelle komplizierter und verteilter Bash-Setup-Skripte wurde auf eine Nix-Config Datei umgestellt. Mit dieser Konfiguration konnten dann sowohl lokale Entwicklungsumgebungen als auch die CI Maschinen sehr einfach aufgebaut werden. Durch diese Änderung hatten die Entwickler nun die Garantie, dass Fehler, die in der CI auftraten, auch lokal reproduziert waren.
Die Ergebnisse der Builds sind immer gleich, unabhängig von Ausführungszeit oder -Umgebung, außerdem gibt es keinen unklaren System-Zustand oder unvorhersehbare Nebeneffekte. Ein weiterer Vorteil der Umstellung auf Nix ist die Vereinheitlichung der Abhängigkeiten. Sollten Entwickler Pakete aktualisieren, so geschieht das über die Nix-Config, die automatisch auch die CI Umgebung aktualisiert.
Parallel dazu wechselte das Team auch auf Buildkite, um die Pipelines zu verwalten. Buildkite ist eine Alternative zu Jenkins, die es ermöglicht CI/CD Prozesse auf der eigenen, selbst gehosteten Infrastruktur auszuführen und dennoch ein zentrales, cloud-basiertes Web-Interface zur Verwaltung bietet. Das ermöglichte den beliebigen Einsatz der zuvor durch Nix vereinheitlichten Systeme als Buildkite-Agenten. Buildkite setzt standardmäßig auf parallel laufende Pipeline-Stages, um Build-Zeiten zu reduzieren. Diese Eigenschaft konnte dank einfacher Parallelisierung einzelner Build-Schritte von Nix-Paketen maximal ausgeschöpft werden. Insgesamt wurde das System dadurch deutlich stabiler, schneller, einfacher zu warten und skalierbarer.
Fazit
Nix und NixOS adressieren ein altes Problem der Softwareentwicklung: Fehleranfälligkeit durch schwache Reproduzierbarkeit. Der deklarative und rein funktionale Ansatz von Nix ermöglicht maximale Reproduzierbarkeit, sodass Software in beliebigen Umgebungen konsistent und zuverlässig bereitgestellt werden kann.
Es ist wahrscheinlich, dass Nix und NixOS in Zukunft vermehrt genutzt werden, da ihre Vorteile im Hinblick auf Zuverlässigkeit und Sicherheit immer mehr Beachtung finden.
Quellen
- Eelco Dolstra. „The Purely Functional Software Deployment Model“. Diss. Utrecht, The Netherlands: Utrecht University, Jan. 2006.
- Eelco Dolstra und Armijn Hemel. „Purely functional system configuration management“. In: Proceedings of the 11th USENIX Workshop on Hot Topics in Operating Systems. HOTOS’07. San Diego, CA: USENIX Association, 2007.
- Pablo Ovelleiro Corral. „Nix and NixOS for DevOps“. Okt. 2022. URL: https://www.inovex.de/de/blog/nix-and-nixos-for-devops/ (Zugriff 27.12.2024)
- Ryan Yin. „Advantages & Disadvantages of NixOS“. Dez. 2024. URL: https://nixos-and-flakes.thiscute.world/introduction/advantages-and-disadvantages (Zugriff 27.12.2024)