Aus Linux-Magazin 08/2017

Container richtig vernetzen

© bowie15, 123RF

Wer Microservices in Containern betreibt, zwingt sie vor allen Dingen dazu, miteinander zu kommunizieren – und mit der Außenwelt. Wie Admins Pods und Nodes in Kubernetes dafür vernetzen, klärt der Artikel.

Kubernetes kennt ganz unterschiedliche Wege, um Container und Microservices miteinander ins Gespräch zu bringen. Das reicht von Verbindungen mit der Hardware im Rechenzentrum bis zur Konfiguration von Load Balancern. Um die Kommunikation zu gewährleisten, verzichtet das Netzwerkmodell von Kubernetes [1] auf Network Address Translation. Sämtliche Container erhalten eine IP-Adresse, sodass die Nodes mit ihnen und sie auch untereinander ohne NAT kommunizieren.

Aus diesem Grund kann ein Admin auch nicht einfach zwei Docker-Hosts mit Kubernetes einrichten: Das Netzwerk ist eine eigene Schicht, die er für Kubernetes konfigurieren muss. Für diesen Schichtdienst bewerben sich mehrere Lösungen, die wie Kubernetes selbst zurzeit eine rasante Entwicklung durchlaufen. Neben Bandbreite und Latenz spielen dabei auch die Integration in existierende Lösungen und der Aspekt der Sicherheit eine zentrale Rolle. Dabei zieht Kubernetes alle Register der in Linux implementierten Protokolle und Lösungen.

Während sich die Containerlösung Docker bereits um die Bridges kümmert, trägt Linux unter anderem IP-IP-Tunnel, IPtables-Regeln, den Berkeley Packet Filter (BPF), virtuelle Netzwerkschnittstellen und sogar das Border Gateway Protocol (BGP) bei.

Netze spricht Kubernetes teilweise als Overlay, teilweise als Software Defined Network an. Alle diese Lösungen haben Vor- und Nachteile, wenn es um Funktionsumfang, Performance, Latenz und einfache Handhabung geht.

Der Artikel beschränkt sich auf eine Lösung aus der Praxis des Autors. Das soll jedoch nicht bedeuten, dass die anderen Lösungen weniger gut sind, aber allein die Vorstellung der unter [2] erwähnten Projekte würde vermutlich ein eigenes Sonderheft erfordern.

Bislang implementiert Kubernetes nur IPv4 durchgängig. Der IPv6-Support beschränkt sich auf die Services von Kubernetes selbst. Dank Calico [3] kommt IPv6 auch für die Pods zum Einsatz. Der Kubeproxy soll ab Version 1.7, die Mitte des Jahres erscheint, IPv6-fähig sein.

Die Abstraktion von Kubernetes ermöglicht völlig neue Konzepte. Network Policy [4] regelt, wie Gruppen von Pods miteinander und mit anderen Netzwerk-Endpunkten reden. Mit dem Feature richtet der Admin etwa abgeschottete Bereiche im Netzwerk ein. Es setzt auf Kubernetes Namespaces (nicht zu verwechseln mit denen im Kernel) und Label. Das in eine Linux-Konfiguration umzusetzen, ist komplex, nicht jedes Kubernetes-Produkt beherrscht die Implementierung.

Flannel

Flannel (Abbildung 1, [5]) von Core OS ist das älteste und einfachste Kubernetes-Netzwerk. Es unterstützt zwei Grundprinzipien, indem es Container untereinander verbindet und dafür sorgt, dass alle Nodes jeden Container erreichen.

Abbildung 1: Ein Flannel-Overlay-Network verwaltet ein Class-C-Netz, die Nodes nutzen meist Teile eines privaten Netzes aus dem 10/8-A-Netz.

Abbildung 1: Ein Flannel-Overlay-Network verwaltet ein Class-C-Netz, die Nodes nutzen meist Teile eines privaten Netzes aus dem 10/8-A-Netz.

Auf jedem Node spannt Flannel ein C-Netz auf und verbindet es nach innen über die Flannel-Bridge »flannel0« mit der Docker-Bridge »docker0«. Nach außen verbindet der Flannel-Dämon »flanneld« die anderen Nodes über ein externes Interface. Je nach Backend transportiert Kubernetes die Pakete zwischen den Pods verschiedener Nodes über VxLAN, Hostrouten oder eingepackt in UDP-Pakete. Ein Node verschickt sie, sein Gegenstück nimmt sie entgegen und leitet sie an den adressierten Pod weiter.

