Container-Technologien wie zum Beispiel Docker stehen bei Entwicklern, CTOs und IT-Architekten weit oben auf der Agenda. Welche Methoden und Tools haben sich bewährt und was sollte man bei ihrer Nutzung beachten?
In den letzten Jahren war in der Softwareentwicklung ein Trend hin zu verteilten Systemen und Anwendungen unter dem Architekturmuster der Microservices zu verzeichnen. Infrastruktur-Technologien wie die Containerisierung sind Teil dieser Entwicklung und sorgen für eine Adaption der innovativen Konzepte im Cloud-Computing-Bereich. Gerade bei Entwicklern wird die Containerisierung mittels Docker immer beliebter. Dies hat mehrere Gründe. Die relevantesten sind sicherlich die Geschwindigkeit und Konsistenz, die sich bei der Entwicklung, beim Debugging und Testen mithilfe von Containern ergeben.
Docker: Was ist es, was kann es, wie funktioniert es?
Das Konzept von Containern ist eine neue Variante der Virtualisierung mit einem Fokus auf PaaS. Im Vergleich zu virtuellen Maschinen sind sie schlanker und lassen sich einsetzen, um Applikationen fertig gepackt zum Testen und Deployment auszuliefern. Während bei virtuellen Maschinen die Hardware emuliert wird, erfolgt die Virtualisierung bei Containerlösungen wie Docker eine Ebene höher, nämlich beim Betriebssystem. Dies wird durch Linux-Kernel-Features wie Cgroups und Namespaces realisiert. Mittels dieser können Prozesse und deren Ressourcen voneinander isoliert und auch limitiert werden. Dadurch ist das Betriebssystem beziehungsweise dessen Kernel nicht Teil eines Containers und entsprechend kompakter sowie schneller und einfacher nutzbar. Der Container enthält lediglich die Abhängigkeiten, die für das auszuführende Programm benötigt werden.
Die in der Sprache Go geschriebene Containerlösung Docker wurde von Docker Inc. erstmals im März 2013 veröffentlicht. Mit der Docker-Runtime ist eine zusätzliche Schicht zwischen Applikation und Host-Betriebssystem gegeben, in welchem sich Docker-Container ausführen lassen. Jeder dieser Container bringt alle benötigten Abhängigkeiten für die jeweilige Applikation mit und läuft in einer eigenen Umgebung. Verteilt werden Container als Docker-Images, gewissermaßen Templates zur Erstellung von Containern. Innerhalb einer Docker-Runtime oder einem Cluster können mehrere Container gleichzeitig laufen. Startet man mehrere Container mit demselben Image, so sind diese zunächst genaue Replikate. Das geniale ist nun, dass man weitere Veränderungen vornehmen kann, ohne dieses Base-Image zu modifizieren. Die Technik, die dahintersteckt, ist das UnionFS. Ein Dateisystem, welches ursprünglich für das Plan9-Betriebssystem entwickelt wurde. Mit diesem können Verzeichnisbäume in so genannte Layer übereinandergelegt werden. Diese werden dann in einer logischen Sicht zusammengefügt, sodass der oberste Layer derjenige ist, dessen Sicht ausschlaggebend ist. Können dort Dateien nicht gefunden werden, werden diese in den unteren Layern gesucht. Ein Entwickler hat daher beispielsweise die Möglichkeit, ein Ubuntu-basiertes PHP-Base-Image zu benutzen und eigenen Code oder Extensions hinzuzufügen. Wird dieses Ubuntu-Base-Image gepatched, hat der Entwickler die Möglichkeit, dieses zu beziehen und seine Modifikationen darüber zu legen. Das erhöht den Komfort und spart Zeit:
Der Entwickler muss sich zum Beispiel nicht damit beschäftigen, dass die Versionen der Pakete auf dem neusten Stand sind, sondern er kann sich auf die Weiterentwicklung seines Codes fokussieren. Auch Operations profitiert enorm, da gewährleistet wird, dass die Entwicklungsumgebung und das Produktivsystem sich aus denselben Images ableiten. Damit kommt es zu keinen negativen Überraschungen à la „bei mir lokal hat es aber noch funktioniert“. Es gibt jedoch eine Einschränkung, die bei einer effizienten Umsetzung von Container-Projekten zu berücksichtigen ist. Docker selbst bringt keinen Ressource-Scheduler, keine Hochverfügbarkeit oder dergleichen mit. Hierfür gibt es aber Lösungen wie beispielsweise Docker Swarm oder Kubernetes.
Wichtig ist in diesem Kontext, dass Container standardmäßig als „Immutable Infrastructure“ zu verstehen sind. Das bedeutet, dass Container intern keine persistenten Daten halten. Soll auf persistente Daten zugegriffen werden, müssen diese als externes Volume in den Container eingebunden (gemountet) werden. Dies bedeutet für Systemadministratoren, dass zum Beispiel Bugfixes niemals in einem Container stattfinden dürfen, da dieser jederzeit zerstört oder neu gestartet werden kann. Die Änderungen wären dann unwiederbringlich verloren. Der richtige Weg ist es, das Problem zu debuggen und im Image zu beheben. Das Image kann im Anschluss neu ausgerollt werden. Ein beispielhafter Workflow hierfür wäre:
- „Check out“ des Git-Repositories, welches die Templates, Skripte, gegebenenfalls Binaries und Konfigurationsdefinitionen für das Image bereithält
- Generieren eines neuen Branches
- Erstellen des Bugfixes
- Lokales Testen via docker-compose
- Wenn nicht funktional: re-iterieren
- Wenn funktional: „committen“, Pull-Request erstellen und die Änderungen zum Master-Branch „mergen“
- Die Delivery-Pipeline anstoßen (lesen Sie zur Delivery-Pipeline den Folgeartikel in diesem Blog)
- Resource-Scheduler: Platziert Pods/Docker-Container anhand der Ressourcenanfragen auf den verfügbaren Compute-Nodes.
- Auto-Scaling: Ermöglicht eine automatisierte Anpassung der benötigten Compute-Nodes aufgrund Lastenänderungen. Bei wachsenden Anforderungen nimmt Kubernetes Compute-Nodes zum Cluster hinzu und reallokiert dynamisch die Ressourcen.
- Self-Healing: Der kubelet-Service überwacht alle laufenden Docker-Container und startet diese bei Bedarf neu. Dies erfolgt automatisiert anhand vorher festgelegten Regeln und befreit den Administrator von lästigen, manuellen Zugriffen.
- Service-Discovery: Eine voll funktionsfähige Service-Discovery mittels DNS, um Verbindungen zu anderen Containern zu erkennen, ist standardmäßig integriert. Alle Pods und alle Service werden Cluster-intern registriert und anhand des Health-Status unter cluster.local propagiert.
- Rolling-Upgrades/ Rollbacks: Die Deployment-Ressource unterstützt Rolling-Upgrades anhand sequenzieller Re-Deployments von allen betreffenden Pods.
- Secret / Configuration-Management: Ist standardmäßig integriert und ermöglicht das sichere Handling von beispielsweise Passwörtern oder API-Keys.
- Storage-Orchestration: Out-of-the-Box werden diverse Storage-Lösungen wie Google GCE PD, AWS EBS, S3, Ceph, NFS oder iSCSI unterstützt. Diese können direkt an den Cluster angeschlossen werden und beispielsweise als externe Volumes für persistente Daten benutzt werden.
Kubernetes: Nicht nur ein Docker-Orchestrierungstool
Zum effizienten Managen von Containern ist der Einsatz einer Clusterverwaltung von Vorteil. Kubernetes ist ein von Google Inc. als Open-Source-Software entwickeltes Management-Tool für die Bereitstellung, Verwaltung und Überwachung einer Container-Umgebung. Diese ermöglicht das Hosten von sogenannten Kubernetes-Clustern, welche sich aus Compute-Nodes zusammensetzen. In diesen können eine große Anzahl von Containern, basierend auf vordefinierten Templates (Kubernetes-Manifests), betrieben werden. Dabei werden von Kubernetes verschiedene Docker-Hosts, sogenannte Nodes, betrieben. Auf diesen lassen sich als kleinste Einheiten sogenannte „Pods“ verteilen. Ein Pod repräsentiert hierbei gewöhnlich eine Applikation, kann aber mehrere Container enthalten. Ein „kubelet“ genannter Agent überwacht die Pods und kann bei einem Ausfall unter anderem einen neuen Pod auf einem anderen Node starten. Auf einem höheren Level lassen sich auch größere Vernetzungen von Containern in mehreren Pods definieren, etwa in Deployments.
Die wichtigsten Features von Kubernetes zusammengefasst:
Kubernetes hilft uns also Docker-Container hochverfügbar, skalierbar und hochperformant zu betreiben.
Lesen Sie im zweiten Teil dieser Artikelserie: Continuous Delivery Pipeline: Wie Deployment-Workflows optimal automatisiert werden