Docker und Kubernetes: Die absoluten Grundlagen gleichzeitig lernen

Erstmal Tacheles

Es gibt tatsächlich, junge, schlaue und hübsche Entwickler, die in ihrem bisherigen Leben erfolgreich Docker vermieden haben.

Dazu gehöre auch ich, und vielleicht auch du – aber das soll sich ändern.

Warum sollst du dich also mit Docker beschäftigen, wenn doch bisher auch alles irgendwie ohne lief? Und warum spricht die ganze Welt nicht nur davon, sondern auch von Kubernetes?

All das kauen wir jetzt mal durch. Und wer weiß, vielleicht änderst du ja sogar deinen Entwickleralltag und stehst mal früh auf!

Docker: Basics

Vielleicht geht es dir wie mir: Zum Entwickeln hast du dein Windows-System, oder eine virtuelle (oder echte) Linux-Maschine und die Reste von deinem ersten XAMPP, Version 1.8, liegen noch auf deiner Platte rum. Das führt dann dazu, dass dein Visual Studio Code denkt, du hättest noch PHP 5.6.

Aber irgendwie läuft doch alles… Bis es das nicht mehr tut, weil ein Projekt PHP 5.6 benötigt (ja, solche Projekte gibt es noch), und das andere PHP 7.4.

Jetzt hast du zwei Möglichkeiten:

  • Ein paralleles System aufsetzen
  • Dich erschießen, weil du was mit PHP 5.6 machen musst

Nr. 2 beendet den Entwicklungszyklus und behindert die Agilität, deshalb ärgerst du dich und wählst Nr. 1. Oder Stopp! Du nimmst Docker!

Mit Docker lädst du dir vorgefertigte Images runter, die du als eigenen Container auf deinem Rechner laufen lässt. Dabei ist es egal, ob du Windows, Linux oder einen Abakus benutzt. Docker läuft überall.

Mit Docker sagst du also: “Lade dir das Image von PHP 5.6 mit Apache runter, starte es und lass es auf Port 7382 laufen! Außerdem hätte ich gerne PHP 7.4 auf Port 2341.

Docker sagt dann: “Jo klar, voll gerne, mach ich.”

Und in (ungelogen) wenigen Sekunden ist dein System einsatzbereit. Wir lernen also: Ein Image ist eine Basis-Datei, aus der der Container entsteht. Der Container selbst ist eigentlich auch nur eine Datei – und ein lebendes Abbild des Images.

Die Vorteile von Docker

Das Image ist eine Grundlage für alle Programme, die du laufen lassen möchtest – z.B. Apache, MySQL oder MongoDB. Viele offizielle Images findest du auf dem Docker-Hub.

Docker startet diese dann in einem eigenen Container, d.h. in einer Kopie des Images. Üblicherweise hast du einen eigenen Container für deine Dienste, also einen für Apache, für PHP und für MySQL.

Solltest du es nicht anders konfigurieren, werden alle Daten (z.B. die Datenbankdateien und Logs) auch im Container gespeichert, denn dieser ist nichts anderes als eine eigene virtuelle Linux-Maschine, in die du dich auch einloggen kannst. Löschst du den Container, gehen auch deine Daten verloren – aber dafür gibt es die Volumes; dazu in einem anderen Artikel mehr.

Es ist also möglich, deinen kompletten Software-Stack in wenigen Dateien zu definieren, so dass ein zweiter Entwickler nur einen Befehl eingeben muss, um exakt die gleiche Umgebung wie du zu bekommen.

Auch wenn du mehrere Instanzen deiner Software brauchst: Mach doch einfach selbst ein Image und starte es auf anderen Servern!

Die Nachteile von Docker

Wenn man mal ehrlich ist, eignet sich Docker doch alleine nicht so wirklich für deine Live-Server. Zum Entwickeln ist es wunderbar, einen Webserver mal schnell hochzufahren, aber im echten Betrieb? Da möchtest du vielleicht skalieren oder besondere Einstellungen vornehmen.

Denn eins ist klar: Auch, wenn du einen MySQL-Server mit einem Docker-Befehl starten kannst, heißt das nicht, dass du dich nicht mit MySQL und dem darunter liegenden Betriebssystem auseinandersetzen musst. Denn tatsächlich ist ein MySQL-Server auch mit ein paar Befehlen mehr ohne Docker installiert.

Dennoch: Der Charme, die ganze Umgebung mit Skripten und Config-Dateien vorzukonfigurieren und einfach starten zu können, hat was!

Kubernetes: Basics

Ganz simpel gesagt: Mit Kubernetes steuerst du deine Docker-Container auf vielen Servern, um eine hohe Ausfallsicherheit zu gewährleisten.

Du sagst Kubernetes: “Ich habe hier ein Apache-Docker-Image. Kannst du bitte dafür sorgen, dass davon immer drei Stück laufen?”

Kubernetes sagt dann: “Sehr geehrter Herr, es würde mir eine Freude sein, Ihnen diesen Wunsch zu erfüllen!” (Kubernetes ist höflicher)

Aber es kann noch viel mehr: Es betreut nicht nur einen Server, sondern eine ganze Armada davon. Du hast 10 Server und willst auf jedem davon einen Docker-Container laufen lassen? Kein Problem. Du bekommst sogar automatisch dein eigenes IP-Netz und einen übergreifenden Speicher.

Und das beste: Auch Kubernetes ist mit Skripten konfigurierbar. Du kannst also mal eben dutzende eigene gigantische Server-Applikationen mit einem Befehl steuern oder deine App doppelt hochskalieren.

Ans Eingemachte

OK, das reicht erstmal als Grundlage. Wir machen jetzt folgendes:

  • Docker installieren
  • Kubernetes installieren
  • Einen NGINX Webserver mit Docker aufsetzen
  • Einen NGINX Webserver mit Kubernetes aufsetzen

Docker installieren

Docker gibt es für Windows, Mac und Linux. Für Ubuntu 20 sind folgende Befehle nötig:

sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
sudo apt update
sudo apt install docker-ce docker-compose -y

Kubernetes installieren

Es gibt viele verschiedene “Kubernetes-Apps”, z.B. microk8s oder die “offizelle Version“. Ich empfehle das schlanke K3s. Das wird so installiert:

curl -sfL https://get.k3s.io | sh -s - --docker

# Service überprüfen
sudo systemctl status k3s

# Service starten
sudo systemctl start k3s

Alle Versionen liefern das Programm kubectl mit, mit dem du den Cluster steuerst.

Achtung: Genaugenommen benötigst du für dieses Tutorial Docker, um mit Kubernetes zu arbeiten. Du hast aber später die Möglichkeit, auch einen anderen Container-Dienst zu nutzen.

Verzeichnisse anlegen

Als nächstes legst du in einem beliebigen Verzeichnis folgende Verzeichnisstruktur an:

  • docker: Hier landet die Docker-Version des Projekts
  • kubernetes: Hier landet die Kubernetes-Version des Projekts

NGINX mit Docker starten

Du kannst Docker direkt über die Shell steuern. Theoretisch würde folgender Befehl reichen:

docker run -p 7080:80 nginx

Damit kannst du in deinem Browser http://<Server IP>:7080 aufrufen und die Willkommensnachricht von NGINX genießen.

Aber! Wir nutzen die Config-Datei docker-compose.yml, um alles zu konfigurieren. Docker-Compose hilft dir, mehrere Container gleichzeitig zu starten – genau wie Kubernetes, nur eben auf nur einem Server, anstatt in einem Cluster.

Also lege die Datei docker/docker-compose.yml mit folgendem Inhalt an:

# Wir benutzen folgende Docker-Compose Version
version: '3'

# Hier werden die Container aufgelistet
services:
  # Der NGINX-Container
  nginx:
  
    # Das grundlegende Image, das von Docker-Hub geladen wird
    image: nginx
    
    # Der Port 80 des Containers soll auf den Port 7080 unseres Servers
    # gemappt werden
    ports:
      - "7080:80"

Wechsel nun in das Verzeichnis /docker und starte den Dienst:

sudo docker-compose up

Das Ergebnis sollte so aussehen:

Folgendes ist passiert: Docker hat das aktuellste Image von NGINX runtergeladen und den Container docker_nginx_1 gestartet.

Der Webserver sollte jetzt unter http://<Server IP>:7080 erreichbar sein.

In diesem Fall läuft Docker nicht als Daemon, d.h. du kannst den Container mit Strg+C beenden.

Images und Container prüfen

Hier ein paar wichtige Befehle, um dir alle Images und Container anzeigen zu lassen. Außerdem eine Möglichkeit alle zu entfernen – was besonders beim Lernen hilfreich sein kann. Es gibt übrigens viele Aliases, die genau das gleiche tun.

# Alle Images anzeigen
docker image ls

# Alle Container anzeigen (auch inaktive)
docker container ls -a

# Alle Container löschen
docker rm -vf $(docker ps -a -q)

# Alle Images löschen
docker rmi -f $(docker images -a -q)

NGINX mit Kubernetes starten

Um es nochmal zu betonen: Kubernetes ist kein Ersatz für Docker, sondern benötigt es in unserem Fall sogar, um das gleiche zu tun, was wir oben getan haben. Tatsächlich sind die Container, die Kubernetes erstellt, auch mit den Docker-Befehlen sichtbar. Kubernetes ist sozusagen Docker Compose auf Steroiden.

Allerdings muss man zunächst verstehen, wie Kubernetes die Docker-Container startet, bzw. worauf wir achten müssen.

Als erstes erstellen wir ein Deployment. Damit sagen wir Kubernetes: “Hey, erstelle bitte einen Container aus dem Image NGINX”. Folgendes muss also in die Datei kuberenetes/nginx-deployment.yml:

# Wir nutzen die erste Version
apiVersion: apps/v1

# Diese Datei beschreibt ein Deploment
kind: Deployment

# Name und Label dieses Deployments
metadata:
  name: nginx
  labels:
    app: nginx
    