Jeder Node erhält dabei ein Klasse-C-Netz mit 254 Adressen. Nach außen bildet Kubernetes die Adresse auf das Klasse-B-Netz ab. Netzwerktechnisch ändert »flanneld« somit also lediglich die Netzwerkmasken von B auf C und zurück. Eine einfache Flannel-Konfiguration zeigt Listing 1.

Listing 1

Eine einfache Flannel-Konfiguration

01 [...]
02 {
03         "Network": "10.0.0.0/8",
04         "SubnetLen": 20,
05         "SubnetMin": "10.10.0.0",
06         "SubnetMax": "10.99.0.0",
07         "Backend": {
08                 "Type": "udp",
09                 "Port": 7890
10         }
11 }
12 [...]

Dieses Konzept wirkt auf den ersten Blick robust und schlicht, stößt allerdings recht schnell an seine Grenzen. Das Problem: Dieses Netzwerkmodell erzeugt maximal 256 Nodes. Das gilt für ein Projekt mit den Ambitionen von Kubernetes als ein eher kleiner Cluster. Dafür lässt er sich auch ohne Weiteres in die VPC-Netzwerke (Virtual Private Cloud) einiger Cloudprovider integrieren. Für komplexere Netzwerke unterstützt Kubernetes das CNI-Konzept (Container Network Interface) und erlaubt die Konfiguration für diverse Varianten von Netzwerkumgebungen.

Größere Netze

Eine Beispielkonfiguration für CNI zeigt Listing 2. Neben der Versionsnummer setzt es einen eindeutigen Namen für das Netzwerk (hier »dbnet«), bestimmt den Typ (hier »bridge«) und erlaubt es ansonsten, die üblichen Parameter zu setzen, wozu die Netzwerkmaske, das Gateway und eine Liste mit Nameservern zählen. Das geschieht über IP-Address Management (Ipam), das unter anderem DNS und DHCP integriert.

Listing 2

CNI-Beispielkonfiguration

01 [...]
02 {
03   "cniVersion": "0.3.1",
04   "name": "dbnet",
05   "type": "bridge",
06   // type (plugin) specific
07   "bridge": "cni0",
08   "ipam": {
09     "type": "host-local",
10     // ipam specific
11     "subnet": "10.1.0.0/16",
12     "gateway": "10.1.0.1"
13   },
14   "dns": {
15     "nameservers": [ "10.1.0.1" ]
16   }
17 }
18 [...]

Nach seinem Start kann ein Container nacheinander eine Reihe von Plugins verwenden. In einer genau definierten Kette reicht Kubernetes dank CNI den Json-Output eines Plugins jeweils als Input an das nächste Plugin weiter. Dieses Vorgehen beschreibt das Github-Repository für CNI im Detail [6].

Projekt Calico

Calico [3] will das Verhältnis der Nodes zu den Containern betrachten. Die Idee ist, die Konzepte und Relationen zwischen Rechenzentrum und Host darauf zu übertragen. Der Node verhält sich zum Pod wie ein Rechenzentrum zum Host. Allerdings überholen die Container die Hardware in einem Datacenter in Sachen Dynamik und Lebenszyklus um viele Größenordnungen.

Für Calico gibt es zwei mögliche Installationswege. Eine Custom-Installation startet Calico in den Containern selbst. Eine Host-Installation verwendet Systemd, um alle Dienste zu starten. Der Artikel beschreibt die Custom-Installation, die flexibler ist, aber je nach Umgebung und beteiligten Versionen subtile Bootstrapping-Probleme nach sich ziehen kann.

Calico nutzt CNI, um sich in Kubernetes als Netzwerkschicht einzurichten. Eine Version zum Ausprobieren mit Vagrant wartet unter [7]. Neben der Konfiguration von Core OS [8] konfiguriert und betreibt der Admin Calico in der Custom-Installation alleine mit Bordmitteln von Kubernetes. Die eigentliche Konfiguration steckt in einer Config Map [9].

