Container und Netzwerkservices

Docker Network Magic – der elegante Umgang mit Netzwerken
Kommentare

Jeder, der mit Docker experimentiert, kann mit dieser Linux-Container-Technologie außerordentlich schnell Erfolge erzielen. Der Docker Daemon sorgt im Hintergrund dafür, dass viele notwendige Dinge wie Isolierung der Prozesse, Bereitstellen des Dateisystems, Konfigurationen und das Netzwerk bereits irgendwie geregelt sind.

So wundert man sich auch nicht, dass ein neu gebauter Container über Netzwerkzugriff ins Internet verfügt, um zum Beispiel Pakete zu installieren. Der folgende Artikel beschreibt, mit welchen Mitteln diese Magie funktioniert, wie sich Container verbinden lassen und welche Leistungen Docker im Bereich des Netzwerks aktuell bietet.

Der Erfolg bei der Kooperation von Docker-Containern mit dem Netz stellt sich schnell ein. Im Hintergrund zeichnet der Docker Daemon dafür verantwortlich, dass die Verbindungen im Netzwerk hergestellt sind. Die Container können mühelos aus dem Internet Pakete nachladen und untereinander Netzwerkservices nutzen. Das Publizieren von Ports der eigenen Services ist geregelt. Spätestens, wenn die eigene Anwendung in die Produktion soll oder der Zugriff auf verschiedenen Docker Hosts geregelt werden muss, stellt sich die Frage, wie Docker denn die Dienste genau bereitstellt? Die Entzauberung auf der Basis eines Linux Ubuntu 14.04 Docker Hosts wird im Folgenden erläutert. Auf der Basis einer Vagrant- und Virtualbox-Installation in Verbindung mit dem Git-Projekt kann die Übung praktisch nachvollzogen werden.

Das Netzwerk im Docker-Container

Wenn man in einem einfachen Docker-Container mit dem Befehl docker run -ti –rm ubuntu:14.04 eine Shell startet, kann man sofort überprüfen, welche Netzwerkeinstellungen vorliegen. Im Container ist auf der eth0-Schnittstelle die IP-Adresse 172.17.0.xxx/16 gebunden. Mit dem Befehl ip ro show sieht man alle IP-Pakete, die über die Default-Brücke 172.17.42.1 geroutet werden. Über diese Brücke werden auch alle IP-Pakete nach außen ins Netz bzw. Internet zugestellt (Listing 1).

Listing 1

$ sudo docker run -ti —rm ubuntu:14.04 /bin/bash
root@4de56414033f:/# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
37: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether e2:47:24:55:de:d4 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::e047:24ff:fe55:ded4/64 scope link
       valid_lft forever preferred_lft forever
root@4de56414033f:/# ip ro show
default via 172.17.42.1 dev ether
172.17.0.0/16 dev eth0  proto kernel  scope link  src 172.17.0.2

root@4de56414033f:/# ping -c 3 www.infrabricks.de
PING github.map.fastly.net (185.31.17.133) 56(84) bytes of data.
64 bytes from github.map.fastly.net (185.31.17.133): icmp_seq=1 ttl=61 time=46.2 ms
64 bytes from github.map.fastly.net (185.31.17.133): icmp_seq=2 ttl=61 time=46.8 ms
64 bytes from github.map.fastly.net (185.31.17.133): icmp_seq=3 ttl=61 time=44.5 ms
--- github.map.fastly.net ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 44.555/45.891/46.823/0.968 ms

Auf dem Docker Host kümmert sich der Docker Daemon um die Magie. Bei der Installation von Docker wird eine Linux-Netzwerkbrücke docker0 angelegt. Eine Brücke erscheint wie eine eigene Netzwerkschnittstelle, im Fall von docker0 besitzt diese Brücke sogar eine eigene IP-Adresse. Man kann sich eine Brücke vorstellen wie das virtuelle Äquivalent eines Hardware-Switches, aber ohne die eigene Logik, die ein Switch natürlich hätte. Andere Netzwerkschnittstellen können an eine Brücke angeschlossen werden, und der Kernel (das Modul bridge) leitet Pakete, die auf beliebigen Schnittstellen ankommen, an alle angeschlossenen Schnittstellen weiter (Abb. 1).

Abb. 1: Die „docker0“-Bridge mit Kontakt zur Außenwelt

Abb. 1: Die „docker0“-Bridge mit Kontakt zur Außenwelt

Die Netzwerkmagie, sprich die Tatsache, dass ein Docker-Container auch direkt Pakete ins Internet verschicken kann, entsteht, indem der Docker Host eine entsprechende iptables-Regel erzeugt. In der Postrouting Chain gibt es einen Masquerade-Eintrag. Dabei setzt der Host in den Paketen, die für die Außenwelt bestimmt sind, die eigene IP der ausgehenden Schnittstelle ein, merkt sich diese Verbindung, sodass die Antworten später auch zur Schnittstelle eth0 des Docker-Containers zurückgeroutet werden (Abb. 1, Listing 2). Im Artikel „ Besuch im Maschinenraum“ wird beschrieben, wie eine solche Linux Bridge mit Bordmitteln angelegt wird.

Listing 2

$ sudo /sbin/iptables -L -n -t nat
[....]

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/16       !172.17.0.0/16

In Listing 3 wird mit dem IP-Kommando die Bridge docker0 als Schnittstelle mit ihrer IP angezeigt. Das Werkzeug brctl zeigt die Schnittstellen an, die an die Bridge angeschlossen sind. Es stammt aus dem Paket bridge-utils. In der letzten, rechten Spalte werden die an die Bridge angeschlossenen Schnittstellen angezeigt. Es sieht etwas seltsam aus, dass hier eine veth-Schnittstelle auftaucht. Man kann sich die Details zu dieser Schnittstelle mit dem Befehl ip addr show vethc3cd anzeigen lassen (Listing 3).

Listing 3

$ sudo ip addr show docker0
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 16:69:e4:75:45:86 brd ff:ff:ff:ff:ff:ff
    inet 172.17.42.1/16 scope global docker0
       valid_lft forever preferred_lft forever

$ sudo brctl show
bridge name bridge id       STP enabled interfaces
docker0     8000.1669e4754586   no      vethc3cd

$ sudo ip addr show vethc3cd
37: vethc3cd: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master docker0 state UP group default qlen 1000
    link/ether 76:c0:d8:ea:50:4d brd ff:ff:ff:ff:ff:ff
    inet6 fe80::74c0:d8ff:feea:504d/64 scope link
       valid_lft forever preferred_lft forever

Es handelt sich quasi um ein virtuelles Kabel, das an der Bridge hängt (in der ersten Ausgabezeile zu sehen, master docker0). Diese Schnittstelle ist damit die Gegenstelle der eth0-Schnittstelle des Containers. Das lässt sich auch mit der Statistikfunktion des Werkzeugs ethtool herausfinden:

$ sudo ethtool -S vethc3cd
NIC statistics:
     peer_ifindex: 37

ethtool zeigt an, dass der Index des Peers zu vethc3cd den Identifier 37 trägt. Genau das ist die ID, die im Container selbst beim eth0 angezeigt wird (Abb. 1).

Docker-Container mit dem Netzwerk verbinden

Docker vergibt beim Start des Containers also eine neue IP und verbindet sie mit der docker0-Bridge. Natürlich kann man umfangreichen Einfluss auf diese Netzwerkeinstellung nehmen (Tabelle 1). Der Daemon kann seine Container an eine andere Bridge anschließen oder die Kommunikation der Container nach außen oder untereinander beschränken. Die Vergabe der IP oder des Hostnames des Docker-Containers kann auf einem DHCP- bzw. DNS-Dienst beruhen oder statisch erfolgen. Seit der Docker-Version 1.2 kann man die Dateien /etc/hosts, /etc/hostname und /etc/resolv.conf auch im laufenden Container schreiben.

