Development containers

21.08.2024Tom Trapp
DevOps Containerization Developer Experience DevOps How-to Cloud Computing Microservices

Banner

Bei der Recherche zu meinem TechUp über den Dex IdP sowie bei Stefans TechUp über Ballerina bin ich über Dev Containers gestolpert und habe mich gefragt, was das ist und ob das was taugt.

Daher wollen wir uns dieses Mal Development Containers (kurz Dev Containers) genauer anschauen.

Wie immer, folgende Fragen stelle ich mir:

  • Was sind Dev Containers❓
  • Wozu brauche ich Dev Containers❓
  • Wie und mit welchen IDEs kann ich Dev Containers nutzen❓
  • Dev Containers Hands-On❓
  • Was sind die Vorteile von Dev Containers❓
  • Was sind die Nachteile von Dev Containers❓

Was sind Dev Containers❓

Auf den ersten Blick wird direkt klar, bei Dev Containers handelt es sich um Container, die speziell für die Entwicklung von Software erstellt wurden. Diese Container enthalten alle notwendigen Tools, Bibliotheken und Abhängigkeiten, die für die Entwicklung von Software benötigt werden. In ersten Linie steckt dahinter eine Spezifikation, die nicht❗️ ein weiteres Orchestrations-Tool oder eine weitere Container-Plattform Abstraktion darstellt. Die Spezifikation ist ein Standard, welche existierende Formate mit Metadaten anreichert, so lassen sich bestimmte Einstellungen, Konfigurationen und Tools definieren. Es geht hierbei rein um den Entwicklungsprozess und nicht um den Betrieb von Containern.

Die Spezifikation wird in JSON with Comments (jsonc) in einer Konfigurationsdatei geschrieben und bietet die Möglichkeiten, sogenannte Dev Container Features zu definieren. Mehr dazu später!

Technisch benötigt Dev Containers eine Docker Container-Runtime, um bestimmte Container zu starten und somit vom lokalen System zu isolieren.

Wozu brauche ich Dev Containers❓

Bei Dev Containers geht es darum, den Inner Loop sowie den Outer Loop zu spezifizieren und zu standardisieren. So lässt sicher sicherstellen, dass alle Entwickler, egal auf welchem System, ob Windows, macOS oder Linux, die gleiche Entwicklungsumgebung haben. Das Problem “Works on my machine” gehört somit der Vergangenheit an!

Was genau bedeutet das? Dev Containers verpackt und standardisiert die komplette Entwicklungsumgebung mit allen nötigen Tools, Plugins, IDEs, Abhängigkeiten und Konfigurationen in einem Container. Dieser Container kann dann auf jedem System gestartet werden, sodass alle Entwickler die gleiche Entwicklungsumgebung haben.

Ein Dev Container sieht so wie folgt aus:

img_14.png

Im Bild ist zu sehen, dass in der “alten Welt”, alles direkt auf dem System, dem localhost installiert und aufgesetzt ist. In der neuen Welt mit Dev Containers befindet sich auf dem lokalen System nur noch Docker und eine sogenannte Remove-Capable IDE, wie Visual Studio Code oder IntelliJ Idea.

Im Dev Container selbst sind dann alle SDKs, Serverinstallationen, Package-Management Tools installiert. Der eigentliche Sourcecode sowie die Dependencies und Package Downloads sind in Volume Mounts gespeichert, damit diese nicht bei jedem Start erneut initialisiert werden müssen.

Wie und mit welchen IDEs kann ich Dev Containers nutzen❓

Auch hier bietet die Webseite eine klare Antwort, generell ist die Homepage sehr schlicht, einfach aber super informativ aufgebaut!

Aktuell werden die IDEs Visual Studio Code und IntelliJ Idea unterstützt. Wichtig ist, dass die IDE die Remote Development Fähigkeit besitzt, um Dev Containers nutzen zu können.

Weitere unterstütze Tools:

  • GitHub Codespaces
  • CodeSandbox
  • DevPod
  • und weitere

Sehr nützlich ist auch die CLI-Specification mit einer Referenz-Implementation. So lassen sich Dev Containers super in einen CI-Prozess, beispielsweise mit der vorhandenen GitHub Action oder dem Azure DevOps Task, integrieren.

Dev Containers Hands-On❓

Nun wollen wir aber selbst Hand anlegen und Dev Containers ausprobieren!

Da es sich in erster Linie um eine Spezifikation mit Referenz-Implementationen handelt, suchen wir vergebens nach einem Hello-World Beispiel.

VS Code Rust Hello World

Schauen wir uns daher zuerst den Einsatz mit VS Code an, dort findet man ein Dev Container Tutorial.

Ich werde nicht komplett Schritt für Schritt durch das Tutorial gehen, sondern nur die wichtigsten Schritte hervorheben. Das fertige Projekt findest du hier.

Nachdem wir die VS Code Extension Dev Containers installiert und Docker gestartet haben, können wir unten links über das blaue Icon zwischen dem local und dem remote Context switchen. Mit Remote ist hier der Dev Containers Context gemeint.

Anschliessend können wir aus einer langen Liste von sogenannten Templates auswählen, welche Art von Dev Container wir verwenden wollen.

img.png

Ich habe mich für Rust entschieden, da ich noch nicht viele Berührungspunkte mit Rust hatte und es nicht auf meinem lokalen System installiert ist. Hierbei handelt es sich um ein offizielles Rust Template von Dev Containers.

Nach kurzer Wartezeit ist unser Rust Dev Container Projekt fertig erstellt und wir können es genau erkunden. Direkt fällt auf, dass mein VS Code Instanz nun auch im Container läuft, da meine lokalen Extensions nicht verfügbar sind. Das ist auch der Sinn dahinter, wir wollen ja eine saubere Entwicklungsumgebung haben.

Schauen wir uns kurz mit Warp an, welche Docker Container gestartet wurden:

1
2
CONTAINER ID   IMAGE                                               COMMAND                  CREATED              STATUS              PORTS     NAMES
dbde5121cbe3   mcr.microsoft.com/devcontainers/rust:1-1-bullseye   "/bin/sh -c 'echo Co…"   About a minute ago   Up About a minute             clever_brahmagupta

Schön zu sehen ist, dass ein pre-build Container von Microsoft verwendet wird, welcher wohl schon das Grundgerüst für Rust sowie VS Code Remote enthält.

Schauen wir uns nun das generierte Projekt genauer an!

1
2
3
4
5
6
7
.
├── .devcontainer
│   └── devcontainer.json
└── .github
    └── dependabot.yml

2 directories, 2 files

Wirklich viel ist nicht erstellt worden, leider wurde auch nicht das Rust Projekt erstellt, sondern nur die Konfiguration für den Dev Container.

Im .github Ordner finden wir ein dependabot.yml File, welches uns zeigt, dass auch Dependabot bereits konfiguriert ist. Mit Dependabot werden automatisch Pull-Requests erstellt, wenn Abhängigkeiten aktualisiert werden müssen.

Im .devcontainer Ordner finden wir die Konfiguration für den Dev Container. In diesem Fall befindet sich hier die devcontainer.json Datei, welche wir uns genauer anschauen wollen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
  "name": "Rust",
  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
  "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye"

  // Use 'mounts' to make the cargo cache persistent in a Docker Volume.
  // "mounts": [
  // 	{
  // 		"source": "devcontainer-cargo-cache-${devcontainerId}",
  // 		"target": "/usr/local/cargo",
  // 		"type": "volume"
  // 	}
  // ]

  // Features to add to the dev container. More info: https://containers.dev/features.
  // "features": {},

  // Use 'forwardPorts' to make a list of ports inside the container available locally.
  // "forwardPorts": [],

  // Use 'postCreateCommand' to run commands after the container is created.
  // "postCreateCommand": "rustc --version",

  // Configure tool-specific properties.
  // "customizations": {},

  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
  // "remoteUser": "root"
}

Wir sehen viele Auskommentiere Zeilen, welche uns Hilfestellungen für weitere Konfigurationen geben. Definiert sind zwei Properties, name und image. Der Name ist selbsterklärend, das Image ist der Docker Container, welcher verwendet wird.

Öffnen wir das VS Code Terminal, so befinden wir uns direkt im Container und somit auch im isolierten Filesystem. Wenn wir beispielsweise rustc --version ausführen, sehen wir, dass Rust bereits installiert ist. Und sogar mit der aktuellsten Version!

Nun aber zur eigentlichen Implementation, wir legen uns ein main.rs File an und schreiben ein einfaches Hello World Programm.

1
2
3
fn main() {
    println!("Hello, world from Tom!");
}

Speichern wir das File, so können wir es direkt im Terminal kompilieren und ausführen.

1
2
rustc main.rs
./main

Und wir sehen, das unser Hello World Programm erfolgreich ausgeführt wurde! 🚀

Kurzer Recap:

  • VS-Code läuft im Container
  • Rust ist bereits installiert im Container
  • Wir haben ein Hello World Programm geschrieben und ausgeführt im Container

Kurzer Exkurs, der Mensch ist ja ein Gewohnheitstier …

Solch ein Blogpost entsteht (leider) nicht immer an einem Stück, also setzte ich mich ein paar Tage später wieder an das Dev Containers TechUp und wollte, bis zu den Haarspitzen motiviert, weiter die Welten der Dev Containers erkunden und niederschreiben. Wie gewöhnlich öffnete ich meine Folderstruktur mit Warp und war erstaunt, wo ist denn mein Dev Containers Projekt hin? 🤔 Nach etwas Ärgernis, Frust und Selbstzweifel habe ich dann gemerkt, dass es sich ja um eine containerisierte Variante handelt und ich somit nicht mehr in meinem lokalen Filesystem, zumindest nicht in meiner gewohnten Orderstruktur, bin. 🤦‍ Glücklicherweise sind Dev Containers Projekts auch via den Recent Projects im VS Code auffindbar, Glück gehabt! 😅

Nun können wir unser komplettes Projekt in einem Git Repository speichern und mit anderen Entwicklern teilen. Diese können dann das Projekt direkt in ihrem VS Code öffnen und haben die gleiche Entwicklungsumgebung wie wir.

Wie gewohnt erstellen wir ein Repository auf GitHub und pushen unser Projekt. Glücklicherweise mounted Dev Containers unsere Credentials, sodass wir uns nicht nochmal authentifizieren müssen. Sehr angenehm!

Das Repo findest du hier.

Setup on another Developer’s Machine

Schauen wir uns nun aber noch kurz an, wie ein anderer Entwickler dieses Projekt bei sich lokal aufsetzen würde.

Wir haben nun ein komplett blankes Setup, wir haben keinen laufenden Docker Container, haben aber Dev Containers schon in VS Code installiert.

1
2
docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Selbstverständlich habe ich mit docker volume rm $(docker volume ls -q) alle Volumes zuvor gelöscht.

Auf der VS Code Startseite kann ich nun Conntect to... und dann Clone Repository in Container Volume auswählen. Hier gebe ich den Link zu meinem GitHub Repository ein und wähle den Dev Container aus.

img_1.png

Und Schwups ist unser Projekt ready! 🚀 Es wurde ein neuer Docker Container gestartet. Kann ich nun das Rust Projekt direkt kompilieren und ausführen?

1
2
rustc main.rs
./main

Funktioniert, sehr cool!

img_2.png

Und auch hier kann ich direkt wieder Änderungen committen und pushen, ohne mich erneut authentifizieren zu müssen.

Selbstverständlich gibt es auch noch einen coolen Link zu dem Projekt, welcher das Setup direkt automatisch macht! Dieser Link bereitet VS Code entsprechend vor und öffnet direkt unser Projekt, sehr elegant! Speziell hilfreich für den Einsatz in IDPs (Internal Development Portals / Platforms), als Landing-Page o.ä.

Unser Hello World Projekt ist damit abgeschlossen!

Java Rest API mit IntelliJ Idea

Schauen wir uns nun noch ein weiteres Beispiel an, diesmal mit IntelliJ Idea und einer Java Rest API.

Nehmen wir an, wir haben ein vorhandenes Projekt, eine einfache Quarkus Rest API, welche wir nun in einem Dev Container laufen lassen wollen. Das Dev Container Setup wurde noch nicht gemacht, wir portieren also eine vorhandene Applikation in einen Dev Container.

Zuerst müssen wir IntelliJ öffenen und in das Remote Development Fenster wechseln. Hier können wir ein neues Projekt erstellen und ein Dev Container Template auswählen.

Hier wöhlen wir Dev Container aus.

img_4.png

Anschliessend dann können wir ein neues Dev Container Projekt von einem Git Repository erstellen. Wir wählen Docker als Laufzeitumgebung, geben unser Repo und den entsprechenden Branch an.

img_5.png

Anschliessend können wir dann ein Template auswählen, in unserem Fall Java in der Version 21. Zusätzlich wollen wir in unserem Container noch Maven installieren.

img_6.png

Und anschliessend sind wir in unserem Dev Container Projekt! 🚀 Das Projekt wurde ausgecheckt und IntelliJ Idea hat die entsprechenden Einstellungen vorgenommen.

img_3.png

Können wir nun, ohne vorherige Dev Container Konfiguration das Projekt direkt starten? Leider nicht, unser JAVA_HOME ist nicht gesetzt und somit kann IntelliJ Idea die Java Applikation nicht starten.

img_7.png

Java an sich ist aber erfolgreich im Dev Container installiert worden.

Erstaunlicherweise lassen sich die Maven Targets via IntelliJ über das Maven Plugin ausführen, scheinbar wird dort nochmal speziell etwas konfiguriert. Passt aber so von unseren UseCase, also starten wir mal den Quarkus Server.

img_8.png

Und der Quarkus Server startet erfolgreich! 🚀

Hmm, unter localhost:8080 ist aber nichts zu sehen, was ist da los? 🤔 Kurz nachgedacht, wir haben den Quarkus ja in einem Container laufen, woher soll unser Container aber nun wissen, das der Port in unser Hostsystem gemappt werden soll?

Laut Dokumentation ist das recht einfach, leider hat unser Projekt aber keine devcontainer.json Datei, welche wir anpassen könnten. Versuchen wir es nochmals zu importieren, vielleicht haben wir ein Haken irgendwo vergessen.

Long Story short, nö leider nicht, das File ist nicht auffindbar, laut Jetbrains Dokumentation sollte es vorhanden sein. Auch das find Command findet die Datei nicht, schade! Das ist recht doof, da wir so nicht wirklich Customization machen können.

Liest man die Dokumentation aber nochmal ganz genau fällt auf, dass es heisst The project to which you are referring should have a devcontainer.json file that contains the dev container configuration.. Das erklärt natürlich, wieso es keine devcontainer.json Datei gibt.

In unserem Remote Development Projekt können wir über new --> Dev Container eine neue Konfigurationsdatei erstellen.

img_9.png

Wählen wir nun wieder die gleichen Konfigurationen wie beim initialen Starten, und schwups, haben wir eine devcontainer.json Datei.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/java
{
  "name": "Java",
  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
  "image": "mcr.microsoft.com/devcontainers/java:1-21-bullseye",

  "features": {
    "ghcr.io/devcontainers/features/java:1": {
      "version": "none",
      "installMaven": "true",
      "installGradle": "false"
    }
  },

  // Use 'forwardPorts' to make a list of ports inside the container available locally.
  // "forwardPorts": [],

  // Use 'postCreateCommand' to run commands after the container is created.
  // "postCreateCommand": "java -version",

  // Configure tool-specific properties.
  "customizations" : {
    "jetbrains" : {
      "backend" : "IntelliJ"
    }
  },

  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
  // "remoteUser": "root"
}

Die Konfiguration sieht sehr standardmässig aus, wir haben einen Namen, ein Image, sowie weitere Image-spezifische Konfigurationen wie Maven und Gradle. Zum Schluss wird spezifiziert, dass IntelliJ genutzt werden soll.

Ok weiter gehts, wir können nun "forwardPorts": [8080], einfügen (einkommentieren), damit der Port in unser Hostsystem gemappt wird.

Gut und nun? 🤔 Wie aktualisiere ich nun die Dev Container Konfiguration? 🤔

Starten wir unseren Container einfach mal neu, via IntelliJ. Wir sehen, leider ist der Port weiterhin nicht gemappt.

1
2
3
docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                      NAMES
73ac882ab5ea   a9b588fe3311   "/bin/sh -c 'while s…"   59 minutes ago   Up 29 seconds   0.0.0.0:62581->12345/tcp   magical_mcclintock

IntelliJ bietet uns im Remote Development Fenster einen magischen Hammer, damit können wir einen Rebuild ausführen und den Container erneut bauen.

img_rebuild.png

Und, Ernüchterung, der Port ist weiterhin nicht gemappt. 🤦‍

Versuchen wir doch mal das devcontainer.json File zu committen und dann alles nochmal von vorne, ganz frisch zu starten. Punkteabzug für IntelliJ, meine Git-Credentials sind nicht gemapped, ich muss mich also erneut authentifizieren.

Auch nach komplettem Neustart des Projekts, leider kein Erfolg, der Port ist weiterhin nicht gemappt.

VS-Code? 🤔 Dort können wir wählen Clone Remote Repository in Dev Container Volume und unser Projekt auschecken.

Dort kommt ein kleines Pop-up, welches uns die gemappten Ports zeigt, 8080 wurde hier von VS Code auf 8081 gemappt, darüber können wir unsere Applikation aufrufen.

img_10.png

Bei erneuten Starten von IntelliJ hat es dann aber auch mit IntelliJ ohne weitere Änderungen funktioniert.

img_11.png

Spannend aber zu sehen, dass das Container-Port-Mapping nicht via docker ps zu sehen ist. 🤔 Das deutet darauf hin, dass das Port-Mapping innerhalb des Containers nochmals gemappt werden muss.

Auch eine Änderung an unserem Projekt können wir nun vornehmen und lokal testen.

img_12.png

Gut, geschafft! Das war deutlich umständlicher mit IntelliJ als mit VSCode.

Was sind die Vorteile von Dev Containers❓

Aus meiner Sicht klar die Standardisierung sowie die Portierung der Entwicklungsumgebung. Works on my local sollte somit Geschichte sein. Auch wird es durch Dev Containers deutlich angenehmer mit mehreren Projekten gleichzeitig zu arbeiten, welche unterschiedliche Abhängigkeiten haben. Dank der Isolation der Container können wir sicher sein, dass sich die Projekte nicht gegenseitig beeinflussen, Stichwort Node.js oder Java Versionen.

Mit Dev Containers kann man sich auf seine IDE speziell für das eine Projekt mit Settings, Plugins, Code-Styles usw. perfekt und massgeschneidert einrichten! Und das auch noch mit allen anderen Entwicklern im Team teilen.

Was sind die Nachteile von Dev Containers❓

Die Unterstützung in IntelliJ hat sicher noch Potenzial, der VCS Ansatz ist aber auch noch in der Beta-Phase. Spannend wäre sicher zu sehen, wie es sich mit riesengrossen Monolithen verhält, ob die Container dann noch performant sind. Auch frage ich mich, wie es ist, wenn man mit mehreren Projekten gleichzeitig arbeiten will und dann mehrere Container laufen, wie da die Performance ist.

Fazit

Spannendes Thema, wir werden das sicherlich weiter verfolgen. Aus meiner Sicht wären die nächsten Steps ein internes Projekt, beispielsweise unsere Homepage zu devcontainerisieren und zu schauen, wie das funktioniert. Zusätzlich würden wir Dev Containers dann direkt mit Daytona verbinden.

Wie wäre es nun, wenn der Dev Container in ähnlicher Art und Weise nicht lokal, sondern in der Cloud laufen würde? 🤔 Stay tuned!

Tom Trapp

Tom Trapp – Problemlöser, Innovator, Sportler. Am liebsten feilt Tom den ganzen Tag an der moderner Software und legt viel Wert auf objektiv sauberen, leanen Code.