services

Docker CE

Teile und Hersche

Teile und Herrsche - das ist in so vielen Aspekten immer wieder wahr.

Nach Jahrmillionen in denen man die Ressourcen eines Computers maximal auslastete, weil die sind ja teuer, begann man - als die Preise fielen, die Aufgaben zu verteilen, ein Server für eine Aufgabe. Dann erkannte man erstaunllicherweise, dass viele der Rechner nicht mal im Ansatz ausgelastet werden. Verschwendung. Dann fing man an, die Dienste in virtuelle Maschinen auf der Hardware zu konsolidieren. Dadurch waren die Rechner gut ausgelastet. Aber die Admins kamen kaum hinterher - jede VM ist ihr eigenes Betriebssystem mit einem Zoo von Diensten und Paketen, damit die VM das tut, wofür sie gedacht ist. Und jedes OS verbrennt auch Ressourcen für OS Aktionen, braucht Sicherheitsupdates und so weiter. Dann ging es los mit Paravirtualisierung und es dauerte dann nicht mehr lange und Docker war geboren.

Docker ist keine Virtualisierung. Auch wenn es manchmal so aussieht, aber es paravirtualisiert, genauer es nutzt die Möglichekten der CPU und des Betriebssystems, die Prozesse von einander zu trennen. Einzig die für einen Prozess notwendigen Ressourcen (Dateien) werden dupliziert und nicht mehr das ganze Betriebssystem. Ich vergleiche das immer gerne mit dem Chroot, was man schon länger kennt.

Das sind schon viele Vorteile, der eigentliche Vorteil ist aber - man verschmutzt mit einem Dockercontainer nicht sein Basissystem. Die Konfigurationsarbeiten für den Betrieb eines Dienstes bleibt im Docker - genauer im Dockercontainer. Wirft man das weg, ist alles spezifische weg. Das hat den Vorteil, dass man schnell was ausprobieren kann, das hat aber auch den Vorteil bei Fehlern schnell zurück zum Grundsystem zu kommen.

Kompositionen

Die normale Nutzung von Docker basiert darauf, dass man mit dem Befehl Docker diverse unabhängige Befehle absendet und über Parametrisierung der einzelnen Befehle dann eine Infrastruktur aufbaut. Zum Beispiel braucht es für ein abgeschottetes System aus zwei Diensten Datenbank und Webserver einen Befehl um ein Netzwerk zu erzeugen, einen um den Dienst Datenbank zu starten und mit dem Netzwerk zu verknüpfen und einen um das gleiche mit dem Webserver zu tun. Das ist ziemlich viel Handarbeit und man fängt schnell an, dass zu scripten.

Und hier beginnt die Arbeit von docker-compose. Dieses Tool bietet die Möglichkeit in einer YAML Datei eine Infrastruktur zu definieren.

Hier mal ein Beispiel:

version: '2.1'

services:
  postgres:
      image: postgres
      volumes:
        - ./postgres_data:/var/lib/postgresql/data
      environment:
        POSTGRES_DB: keycloak
        POSTGRES_USER: keycloak
        POSTGRES_PASSWORD: password
  keycloak:
      image: jboss/keycloak
      command: -b 0.0.0.0 -Djboss.bind.address.private=127.0.0.1
      environment:
        DB_VENDOR: POSTGRES
        DB_ADDR: postgres
        DB_DATABASE: keycloak
        DB_USER: keycloak
        DB_SCHEMA: public
        DB_PASSWORD: password
        KEYCLOAK_USER: admin
        KEYCLOAK_PASSWORD: pasword
      ports:
        - "2600:8443"
      depends_on:
        - postgres

networks:
  default:
    driver: bridge
    enable_ipv6: true
    ipam:
      config:
        - subnet: "172.26.0.0/24"
        - subnet: "fc00:2600::/96"

Das ist ein einfaches Beispiel und es sieht komplizierter aus, als es ist. Die erste Zeile enthält eine Versionsnummer, die für docker-compose relevant ist. Hier ist 2.1 eine gute Wahl. Es gibt auch neuere Versionen, die bringen aber nur etwas in Zusammenhang mit docker-swarm, welches wir nicht nutzen. Dann gibt es drei Blöcke (in unserem einfachen Beispiel sind nur zwei zu sehen) - services, networks, volumes.

Im Bereich services definieren wir, die jeweiligen Docker-Container, die zusammen einen “Service” abbilden. Theoretisch kann man hier eine ganze Infrastruktur abbilden - allerdings begrenz docker-compose sich auf einen Host und verteilt die Container nicht in einem Kubernetes Cluster. Jedes Element darunter hat einen frei zu wählenden Namen und darunter gibt es dann einige elementare Einträge wie image oder build um anzugeben, welcher Container zu verwenden ist, welche Ports freigegeben werden bestimmt man mit ports. Der Rest ist optional. Zum Beispiel environment definiert Umgebungsspezifische Variablen. Man kann diese auch aus einer Datei auslesen. Man kann für bestimmte Pfade im Container auch volumes deklarieren, um sie außerhalb des Containers zu lagern. Hier verwende ich einen relativen Pfad - was in Docker eigentlich nicht erlaubt ist, aber mit docker-compose ab seiner Definitionsdatei funktioniert. Man kann auch Volumes definieren, die anders liegen. Dafür gibt es ja die Volumes Definition (die wir hier nicht verwenden).

Weiterhin kann man Netzwerke mit networks definieren - ich mache das gerne explizit, weil man so mehr Kontrolle hat und für IPv6 ist das sogar zwingend notwendig, weil Docker bzgl. IPv6 leider kein gutes Vorbild in der Umsetzung ist. Es wird gesagt, IPv6 geht - man muss es aktivieren (via enable_ipv6), aber es wählt dann nicht selbständig ein Segment aus dem Host-Adressraum, es macht kein NAT (und arbeitet damit bei IPv6 gänzlich anders als IPv4). Viel schlimmer ist aber, dass offene Ports von Containern in IPv6 für alle offen sind, egal ob man das will oder nicht. Bei IPv4 wird die ports deklaration also beachtet - bei IPv6 nicht. Ich verwende daher gerne ein Hilfspaket von robertkl/ipv6nat.

Zum Thema IPv6 NAT kann man viel erzählen und diskutieren. Aber so lange man keine dynamische Kontrolle über die Ports hat, die Docker aufmacht, ist das Thema bei einem direkt ans Internet gehängten Server obsolet. Da muss was “vor”.

Docker und die Firewall

Bedingt durch die Unabhängigkeit der einzelnen Services voneinander und der Tatsache, dass jedes seine eigene Infrastruktur bekommt, ergibt sich in einer gemeinsam zu nutzenden Ressource schnell ein Problem. Bei Docker ist das das Netzwerk. Jeder Service bekommt sein eigenes lokales internes Netzsegment und das landet alles auf einem Interface. Zum Teil schränkt Docker die Nutzung erheblich ein. Zum Teil ignoriert es bestehende Regeln und liefert keine Lösung. Dann muss die Firewall ran.

Wir haben auf einem Server A einen Service etabliert, und wollen nun nur von einem Server B darauf zugreifen. Alle anderen sollen den Service nicht sehen. Hängt der Server im Internet, kann man das nur mit lokalen Firewallregeln auf Server A direkt lösen.

Docker erstellt selber ein kleines Regelwerk in den IPTables Tabellen NAT und Filter. Dabei geht es Docker primär darum, die Pakete vom externen Netzwerkinterface auf das interne Netz des jeweiligen Service zu bekommen.

Die NAT Regeln sorgen dafür, dass Anfragen vom externen Interface auf dem externen Port auf das interne Interface des Docker Containers mit seinem internen Port weitergereicht werden. Hier sollte man nicht viel dran ändern. Bei den Filtern kann man aber sowohl INPUT als auch eine Docker-spezifische Chain DOCKER-USER befüllen. Wichtig dabei ist - wenn, dann nur diese beide Chains flushn und alles andere so lassen.

Was ich mache ist - die INPUT Chain wie üblich konfigurieren - alles was rein darf, darf rein, den Rest drop’n. Zu beachten gilt, dass die Forward Chain für NAT zuerst angesprochen wird und INPUT deshalb nur für Nicht-Docker Dienste relevant ist. Die für Docker relevanten Services finden sich in DOCKER-USER Anwendung und müssen dort getrennt behandelt werden. Bei der DOCKER-USER Chain (die in FORWARD angesiedelt ist), muss man vorsichtig sein mit dem DROP oder REJECT. Hier sollte man die Regeln nur auf das externe Interface anwenden, damit die Kommunikation zwischen Containern (die über interne Bridges und docker0 laufen) nicht gestört werden.