Auf den Nodes läuft mit Felix [10] ein Daemon-Set [11], das garantiert, dass der Calico-Node-Pod pro Node genau einmal läuft (Abbildung 2). Weil Calico mit dem Per-Host-Daemon Felix auch die Kubernetes Network Policy [4] implementiert, startet Kubernetes genau eine Replica eines Calico-Policy-Controllers [12].

Abbildung 2: Calico baut auf dem Node mit BGP und IPtables ein Netzwerk wie in einem Rechenzentrum auf.

Abbildung 2: Calico baut auf dem Node mit BGP und IPtables ein Netzwerk wie in einem Rechenzentrum auf.

BGP, Routing und IPtables-Regeln

Beim Anlegen weist Felix einem Pod eine IP-Adresse zu, IPtables-Regeln konfigurieren alle erlaubten und verbotenen Verbindungen. Soll der Cluster das Routing anpassen, erzeugt Felix auch die notwendigen Routen und passt sie im Kernel an. Routing ist aber, im Gegensatz zu den IPtables-Regeln, ein Konzept, das über einen Node hinauswächst.

Hier kommt das Border Gateway Protocol ins Spiel. Ein Protokoll, das Internet-weite Routen konfiguriert, hilft ja auch Container auf verschiedenen Nodes zu verbinden. Dabei beschränkt der Netzwerker den Geltungsbereich des Calico-BGP auf den Kubernetes-Cluster. Die meisten Admins blicken eher mit Ehrfurcht auf das BGP, ein fast allmächtiges Routingprotokoll, das den Datenverkehr ganzer Länder umleiten kann.

Seine Möglichkeiten schrecken auf den ersten Blick eher ab, vor allem im Hinblick auf die Auswirkungen und die Security. Dabei ist eine Verbindung zu den Backends der Carrier nicht nötig. Calico braucht den Zugang zum sehr stark abgeschotteten Teil der Internet-Routing-Infrastruktur gar nicht. Das ist also nur ein scheinbares Sicherheitsrisiko.

Regeln fürs Netzwerk

Ein wichtiger Aspekt von Kubernetes ist die Möglichkeit, Cluster zu teilen, also einen gemeinsamen Cluster für mehrere Teams einzurichten, eventuell sogar für mehrere Stufen von Entwicklung, Test und Produktion. Die Listings 3 und 4 zeigen eine typische Konstellation. Zuerst fügt der Admin wie in Listing 3 einem Namespace eine Annotation hinzu. Im konkreten Fall ergänzt der Admin »net.beta.kubernetes.io/network-policy« um ein Json-Objekt, das die Ingress-Isolation auf den Wert »DefaultDeny« setzt. Das bewirkt, dass andere Namespaces keinen der Pods in diesem Namespace mehr erreichen. Will er die Zugriffe noch weiter einschränken, isoliert der Admin nicht nur die Namespaces, sondern auch Pods innerhalb dieser Namespaces.

Listing 3

Annotation eines Namespace

01 [...]
02 kind: Namespace
03 apiVersion: v1
04 metadata:
05   annotations:
06     net.beta.kubernetes.io/network-policy: |
07       {
08         "ingress": {
09           "isolation": "DefaultDeny"
10         }
11       }
12 [...]

Listing 4 beschreibt die Policy für einen Pod mit der Rolle »db«, in dem eine Redis-Datenbank läuft. Sie erlaubt nur eingehenden TCP-Traffic auf Port 6379 (auch Ingress genannt) von Pods aus dem Namespace »myproject« mit der Rolle »frontend«. Alle anderen Pods erhalten keinen Zugriff. Wer einen genaueren Blick auf die Definition wirft, erkennt deutliche Ähnlichkeiten mit bekannten Paketfiltern, etwa mit IPtables. Nur dass der Kubernetes-Nutzer diese Regeln auf der Ebene von Pods und Containern sehr schnell anpassen muss.

Listing 4

Beispiel für Network Policies

01 [...]
02 apiVersion: extensions/v1beta1
03 kind: NetworkPolicy
04 metadata:
05   name: test-network-policy
06   namespace: default
07 spec:
08   podSelector:
09     matchLabels:
10       role: db
11   ingress:
12   - from:
13     - namespaceSelector:
14         matchLabels:
15           project: myproject
16     - podSelector:
17         matchLabels:
18           role: frontend
19     ports:
20     - protocol: tcp
21       port: 6379
22 [...]

Bekommt der Networklayer über ein Container Network Interface, etwa das erwähnte Calico, ein Änderung im Container-Lifecycle mit, generiert und implementiert er die Regeln auf der IPtables- und Routing-Ebene. Wichtig ist hier, die Verantwortlichkeiten zu trennen (Separation of Concerns). Kubernetes stellt lediglich den Controller bereit, die Implementierung übernimmt der Network Layer.

Ingress

Auch hier bietet Kubernetes also wieder nur den Rahmen, während sich ein Controller um die Verfügbarkeit kümmert. Dabei stehen Services auf der Basis von HA Proxy [13], Nginx Reverse Proxy [14] und F5 Hardware [15] bereit, der Beitrag unter [16] beschreibt die generellen Konzepte rund um Services.

Listing 5

Beispiel für eine Pfad-basierte Regel

01 [...]
02 apiVersion: extensions/v1beta1
03 kind: Ingress
04 metadata:
05   name: test
06 spec:
07   rules:
08   - host: foo.bar.com
09     http:
10       paths:
11       - path: /foo
12         backend:
13           serviceName: s1
14           servicePort: 80
15       - path: /bar
16         backend:
17           serviceName: s2
18           servicePort: 80
19 [...]

Ingress erweitert diese Konzepte erheblich. Während der eingebaute Service nur einfaches Round-Robin-Load-Balancing erlaubt, kann ein externer Controller alle Register ziehen und externe öffentliche Hosts mit beliebigen erreichbaren URLs, beliebigen Load-Balancer-Algorithmen, SSL-Termination oder Virtual Hosting aus dem Boden stampfen.

Listing 5 zeigt ein einfaches Beispiel für eine Pfad-basierte Regel. Sie leitet alles unter »/foo« auf Service »s1« um, alles unter »/bar« auf Service »s2«. Eine Host-basierte Regel zeigt Listing 6. Sie dient dazu, die Header der Hosts auszuwerten, um die Hostnamen zu erkennen.

Listing 6

Beispiel für eine Host-basierte Regel

01 [...]
02 apiVersion: extensions/v1beta1
03 kind: Ingress
04 metadata:
05   name: test
06 spec:
07   rules:
08   - host: foo.bar.com
09     http:
10       paths:
11       - backend:
12           serviceName: s1
13           servicePort: 80
14   - host: bar.foo.com
15     http:
16       paths:
17       - backend:
18           serviceName: s2
19           servicePort: 80
20 [...]

Wer einen Service mit SSL-Support ausstatten möchte, kann dies nur auf Port 443 tun. Listing 7 zeigt die Bereitstellung eines SSL-Zertifikats, das in einem Kubernetes-Secret landet. Um das Zertifikat zu verwenden, muss es der Admin in einer Ingress-Regel referenzieren und gibt dazu im Feld ».spec.tls.secretName[0]« einfach den Namen an (Listing 8).

Listing 7

SSL-Zertifikat für einen Service einrichten

01 [...]
02 apiVersion: v1
03 data:
04   tls.crt: <base64 encoded cert>
05   tls.key: <base64 encoded key>
06 kind: Secret
07 metadata:
08   name: mysecret
09   namespace: default
10 type: Opaque
11 [...]

Listing 8

Zertifikat im Service verwenden

01 [...]
02 apiVersion: extensions/v1beta1
03 kind: Ingress
04 metadata:
05   name: no-rules-map
06 spec:
07   tls:
08     - secretName: mysecret
09   backend:
10     serviceName: s1
11     servicePort: 80
12 [...]

Fallstricke

Auch einige der Fallstricke auf dem Weg zum brauchbaren Kubernetes-Cluster sollen hier nicht unerwähnt bleiben. So muss Docker beispielsweise einen Pause-Container starten, um einen Network-Namespace zu erzeugen. In diesem Container läuft in diesem Fall ein Prozess, der nichts weiter tut. Auch das Bootstrapping mit Docker gelingt nicht immer ohne Reibungen. Auf Red-Hat-Systemen mit Calico traten zum Beispiel Inkonsistenzen mit dem Firewall-Daemon Firewalld auf, was eine Reihe lösbarer, aber lästiger Probleme verursachte.

Listing 9

Security-Context am Beispiel eines Pod

01 [...]
02 apiVersion: v1
03 kind: Pod
04 metadata:
05   name: hello-world
06 spec:
07   containers:
08   # Specification of the Pod's Containers
09   securityContext:
10     readOnlyRootFilesystem: true
11     runAsNonRoot: true
12 [...]

Fazit

Kubernetes ist ein sich schnell entwickelndes Ökosystem, das viele sehr gute Konzepte verfolgt, die das Projekt aber noch nicht alle vollständig umsetzt. Wer sich dessen bewusst ist, auf die Releasezyklen von drei Monaten eingeht und bereit ist, die Entwicklungen an Kubernetes mitzumachen, erhält ein System, das das Entwickeln, Aktualisieren und Anpassen von verteilten Applikationen einfacher macht als in jedem anderen verteilten System.

Multitenancy in Kubernetes

Neben der Absicherung durch Network Policy ist es auch wichtig, einen Cluster auf Container-Ebene abzusichern. Das bedeutet, dass der User keine privilegierten Container verwenden darf, wenn er nicht dazu berechtigt ist.

User identifizieren sich gegenüber Kubernetes durch ihre Clientzertifikate oder – weniger empfehlenswert – über Passwörter. Auch externe Dienste lassen sich anbinden. Clientzertifikate konstituiert Kubernetes beim Anlegen von Service-Accounts automatisch als Secrets. Die damit verbundenen Konzepte befinden sich zwar noch auf dem Weg vom Alpha- ins Beta-Stadium zur Produktionsreife, lassen sich aber schon recht gut einsetzen.

Privilegierte Container sind die neuen Rootrechte. Sie sind sehr mächtig und bei falscher Anwendung gefährlich. In diesen Containern bettet der Admin etwa den Docker-Socket des umgebenden Hosts ein, um Docker-Images zu bauen. Schlimmstenfalls lässt sich so aber auch der umgebende Node übernehmen.

Der Weg zu einem sicheren System ist nicht einfach und lässt sich an dieser Stelle nur umreißen. Zunächst legt der Kube-Admin Service-Accounts an, erzeugt dann über Role Based Access Control (RBAC, [17]) Rollen und bindet diese an User oder Gruppen. Das Recht, zum Beispiel eine Security-Policy zu verwalten, muss er dabei stark einschränken.

Rollen verleihen Rechte auf Ressourcen in Form von Verben. Bei Verben handelt es sich um Aktionen, hier REST-API-Aufrufe, bei Konzepten um Pods. Vereinfacht geht es um das Recht, lesende (»get« und »list«) oder schreibende (»create« oder »update«) Operationen auf Pods, Services, Ingresses oder Secrets anzuwenden. Das reicht oft tief in die Ressourcen hinein und kann es etwa Usern verbieten, Pods ohne oder mit dem falschen Security-Context anzulegen.

Entzieht der Admin über »spec.securityContext.runAsNonRoot: true« das Recht, in Containern Rootprozesse laufen zu lassen, starten die meisten Beispiele aus dem Netz nicht mehr. Das Feature sollten Admins in sicheren Umgebungen unbedingt einschalten (Listing 9). Es ist in verschiedenen Implementierungen von Kubernetes unterschiedlich gut umgesetzt, auf der sicheren Seite ist der Admin unter anderem mit Open Shift, der Kubernetes-Distribution von Red Hat.

Wer keine Multitenancy (siehe Kasten “Multitenancy in Kubernetes) braucht oder nicht mit extrem sicherheitsempfindlichen Daten arbeitet, kann Kubernetes bereits heute für den Betrieb großer, verteilter Anwendungen einsetzen. Administratoren, die andere Anwendungsfälle im Auge haben, sollten hingegen noch ein oder zwei Versionen abwarten.

Der Autor

Thomas Fricke ist CTO der Endocode AG und beschäftigt sich als Cloud-, Database- und Software-Architekt mit Systemautomation und Devops. In Berlin bewegt er sich durch die Stadt und den Wald, entgegen allen Vorurteilen über die Hauptstadt, hauptsächlich mit dem Fahrrad.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben