Die Verwaltung hardwarebezogener Aspekte von Kubernetes-Clustern gestaltet sich oft schwierig. Der Zugriff auf Host-Hardware erfordert meist zusätzliche Kernelmodule und eine tiefe Integration in oder sogar Modifikation der Container Runtime. Gleichzeitig kommen und gehen Nodes, aber Provisioning-Tools wie coreos/ignition oder canonical/cloud-init können diese teils komplexen Modifikationen nur unzureichend abdecken. Noch schwieriger wird es bei spezialisierten Container-Betriebssystemen wie Fedora/RedHat CoreOS und Flatcar aufgrund ihrer weitgehend unveränderlichen oder flüchtigen Dateisysteme. Oftmals scheint der Einsatz einer zusätzlichen IT-Automatisierungslösung wie Ansible oder Chef notwendig.
Glücklicherweise sind Anwendungsfälle, die einen direkten Hardware-Zugriff erfordern, wie dedizierte z.B. Netzwerkhardware, selten. Es gibt nur eine wirkliche Ausnahme von dieser Regel: GPUs. Obwohl die GPU-Integration von Kubernetes im Laufe der Jahre viel besser geworden ist, kann die Verwaltung von Treibern und Laufzeiten immer noch eine Herausforderung sein. Daher war ich regelrecht begeistert, als ich kürzlich den NVIDIA/gpu-operator entdeckte. Der gpu-operator ist ein nativer Kubernetes-Operator, der fast alle Arbeiten im Zusammenhang mit der Verwaltung von Nodes mit GPUs automatisiert: Er installiert Treiber, Laufzeiten, Geräte-Plugins, Metrik-Exporteure, fügt Nodes passende Label hinzu usw.
Das mag fast zu schön klingen, um wahr zu sein, und natürlich gab es ein Problem für meine Anwendungsfälle. Bisher unterstützt der Operator nur eine sehr begrenzte Anzahl von Plattformen, nämlich Ubuntu, RHCOS und CentOS1, und alle unsere Nodes laufen auf Flatcar Linux. Wie ich allerdings herausgefunden habe, scheint die Flatcar-Unterstützung in der Entwicklung zu sein 2 und es gibt sogar bereits einige mit Flatcar getaggte Treiber Container Images in der NVIDIA Container Registry.
Setup
Die Installation ist natürlich noch nicht so reibungslos wie auf den unterstützten Betriebssystemen und es gibt keine regelmäßigen Treiber-Builds auf der offiziellen CI. Aber ansonsten funktioniert es! Natürlich gibt es auch noch keine offizielle Dokumentation zu den Voraussetzungen und Vorbereitung der Nodes, aber ansonsten kann man der offiziellen Getting Started Anleitung folgen.
Vorbereitung
Bevor der GPU-Operator einen Flatcar-Node konfigurieren kann, müssen zwei Dinge erledigt werden. Erstens müssen die Kernelmodule i2c_core und ipmi_msghandler aktiviert werden3. Wenn man möchte, dass der Treiber-Container in der Lage ist, die Treibermodule nach Kernel-Upgrades dynamisch zu erstellen, muss auch das Modul loop aktiviert werden4.
Die Module können mit den folgenden Befehlen geladen und dauerhaft aktiviert werden
sudo modprobe -a loop i2c_core ipmi_msghandler
sudo echo -e "loop\ni2c_core\nipmi_msghandler" | tee /etc/modules-load.d/driver.conf
Der zweite Vorbereitungsschritt betrifft das Dateisystem von Flatcar. Standardmäßig sind die meisten Systemverzeichnisse bei Flatcar auf schreibgeschützte Dateisystemen. Unglücklicherweise versucht der GPU Operator, seine Artefakte, wie den Treiber und die Container-Laufzeiten, an einem solchen Ort abzulegen. Ich konnte bisher keine Lösung finden, um dieses Verhalten zu ändern, ohne einen Fork der offiziellen Operator-Ressourcen pflegen zu müssen. Ich entschied daher, dass es die einfachste Lösung sei, das Ziel beschreibbar zu machen. Der beste Weg, dies zu tun, scheint die Erstellung eines beschreibbaren Overlays für die benötigten Verzeichnisse zu sein5.
Dies gelingt mit einer einfachen systemd Unit.
[Unit]
Description=Writable nvidia driver location
Before=local-fs.target
ConditionPathExists=/opt/usr-local-overlay
[Mount]
Type=overlay
What=overlay
Where=/usr/local
Options=lowerdir=/usr/local,upperdir=/opt/usr-local-overlay,workdir=/opt/usr-local-overlay.wd
[Install]
WantedBy=local-fs.target
Ich verwende Container Linux Configs, um die Kernelmodule zu aktivieren und die systemd Unit zu installieren. Diese werden in ingnition Dateien übersetzt und an die Nodes ausgeliefert, wenn diese initialisiert werden. Sollte dies nicht möglich sein, schlage ich vor, die notwendigen Befehle in einem Kubernetes DaemonSet auszuführen.
Treiber Container
Wie bereits erwähnt, gibt es noch keine offiziellen regulären vorgefertigten Treiber-Container für Flatcar. Allerdings gibt es einige vereinzelte Builds in der NVIDIA Container Registry, z.B. nvcr.io/nvidia/driver:460.73.01-5.10.32-flatcar. Eine Möglichkeit ist also, eines dieser Images zu verwenden.
Wenn man eine neuere Treiberversion verwenden will oder die vorkompilierte Kernelmodule nutzen möchten, muss der Container selbst gebaut werden. Glücklicherweise ist dies ziemlich einfach. Man muss nur das Repository klonen, eine Treiberversion auswählen und docker build ausführen.
git clone https://gitlab.com/nvidia/container-images/driver.git
cd driver/flatcar
DRIVER_VERSION=470.57.02
docker build --pull --tag your-registry/nvidia-driver:${DRIVER_VERSION} --file Dockerfile . --build-arg DRIVER_VERSION=${DRIVER_VERSION}
Die aktuelle Version verwendet eine veraltete URL, um Versionsinformationen für Flatcar zu herunterzuladen. Die URL scheint sich vor kurzem geändert zu haben, als Kinvolk (die Macher von Flatcar) von Microsoft aufgekauft wurde. Obwohl der alte Link immer noch online ist, scheint er bei neueren Versionen von Flatcar einige Probleme zu verursachen. Ich schlage daher vor, den Link durch den neuen zu ersetzen, bevor der Container gebaut wird
-COREOS_ALL_RELEASES="https://kinvolk.io/flatcar-container-linux/releases-json/releases-${COREOS_RELEASE_CHANNEL}.json"
+COREOS_ALL_RELEASES="https://www.flatcar-linux.org/releases-json/releases-${COREOS_RELEASE_CHANNEL}.json"
Vorkompilierte Kernel Module
Das Standardverhalten des Treiber-Containers ist es, den Flatcar-Entwicklungscontainer herunterzuladen und die Kernel-Module im laufenden Betrieb zu bauen. Dies hat den Vorteil, dass man denselben Treiber-Container auf allen Nodes verwenden kann, auch wenn auf diesen unterschiedliche Flatcar-Versionen mit unterschiedlichen Kerneln laufen. Der Nachteil ist, dass es je nach Hardware ein paar Minuten dauern kann, bis die Nodes verfügbar sind.
Eine schnellere Lösung besteht darin, vorkompilierte Kernelmodule in den Container zu packen. Das Einzige, was man dafür tun muss, ist, den Container mit dem Befehl update zu starten, ihn danach zu committen und die zusätzlichen Layer in die Registry zu hochzuladen. Man muss dies jedes Mal wiederholen, wenn eine neue Betriebssystemversion veröffentlicht wird. Der GPU Operator kann automatisch den richtigen Tag auswählen, wenn er das Image für einen Node herunterlädt. Wenn der Container eine inkompatible Kernelversion feststellt, wird er einfach auf das Standardverhalten zurückfallen und das Modul im Container neu bauen.
Genaueres über diese Funktion in der gibt es in der README des Treiber-Containers zu lesen.
Deployment
Für den Einsatz des GPU Operators kann man der offiziellen Getting Started Anleitung folgen. Man muss nur sicher stellen, dass der richtige Treiber-Container konfiguriert ist. Wenn Helm verwendet wird, sind die entsprechenden Werte die folgenden:
driver:
repository: your-repo
image: nvidia-driver
version: "sha256:XXXXX"
Ich habe hier die SHA-Summe des Containers anstelle eines Tags verwendet. Dies ist erforderlich, wenn keine vorkompilierten Kernelschnittstellen verwendet werden. Man kann hier natürlich auch einen Tag angeben. In diesem Fall wird der GPU Operator -flatcarVERSION, z.B. -flatcar2905 an den angegebenen Tag anhängen. Es muss daher sichergestellt sein, dass für alle Flatcar-Versionen, die im Cluster im Einsatz sind, getaggte Images bereitstehen. Andernfalls werden die Nodes das Treiber-Image nicht finden können und der Container Start fehlschlagen.
Und das war’s auch schon! An dieser Stelle sollten alle GPU Nodes in wenigen Minuten voll einsatzbereit sein :)