# Weitere Spezifikationen    
spec:
  # Anzahl der Container
  replicas: 1
  
  # Das Deployment soll das Template weiter unten finden
  selector:
    matchLabels:
      app: nginx
      
  # Infos zum Image      
  template:
    # Alle Container haben folgende Labels
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      # Image und (native) Ports der Container
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

OK, das ist etwas umfangreicher. Bevor wir das Deployment starten, ein paar kurze Hinweise:

  • Es gibt kein Portmapping, d.h. wir geben hier nur die Ports an, die das Image freigibt (bei MySQL wäre das z.B 3306).
  • Es gibt eine Menge von Labels. Labels werden benutzt, um Container, Images, Deployments und alles andere, was Kubernetes hat, zu kennzeichnen. Dabei kann man den Key (“app”) und den Value (“nginx”) selbst wählen.
  • Das, was wir hier definieren ist ein sogenannter Pod. Ein Pod kann mehrere Container beinhalten (unter containers).
  • Weitere Details findest du in der offizielen Doku.

Analog zu docker-compose starten wir doch einfach mal das Deployment mit:

sudo kubectl apply -f nginx-deployment.yaml

Die Antwort ist folgende:

deployment.apps/nginx configured

OK… Was heißt das jetzt?

Docker hat einen Container gestartet, der aber noch nicht von außen erreichbar ist (wir haben ja noch nicht gesagt, auf welchem Port er hören soll). Schauen wir mal rein:

sudo kubectl get deployments

NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   1/1     1            1           1min

Das heißt, es läuft 1 von 1 Container. Das gleiche sagt auch Docker:

Gut, es laufen noch mehr Container – aber die beachten wir mal nicht.

Wie können wir denn jetzt auf den NGINX Container von außen zugreifen?

Die Antwort ist ein Service. Wenn du versuchst, jetzt über Port 80 auf NGINX zuzugreifen, führt das ins Leere. Kubernetes nutzt nämlich selbst die Ports 80 und 443, um mit seinen Pods zu kommunizieren. Das ist ein ziemlich wichtiger Punkt, denn wie soll ein normaler User auf den Webserver zugreifen, wenn er mit Kubernetes läuft? Die Antwort darauf ist ein LoadBalancer, aber dazu später.

Kümmern wir uns erstmal darum, den NGINX-Container auf Port 30007 erreichen zu können. Ein Service, genauer gesagt vom Typ NodePort, ist dafür zuständig.

Dazu legst du die Datei kubernetes/nginx-service.yml an:

apiVersion: v1

# Diese Datei beschreibt einen Service
kind: Service

# Name dieses Services
metadata:
  name: ngnix-service
  
# Infos zum Service  
spec:
  # Dieser Service betrifft alle Container mit
  # folgendem Label
  selector:
    app: nginx
  
  # Typ des Services (es gibt noch andere)
  type: NodePort

  # Die Ports, die wir mappen wollen
  ports:
  - name: http
    protocol: TCP
    
    # Port des Containers
    port: 80
    
    # Port der von außen erreichbar sein soll
    nodePort: 30007

Den Service richten wir dann genauso ein wie das Deployment:

sudo kubectl apply -f nginx-service.yaml

Auch analog zum Deployment listen wir alle Services auf:

sudo kubectl get service

NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes      ClusterIP   10.43.0.1      <none>        443/TCP        6d19h
ngnix-service   NodePort    10.43.122.70   <none>        80:30007/TCP   6d19h

Da isser! Hier sieht man auch gut, dass Kubernetes ein eigenes kleines Subnetz aufgebaut hat. Jeder Pod, Service und Co. hat seine eigene IP.

Stunde der Wahrheit: Rufe http://<Server IP>:30007 auf. NGINX sollte die bekannte Meldung ausgeben.

Rekapitulation

Wir haben jetzt das gleiche mit Kubernetes gemacht, was wir auch mit Docker Compose erreicht haben. Der Nachteil: Momentan können wir den Webserver nicht auf den Ports 80 und 443 erreichen, was für Live-Apps ja grundlegend wichtig ist. Mit Docker, bzw. Docker Compose kein Problem, denn diese Ports sind ja frei und nicht von Kubernetes belegt.

Das ist aber kein wirkliches Problem – denn Kubernetes ist ja dazu gedacht, in einem Server-Cluster zu laufen, der einiges an Replikation liefert. Wir brauchen nur einen LoadBalancer, der die Requests von außen auf Port 80/443 empfängt und dann intern an den NGNIX-Container weiterleitet. Auch das kann Kubernetes! Es gibt sogar Cloud-Hoster (wie AWS oder Azure), die die Hardware dazu bereitstellen – kann man aber auch selbst machen.

Björn Falszewski
6. Februar 2021
Disclaimer
Alle meine Artikel entstehen mit bestem Wissen und Gewissen, sind aber nicht perfekt und sollten immer nur als Ausgangspunkt für deine eigenen Recherchen bilden.

Sollte dir etwas Fehlerhaftes auffallen, freue ich mich über deine Nachricht!