Die beiden wichtigsten Optionen beim Starten eines Containers sind aber sicherlich Publish und Link. Mit der Option –publish=<IP>:<PORT HOST>:<PORT CONTAINER> wird ein Port eines Containers durch eine Portweiterleitung an die docker0-Bridge realisiert. Damit wird es also möglich, dass der Dienst im Container von außen erreichbar wird. Mit der Option –link=<CONTAINER>:<ALIAS at CONTAINER> wird ein anderer Container auf demselben Host „sichtbar“. Die IP- und Host-Zuordnung des anderen Containers werden in die Datei /etc/hosts aufgenommen, und es werden Umgebungsvariablen generiert, die den Zugriff auf die Dienste des anderen Containers erleichtern. In Listing 4 sieht man, dass der www-tools-Container die entsprechende Names-Auflösung und Umgebungsvariable für den Zugriff auf den lighttpd-Container besitzt. Wenn der Docker Daemon mit einer Netzwerkisolierung in der Datei /etc/default/docker, beispielsweise DOCKER_OPTS= „—icc=false —iptables=true“ gestartet wurde, wird die entsprechende iptables-Regel im Host generiert. So kann direkt bestimmt werden, welcher Container mit welchem kommunizieren kann.

Am Beispiel eines lighttpd-Servers wird in Listing 4 deutlich, wie auf den Service eines anderen Containers zugriffen wird. Da sich die IP-Adresse eines Containers beim erneuten Start eventuell verändert, werden der Client und der Server am besten über Namen miteinander bekannt gemacht. Die Startreihenfolge ist relevant und kann beispielsweise einfach mit den Werkzeug fig, aber besser mit upstart oder systemd auf einem Host vereinbart werden. Hier wird schnell klar, dass in einer Produktion weitere Festlegungen via DNS oder besser noch eine Orchestrierung mit einer Service-Discovery bereitgestellt werden muss. Im Artikel „CoreOS“ ist eine Lösungsvariante für diese Aufgabe beschrieben. Für die Entwicklung empfiehlt es sich, das Projekt dnsmasq einzusetzen. Damit lässt sich ein einfacher lokaler DNS-Server aufbauen. Natürlich sollte auch dieser besser als Docker-Container bereitgestellt werden.

Listing 4

# start lighttpd container 
$ docker run -d --name=lighttpd rossbachp/lighttpd

# link and access lighttpd container
$ docker run -it --rm --link lighttpd:lighttpd rossbachp/www-tools
root@8152f401d0a0: curl http://$LIGHTTPD_PORT_80_TCP_ADDR/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Welcome page</title>
</head>
<body>
<h1>Welcome to this tiny httpserver</h1>
</body>
</html>
root@8152f401d0a0: ping -c 1 lighttpd
PING lighttpd (172.17.0.42) 56(84) bytes of data.
64 bytes from lighttpd (172.17.0.42): icmp_seq=1 ttl=64 time=0.162 ms

--- lighttpd ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.162/0.162/0.162/0.000 ms
$ cat /etc/hosts
172.17.0.43	8152f401d0a0
127.0.0.1	localhost
[…]
172.17.0.42	lighttpd
root@8152f401d0a0:/# env | grep LIG
LIGHTTPD_PORT_80_TCP_PORT=80
LIGHTTPD_NAME=/cocky_ritchie/lighttpd
LIGHTTPD_PORT_80_TCP_ADDR=172.17.0.42
LIGHTTPD_PORT_80_TCP_PROTO=tcp
LIGHTTPD_PORT=tcp://172.17.0.42:80
LIGHTTPD_PORT_80_TCP=tcp://172.17.0.42:80
Tabelle 1: Netzwerkoptionen des Docker Daemon bzw. -Containers

Tabelle 1: Netzwerkoptionen des Docker Daemon bzw. -Containers

Nicht immer möchte man, dass ein Docker-Container überhaupt mit der Docker Bridge verbunden ist. Dazu dient die Option –net=none beim Start des Containers (Tabelle 1). Datentransformatoren, Numbercruncher oder einfache Batch-Jobs können ihre Aufgaben auch auf Basis einfacher Dateien durchführen. Manchmal möchte man die Netzwerkanbindung vielleicht gar nicht mit Docker-Mitteln erzeugen.

Eine Bridge hat geringfügigen negativen Einfluß auf die maximale Transferleistung des Services. Wenn Ihre Services also 10-Gigabyte-Ethernet, 20-Gigabyte-Thunderbold oder gar Infiniband-Anschlüsse auslasten, können sie die Latenz der Brücke vermeiden und den Container direkt mit dem Hostnet mit der Option –net=host an die Netzwerkschnittstelle des Hosts anbinden. Weiterhin können sie mehrere unabhängige Container mit derselben IP binden. Die Option –net=container:<CONTAINER | ID> ermöglicht die Referenz auf die IP des befreundeten Containers. Es ist also nicht notwendig, dass mehrere Prozesse in einem Container laufen müssen, damit sie unter einer IP erreichbar sind. Docker hat alle Möglichkeiten, jeden Prozess einzeln laufen zu lassen und auf einem Host trotzdem als gemeinschaftlicher Verbund wahrgenommen zu werden (Abb. 2). Das Projekt „Docker in Docker“ oder die Definition von Pods aus dem Google-Kubernetes-Projekt sind eine gute Anregungen für die Gestaltung der eigenen Servicebereitstellung.

Abb. 2: Optionen von Docker-Container zum Anschluss an Netze

Abb. 2: Optionen von Docker-Container zum Anschluss an Netze

Ein Verbund von Docker Host ermöglichen

Nicht alle Docker-Container können auf einem Host ablaufen. Da stellt sich die Frage: Wie können die Docker-Container über Host-Grenzen hinweg miteinander und sicher kommunizieren? Natürlich gibt es in der Welt der Netze hierfür mehr als eine Lösung. Direktes Routing, SSH-Tunnel oder VPN-Netze sind hier meist die Wahl. Das Thema Software Defined Networking (SDN) kann allerdings schnell unheimlich werden. Verstrickungen sind leider wahrscheinlich. Die Anwendung des Projekts Open vSwitch wird hier für komplexe Installationen empfohlen. Eine Integration der verschiedenen Möglichkeiten bietet das Projekt Docker und sein Ökosystem leider noch nicht. Verschiedene Projekte und Konzeptionen rangeln hier um Aufmerksamkeit. Im Kern ist eine Verbindung durch einen abstrakten Tunnel schnell beschrieben und schon mit Linux-Bordmitteln problemlos zwischen mehreren Hosts oder gar Standorten realisierbar, aber der Teufel liegt hier im Detail der Anforderungen, wie Abbildung 3 zeigt.

Der Einstieg in diese „interessante“ IT-Küche ist nicht schwer. Sehr hilfreich erweist sich hier das Projekt Pipework von Jérôme Petazzoni. Pipework ist ein Wrapper-Skript, um verschiedene virtuelle Netzwerkinfrastrukturen auf dem Host und im Docker-Container zu erzeugen. Schnell sind die neuen Brücken br1 auf jedem Host gebaut und die Container jedes Hosts daran gebunden, wie zum Beispiel in Abbildung 3 zu sehen ist. Wenn man dann die verschiedenen Hosts mit ihren externen Schnittstellen an die jeweiligen Brücken bindet, können die Container auf den verschiedenen Hosts miteinander Pakete austauschen. Ein nachvollziehbares Beispielprojekt zu diesem Ansatz befindet sich auf unserem infrabricks-Blog.

Abb. 3: Docker-Container-Kommunikation über Host-Grenzen hinweg ermöglichen

Abb. 3: Docker-Container-Kommunikation über Host-Grenzen hinweg ermöglichen

Verschiedene Projekte versuchen den Umgang mit einfachen und komplexen Netzwerksetups zu lösen. Aktuell sind die Projekte Wire, Weave und Flanel im Spätsommer 2014 mit ersten Betareleases erschienen. Das Projekt libswarm von Docker Inc soll helfen, die verschiedenen Ideen und Standards zu vereinheitlichen, macht aber bisher keine sichtbaren Fortschritte. Im Infokasten „Anforderungen und Status der Docker-Netzwerkeben“ befinden sich ein paar Gedanken zu Anforderungen an eine Lösung. Je nach Ihren konkreten Projektanforderungen oder dem Umfeld sollte die Lösung beurteilt, ausgewählt und angepasst werden.

Anforderungen und Status der Docker-Netzwerkebene
Die Docker-Community wartet gespannt darauf, wie sich das Thema Netzwerk und Docker entwickelt. Mit dem Projekt libswarm hat Docker Inc ein Projekt gegründet, das sich um die Entwicklung eines gemeinsam nutzbaren Modells kümmert. Am Markt gibt es aber schon eine Vielzahl von ersten Implementierungen dieser Aufgabe. Eine Integration ins große Ganze wird aber wohl noch dauern.
Wem die grundlegenden Netzwerkfähigkeiten von Docker nicht ausreichen, muss weiterführende Werkzeuge einsetzen, besonders wenn es darum geht, Container über mehrere Hosts zu verbinden. Weave ist eine in Go implementierte Switch/Router/Overlay-Kombination, die eine Art privates Netzwerk zwischen Containern schaltet. Flannel ist ebenfalls ein Overlay-Netzwerk, das im Zusammenhang mit Kubernetes Cluster zusammenschaltet werden kann. Wire hingegen zielt darauf ab, Container und Docker-Hosts in bestehende Netzwerkarchitekturen und -tools zu integrieren.
Hier eine Liste mit Vorschlägen für Anforderungen an ein zukünftiges Docker-Netzwerk:1. Wir brauchen eine transparente Netzwerkschicht über Host-Grenzen hinweg
2. Die Vermittlung der Ports/Services muss so einfach wie ein Link einsetzbar sein
3. Die Container müssen in Gruppen und in verschiedenen Netzen isolierbar sein
4. Ein Container muss sich an verschiedene Netze anschließen lassen
5. Ports und Netze müssen gezielt „veröffentlicht“ werden können und der Zugang zu den Services muss reguliert sein
6. Die Container eines Service müssen bei verschiedenen Providern bzw. Plattformen laufen können
7. Verlagerung, Änderungen oder Erneuerung eines oder mehrerer Container müssen transparent für den Client werden
8. Der Netzwerk-Traffic zwischen Containern eines Netzes muss verschlüsselt übertragbar sein
9. Skalierbarkeit und Bandbreite des Netzwerks müssen einstellbar und messbar sein
10. Es muss eine Kontrolle über die Zugehörigkeit eines Netzwerks und seiner Container geben

Fazit

Nicht jeder von uns wird das Thema Netzwerke in seiner ganzen Tiefe ausschöpfen müssen, aber für den Einsatz produktiver Software und die Gestaltung effektiver Systeme ist Basiswissen unerlässlich. Das Docker-Ökosystem schafft neue Lösungen und einen eleganteren Umgang mit dem Thema Netzwerke. In Zeiten von Clouds und unerlaubtem bzw. kriminellem Zugriff auf die eigenen Daten zeichnen sich große Herausforderungen im Design und der Bereitstellung von Software ab. Jeder, der sich mit dem Thema Microservices auseinandersetzt, kommt nicht um die Gestaltung einer brauchbaren Netzwerktopologie und dynamischeren Orchestrierung der Services herum. Wir dürfen darauf gespannt sein, welche interessanten neuen Ideen schon bald unseren Alltag bestimmen werden.

Aufmacherbild: Container terminal at night, working in the bridge crane von Shutterstock / Urheberrecht: gyn9037

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -