Wenn Sie diese Kolumne regelmäßig verfolgen, wissen Sie, dass ich für die Erledigung vieler Aufgaben die Kommandozeile gegenüber grafischen Werkzeugen präferiere. Für mich funktionieren diese in der Regel besser und passen besser in meinen Arbeitsfluss. Gleichzeitig habe ich es in meiner Arbeit auch mit Containern und Kubernetes zu tun. Dabei lassen sich die meisten Aufgaben mit den beiden Werkzeugen docker und kubectl erledigen.

Aber es gibt auch eine Reihe weiterer Werkzeuge, die Fehlendes ergänzen und die Arbeit bequemer oder effizienter machen. Deshalb erlaube ich mir in dieser Kolumne, obwohl Thomas Ronzon hier im Heft ja der Experte für Tools ist, eine zusammengewürfelte Menge an Werkzeugen auf der Kommandozeile für Aufgaben rund um Container und Kubernetes vorzustellen. Unser Werkzeugkasten ist schließlich virtuell und hat damit eine quasi unendliche Größe.

Lazydocker

Heutzutage gibt es zur Verwaltung von Containern zwar Alternativen zu Docker, wie beispielsweise Podman, aus der Historie heraus bin ich jedoch bis heute beim Original geblieben und nutze Docker-Desktop. Dementsprechend sind bei mir viele der Befehle docker oder docker compose, ja – mittlerweile nicht mehr docker-compose, ins Muskelgedächtnis übergegangen und ich tippe diese, ohne lange darüber nachzudenken. Und trotzdem muss ich zugeben, dass es manchmal aufwendig ist, da ich der Reihe nach diverse Befehle ausführen muss, um zu meinem Ziel zu gelangen.

Genau hier setzt Lazydocker an. Lazydocker ist zwar ein Kommandozeilenwerkzeug, bietet aber auf der Kommandozeile eine interaktive Oberfläche an, um mit docker und docker compose zu interagieren (s. Abb. 1).

Abb. 1: Lazydocker

Standardmäßig unterteilt Lazydocker die Oberfläche dabei vertikal in zwei Teile:

Neben der reinen Anzeige von Informationen können wir aber auch mit den vorhandenen Elementen interagieren, beispielsweise einen Container starten oder stoppen. Hierzu gibt es eine Reihe von Tastaturkürzeln, die man über die Zeit lernt. Kennt man diese nicht, hilft es, nach der Selektion eines Elements x zu drücken. Anschließend öffnet sich ein Menü, das uns alle aktuell zur Verfügung stehenden Aktionen und deren Tastaturkürzel anzeigt (s. Abb. 2).

Abb. 2: Lazydocker-Menü mit Aktionen

Lazydocker bietet uns somit eine zwar Kommandozeilen basierte, aber dennoch grafische und interaktive Oberfläche, um mit Containern und Compose-Projekten zu interagieren. Dies sorgt für eine erhöhte Benutzerfreundlichkeit gegenüber der Nutzung von einzelnen Befehlen, bleibt aber durch die Steuerung über die Tastatur trotzdem sehr produktiv.

Natürlich lassen sich über Konfiguration sowohl das Aussehen als auch die vielen Tastaturkürzel verändern und an die eigenen Bedürfnisse anpassen. Zudem ist Lazydocker auf allen gängigen Betriebssystemen verfügbar und somit eine super Ergänzung für den eigenen Werkzeugkasten.

k9s

Auch bei Kubernetes wird häufig eine Abfolge von kubectl-Befehlen benötigt, um an die benötigten Informationen zu gelangen. So muss ich in der Regel erst mal mit einem get herausfinden, wie mein Objekt heißt, bevor ich dann mittels describe oder logs auf weitere Informationen zugreifen kann.

Mit k9s gibt es auch hierfür ein Werkzeug, um auf der Kommandozeile zu arbeiten, aber vom Vorteil der interaktiven Bedienung zu profitieren (s. Abb. 3).

Abb. 3: k9s Liste aller Pods

k9s unterteilt die Oberfläche dabei horizontal in zwei Bereiche:

Im Falle von Abbildung 3 bringt uns Enter zu den Containern, die innerhalb des Pods definiert sind. Wählen wir hier einen davon aus und drücken erneut Enter, landen wir wiederum in den zum Container passenden Logs (s. Abb. 4). Andersherum können wir über die Esc-Taste wieder zurück zum vorherigen Screen gelangen. Beenden lässt sich k9s übrigens, ähnlich wie vim, mit :q.

Abb. 4: k9s Logs zu einem Container

Mit k9s ist es, im Vergleich zur manuellen Nutzung von kubectl, einfach, innerhalb eines Clusters zu navigieren und Aktionen auszuführen. Die angezeigten Informationen und Objekte werden dabei dauerhaft, im Hintergrund, aktualisiert und zeigen somit den wirklichen aktuellen Zustand an. Und dadurch, dass k9s wie ein regulärer Client mit dem Cluster interagiert, werden auch sämtliche Zugriffsrechte beachtet. Darf ein Nutzender einen bestimmten Namespace oder Pod nicht sehen, dann ist dieser für diesen auch nicht in k9s sichtbar.

Genau wie Lazydocker verbindet k9s somit die Vorteile von Tastaturbedienung und Kommandozeile mit einer grafischen und vor allem interaktiven Oberfläche. Ich kann mir, vor allem zur Anzeige von Informationen, den Umgang mit reinem kubectl nur noch im Notfall vorstellen.

kubectx und kube-ps1

kubectl wurde für mich zwar durch k9s für die Informationsgewinnung in einem Kubernetes-Cluster unnötig, trotzdem benötige und benutze ich das Tool, meist in Kombination mit apply, jedoch weiterhin. Vor allem, wenn mit mehreren Clustern oder verschiedenen Namespaces innerhalb eines Clusters interagiert werden muss, macht, zumindest mir, die Nutzung jedoch wenig Spaß. Zwar kennt kubectl das Prinzip von Kontexten, mit denen zwischen verschiedenen Clustern gewechselt werden kann, siehe Listing 1, aber vor allem bei Namespaces bleibt einem nur übrig, noch mehr Kontexte zu erzeugen oder bei jedem Kommando mit -n NAMESPACE_ NAME zu arbeiten. Das fühlt sich nicht nur umständlich an, sondern ist es meiner Meinung nach auch, und zu oft bin ich darüber gestolpert, dass ich dann doch im falschen Kontext oder Namespace unterwegs war.

$ kubectl config current-context
docker-desktop
$ kubectl config get-contexts
CURRENT NAME      CLUSTER        AUTHINFO       NAMESPACE
  another-cluster docker-desktop docker-desktop
* docker-desktop  docker-desktop docker-desktop all
$ kubectl config use-context another-cluster
Switched to context "another-cluster".
Listing 1: kubectl contexts

Mit kubectx gibt es ein weiteres Projekt, das uns gebündelt zwei Werkzeuge, nämlich kubectx und kubens, zur Verfügung stellt. Diese vereinfachen das Wechseln von Kontexten und erlauben uns dieselbe Funktionalität für Namespaces (s. Listing 2). Werden die Tools dabei ohne Optionen und Argumente aufgerufen, wird uns eine interaktive Liste der vorhandenen Kontexte beziehungsweise Namespaces angezeigt. Diese lässt sich filtern und durch Auswählen eines Eintrags und Drücken von Enter wird auch dieser direkt gesetzt, anstatt dass wir uns erst alle auflisten lassen und dann durch ein zweites Kommando einen Namespace setzen können.

$ kubectx another-cluster
Switched to context "another-cluster".
$ kubens kube-system
Context "another-cluster" modified.
Active namespace is "kube-system".
Listing 2: Einsatz von kubectx und kubens

Ergänzend hierzu ist es sinnvoll, auch noch kube-ps1 einzusetzen. Dieses erlaubt es uns, den Prompt in einer Bash- oder Zsh-Shell um den gerade aktiven Kontext und Namespace zu erweitern (s. Abb. 5). kube-ps1 lässt sich dabei über eine Reihe von Umgebungsvariablen an die eigenen Ansprüche anpassen. So lassen sich neben den angezeigten Informationen auch die genutzten Farben anpassen.

Abb. 5: kube-ps1 im Einsatz

Beide Werkzeuge sind für mich eine super Ergänzung, um die Aufgaben, die ich mit kubectl erledigen muss, bequem auszuführen, und sie verbessern für mich an dieser Stelle den Komfort deutlich.

Hadolint und kube-score

Um Container-Images zu erzeugen, werden auch heute oft noch eigene Dockerfiles geschrieben. Dabei ist es schnell passiert, dass sich, aus Unwissen oder Flüchtigkeit, kleine Fehler einschleichen. Um diese zu finden und sich an gängige Best Practices zu halten, kann Hadolint helfen. Hierbei handelt es sich um einen Linter, der das angegebene Dockerfile auf eine Reihe von bekannten Fehlern prüft und uns direkt Feedback gibt, was wir ändern sollten oder müssen (s. Listing 3).

$ cat Dockerfile
FROM ubuntu
RUN export node_version="22" \

    && apt-get update \

    && apt-get -y install \

        nodejs="$node_vesrion" \

        mysql
$ hadolint Dockerfile
Dockerfile:1 DL3006 warning:
    Always tag the version of an image explicitly
Dockerfile:2 DL3008 warning:
    Pin versions in apt get install. ...
Dockerfile:2 SC2154 warning:
    node_vesrion is referenced but not assigned ...
Dockerfile:2 DL3009 info:
    Delete the apt-get lists after installing ...
Dockerfile:2 DL3015 info:
    Avoid additional packages by specifying ...
Listing 3: Einsatz von Hadolint

Wie fast jedes Werkzeug lässt sich Hadolint auch über eine Konfigurationsdatei oder Umgebungsvariablen an die eigenen Ansprüche anpassen. So werden beispielsweise diverse Ausgabeformate unterstützt, um das Ergebnis auch in Reports zu integrieren. Um Hadolint einfach nur einmal auszuprobieren, kann auch die Onlineversion unter https://hadolint.github.io/hadolint genutzt werden.

Was Hadolint für Dockerfiles ist, ist kube-score für Kubernetes. Hiermit lassen sich Kubernetes-Manifeste auf gewisse Regeln überprüfen (s. Listing 4). Auch hier gibt es unter https://kube-score.com eine Onlineversion, um das Werkzeug zu testen, ohne es installieren zu müssen.

$ cat busybox.yaml
apiVersion: v1
kind: Pod
metadata:
  name: busybox
spec:
  restartPolicy: Always
  containers:
    - name: busybox
      image: busybox
      command:
        - sleep
        - "3600"
      imagePullPolicy: IfNotPresent
$ kube-score score busybox.yaml
v1/Pod busybox
    path=.../busybox.yaml
    [CRITICAL] Container Security Context User Group ID
        busybox -> Container has no configured security context
            ...
    [CRITICAL] Container Security Context ReadOnlyRootFilesystem
        busybox -> Container has no configured security context
            ...
    [CRITICAL] Container Image Pull Policy
        busybox -> ImagePullPolicy is not set to Always
            ...
    [CRITICAL] Container Resources
        busybox -> CPU limit is not set
            ...
        busybox -> Memory limit is not set
            ...
        busybox -> CPU request is not set
            ...
        busybox -> Memory request is not set
            ...
    [CRITICAL] Container Ephemeral Storage Request and Limit
        busybox -> Ephemeral Storage limit is not set
            ...
        busybox -> Ephemeral Storage request is not set
            ...
    [CRITICAL] Pod NetworkPolicy
        The pod does not have a matching NetworkPolicy
            ...
    [CRITICAL] Container Image Tag
        busybox -> Image with latest tag
            ...
Listing 4: Einsatz von kube-score

Bei beiden Werkzeugen bietet es sich an, diese innerhalb der eigenen Pipeline laufen zu lassen, um Fehler und potenzielle Probleme so früh und schnell wie möglich sichtbar zu machen und anschließend zu beheben.

docker-ls

Immer mal wieder möchte ich wissen, ob es in einer Container-Registry ein neueres Tag für ein bestimmtes Image gibt. Lange bin ich dazu auf die Weboberfläche der Registry gegangen und habe mir dort die Information zusammengesucht. Aber schon der Wechsel in den Browser ist ein Kontextwechsel, den ich gerne vermeiden möchte.

Hier schafft das Werkzeug docker-ls Abhilfe. Mit diesem ist es möglich, sich auf der Kommandozeile Informationen aus einer Remote Registry zu einem Image zu holen. Dabei können sowohl alle zu einem Image vorhandenen Tags aufgelistet werden als auch Informationen, vor allem das zugehörige Digest, zu einem spezifischen Tag abgefragt werden (s. Listing 5).

$ docker-ls tags library/redis
requesting list . done
repository: library/redis
tags:
- "2"
...
- 7.4-rc2-bookworm
- 7.4.0
- 7.4.0-alpine
...
- stretch
- windowsservercore
$ docker-ls tag library/redis:7.4.0
requesting manifest . done
repository: library/redis
tagName: 7.4.0
digest: sha256:e30eac723e308f27e92cb609024b6be3ed2b2fb67899ec37043f743b169e17a5
layers: []
Listing 5: Nutzung von docker-ls

Das Digest wird vor allem benötigt, wenn wir innerhalb unseres Projekts die genutzten Images nicht nur auf ein spezifisches Tag, sondern wirklich auf eine exakte Version festzurren möchten. Ein normales Tag, wie in Listing 5 7.4.0, verweist bei Containern schließlich nicht zwangsweise immer auf den exakt gleichen Stand, da es sich hierbei um „Floating“ Tags handelt, die im Laufe der Zeit verändert werden können.

Auch docker-ls lässt sich bei Bedarf etwas konfigurieren und auch hier ist es möglich, die Art der Ausgabe, durch go-Templates, zu beeinflussen. Weiterhin ist zu beachten, dass bei offiziellen Images, wie hier beim Image für redis, ein library/ vorgestellt werden muss.

dive

Eine der Besonderheiten von Containern besteht im aus Layern bestehenden Dateisystem. Möchte man diese Änderungen, die ein solcher Layer verursacht hat, nachvollziehen, ist dive das Werkzeug meiner Wahl.

Nachdem dive mit dem Namen eines Images aufgerufen wurde, hier redis, öffnet sich eine an Lazydocker oder k9s erinnernde Oberfläche (s. Abb. 6). Hier werden auf der linken Seite, neben den Details zum Image, alle im Image vorhandenen Layer und die Informationen zum aktuell ausgewählten Layer angezeigt. Der ausgewählte Layer lässt sich dabei per Pfeiltasten auswählen.

Abb. 6: dive im Einsatz

Auf der rechten Seite ist dann die Hauptinformation zu sehen, die eigentlichen, durchgeführten Änderungen. Wir können dabei, mittels Drücken von Tab, zwischen der Auswahl von Layern und dem Dateibaum wechseln. Somit können wir innerhalb des Dateibaumes Ordner einklappen oder einen Filter definieren.

Die Leiste am unteren Rand zeigt uns schließlich die aktuell zur Verfügung stehenden Tastaturkürzel an. Mit diesen lassen sich die angezeigten Informationen verändern. Ist beispielsweise die Layer-Auswahl aktiv, können wir uns durch Strg + A nicht nur die Änderungen, die der aktuell ausgewählte Layer erzeugt hat, anzeigen lassen, sondern auch alle Änderungen, die vorher passiert sind. Strg + L bringt uns dann wieder in den Modus, der nur die Änderungen des Layers anzeigt.

Neben dieser grafischen Anzeige bringt dive auch noch einen zweiten Modus zur Integration in Pipelines mit und lässt sich somit in eine Art Linter verwandeln. Hierzu rufen wir dive, wie in Listing 6 zu sehen, in Kombination mit der Umgebungsvariable CI auf, welche wir auf den Wert true setzen. In diesem Modus berechnet dive drei Metriken, um zu beurteilen, wie gut die Layer für das Image funktionieren. Für jede dieser Metriken gibt es dann, natürlich konfigurierbare, Schwellwerte, bei denen das Tool einen Fehler meldet.

$ CI=true dive redis
  Using default CI config
Image Source: docker://redis
Fetching image... (this can take a while for large images)
Analyzing image...
  efficiency: 96.9111 %
  wastedBytes: 5341142 bytes (5.3 MB)
  userWastedPercent: 12.6665 %
Inefficient Files:
Count Wasted Space File Path
  3   2.3 MB       /var/cache/debconf/templates.dat-old
...
  3   0 B          /var/lib/dpkg/lock
Results:
  FAIL: highestUserWastedPercent: too many bytes wasted...
  SKIP: highestWastedBytes: rule disabled
  PASS: lowestEfficiency
Result:FAIL [Total:3] [Passed:1] [Failed:1] [Warn:0] [Skipped:1]
Listing 6: Nutzung von dive

ttl.sh und Nixery

Zum Schluss kommen wir zur Unterstützung bezüglich der Registry. Bei ttl.sh und Nixery handelt es sich zwar nicht um Werkzeuge im engeren Sinne, aber sie sind trotzdem ganz nett zu kennen. Ich würde beide aber nur mit extremer Vorsicht einsetzen.

ttl.sh stellt eine anonyme Docker-Registry zur Verfügung, zu der ich Images pushen kann. Der eigentliche Clou besteht jedoch darin, dass der Tag dazu genutzt wird zu bestimmen, für wie lange das Image in dieser Registry verbleibt. So führt ein Pushen des Images ttl.sh/my_image:8h dazu, dass es für 8 Stunden in der Registry verbleibt und anschließend entfernt wird. Neben h für Stunden sind auch m für Minuten, s für Sekunden und d für Tage erlaubt. Das Maximum beträgt dabei jedoch einen Tag. Da es sich hierbei um eine anonyme Registry handelt, sollte einem dabei stets bewusst sein, dass jeder, der den Image-Namen kennt, oder errät, Zugriff auf das Image erlangen kann. Deswegen ist dies für interne Dinge oder Images, die Geheimnisse enthalten, mit Sicherheit keine gute Idee. Um kurzfristig unkritische Images mit anderen zu teilen oder um bei Open-Source-Projekten eine kurzfristige Registry für Zwischenschritte zu haben, ist diese Registry jedoch gut zu gebrauchen.

Auch Nixery stellt eine Registry zur Verfügung. In diese können wir jedoch keine eigenen Images pushen, dafür aber quasi ad hoc zusammengestellte Images pullen. Starten wir beispielsweise das Image nixery.dev/shell/git/curl, so erhalten wir ein Image, das mit installierter Bash, Git und curl ausgestattet ist. Wir können dabei beliebig viele Werkzeuge, durch Slash getrennt, angeben. Als Name für die Werkzeuge müssen dabei die passenden Nix-Pakete genutzt werden. Neben der öffentlich zur Verfügung gestellten Registry unter https://nixery.dev kann Nixery auch lokal installiert und betrieben werden. Gerade, um nur mal kurz etwas auszuprobieren, kann dieser Weg einfacher sein, als ein Basis-Image zu starten und die benötigten Pakete anschließend mittels Paketmanager zu installieren. Aus Sicherheitssicht bleibt jedoch zu beachten, dass ich hier etwas ausführe, was ich vorher nur sehr schwer inspizieren kann. Denn zu diesem Image gibt es schließlich nicht mal ein Dockerfile, in das man gucken könnte.

Conclusion

In diesem Artikel haben wir uns eine ganze Reihe von Kommandozeilenwerkzeugen angeschaut, die uns beim Umgang mit Containern und Kubernetes unterstützen:

  • Lazydocker und k9s bieten uns eine grafische und interaktive Oberfläche, um Container, Compose-Projekte und Objekte in einem Kubernetes-Cluster zu verwalten.
  • Um den Überblick über Kontexte und Namspaces bei Kubernetes zu behalten, helfen uns kube-ps1 und kubectx. Zweiteres unterstützt dann auch aktiv beim Wechseln.
  • Mittels Hadolint und kube-score können wir Dockerfiles und Kubernetes-Manifeste statisch prüfen und somit Fehler früh und schnell finden, bevor diese untergehen.
  • Mit docker-ls können wir Informationen zu Container-Images aus einer Remote Registry auslesen, ohne auf die, meistens Web-basierte, Oberfläche der Registry zu wechseln.
  • dive unterstützt uns dabei, den Überblick über die Layer in einem Image zu behalten, und kann auch als Linter genutzt werden, um sicherzugehen, dass unsere Layer nicht explodieren.
  • Und zuletzt haben wir mit ttl.sh und Nixery zwei spezielle Container-Registrys kennengelernt, die wir für die kurzzeitige anonyme Publizierung beziehungsweise für das Zusammenstellen eines spezifischen Sets von Werkzeugen innerhalb eines Images nutzen können.

Natürlich gibt es noch unzählige weitere Werkzeuge, über die es zu schreiben lohnt, wie Debugging mittels cdebug, Securityscanning mit grype, das Erzeugen von SBOMs durch syft oder weitere Linter für Kubernetes-Manifeste wie kubeconform. Und auch das Signieren von Images und deren Layer wäre einen Artikel wert, aber dieser ist am Ende angekommen.

Besonders bedanken möchte ich mich an dieser Stelle bei meinem ehemaligen Kollegen Sascha Selzer, der mich durch einen Talk auf einige dieser Werkzeuge aufmerksam gemacht hat.