Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Das Ziel des Artikels ist es, den Leser mit den Grundlagen der Netzwerkinteraktion und dem Management von Netzwerkrichtlinien in Kubernetes sowie mit dem Drittanbieter-Plugin Calico, das die Standardfunktionen erweitert, vertraut zu machen. Nebenbei werden die Benutzerfreundlichkeit der Konfiguration und einige Funktionen anhand realer Beispiele aus unserer Erfahrung demonstriert.

Schnelle Einführung in das Netzwerkgerät Kubernetes

Ein Kubernetes-Cluster ist ohne Netzwerk nicht vorstellbar. Wir haben bereits Materialien zu den Grundlagen veröffentlicht: „Illustriertes Handbuch zum Netzwerkaufbau in Kubernetes» und „Einführung in die Netzwerkrichtlinien von Kubernetes für Sicherheitsexperten».

Im Kontext dieses Artikels ist es wichtig zu beachten, dass die Netzwerkverbindung zwischen Containern und Knoten nicht von K8s selbst verantwortet wird: Hierfür kommen verschiedene CNI-Plugins (Container Networking Interface) zum Einsatz. Mehr über dieses Konzept haben wir auch berichtet..

Das am weitesten verbreitete dieser Plugins ist Flannel – es gewährleistet die vollständige Netzwerkverbindung zwischen allen Knoten des Clusters, indem es Brücken auf jedem Knoten aufbaut und diesem ein Subnetz zuweist. Eine vollständige und unregulierte Erreichbarkeit ist jedoch nicht immer vorteilhaft. Um eine gewisse minimale Isolation im Cluster zu gewährleisten, ist es erforderlich, in die Firewall-Konfiguration einzugreifen. Im Allgemeinen liegt dies in der Verantwortung des genannten CNI, weshalb alle externen Eingriffe in iptables möglicherweise falsch interpretiert oder sogar völlig ignoriert werden.

Da "out of the box" eine Verwaltung von Netzwerkrichtlinien im Kubernetes-Cluster bereitgestellt wird, gibt es eine NetworkPolicy API. Dieser Ressource, die auf ausgewählte Namensräume anwendbar ist, können Regeln zur Zugangstrennung von einer Anwendung zur anderen beigefügt werden. Sie ermöglicht auch die Konfiguration der Erreichbarkeit zwischen bestimmten Pods, Umgebungen (Namensräumen) oder IP-Adressblöcken:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978

Dieses nicht gerade einfache Beispiel stammt aus offiziellen Dokumentation Es könnte endgültig das Verlangen dämpfen, die Logik der Netzwerkrichtlinien zu verstehen. Dennoch werden wir versuchen, die grundlegenden Prinzipien und Methoden der Verarbeitung von Datenverkehr durch Netzwerkrichtlinien zu verstehen…

Es ist logisch, dass es 2 Arten von Datenverkehr gibt: eingehender (Ingress) und ausgehender (Egress).

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Tatsächlich wird die Richtlinie nach diesen 2 Kategorien der Bewegungsrichtung unterteilt.

Das nächste erforderliche Attribut ist der Selector; das ist der, auf den die Regel angewendet wird. Das kann ein Pod (oder eine Gruppe von Pods) oder ein Namespace sein. Wichtig ist: Beide Typen dieser Objekte müssen ein Label enthalten (label in der Kubernetes-Terminologie) – genau diese haben Einfluss auf die Richtlinien.

Neben der endlichen Anzahl an Selectoren, die durch ein bestimmtes Label verbunden sind, gibt es die Möglichkeit, Regeln wie "Alles erlauben/verbieten" in verschiedenen Variationen zu schreiben. Hierfür werden Konstruktionen wie diese verwendet:

  podSelector: {}
  ingress: []
  policyTypes:
  - Ingress

– In diesem Beispiel wird allen Pods im Namespace der eingehende Datenverkehr verwehrt. Um das Gegenteil zu erreichen, kann folgende Konstruktion verwendet werden:

  podSelector: {}
  ingress:
  - {}
  policyTypes:
  - Ingress

Ähnlich für den ausgehenden Datenverkehr:

  podSelector: {}
  policyTypes:
  - Egress

– um ihn auszuschalten. Und das ist für die Aktivierung:

  podSelector: {}
  egress:
  - {}
  policyTypes:
  - Egress

Wenn wir zur Auswahl des CNI-Plugins für den Cluster zurückkehren, ist zu beachten, dass nicht jedes Netzwerk-Plugin die Verwendung von NetworkPolicy unterstützt. Zum Beispiel kann das bereits erwähnte Flannel keine Netzwerkrichtlinien konfigurieren, was deutlich angegeben ist im offiziellen Repository. Dort wird auch eine Alternative erwähnt – ein Open Source-Projekt Calico, das die Standard-API von Kubernetes im Hinblick auf Netzwerkrichtlinien erheblich erweitert.

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Lernen wir Calico kennen: Theorie

Das Calico-Plugin kann in Kombination mit Flannel (Unterprojekt Canal) oder eigenständig verwendet werden und deckt sowohl die Funktionen zur Gewährleistung der Netzwerkverbindung als auch die Möglichkeiten zur Verwaltung der Verfügbarkeit ab.

Welche Möglichkeiten bietet die Verwendung eines "Out-of-the-Box"-K8s und des API-Sets aus Calico?

Das ist in NetworkPolicy integriert:

  • Richtlinien sind auf den Namespace beschränkt;
  • Richtlinien werden auf Pods angewendet, die mit Labels versehen sind;
  • Regeln können auf Pods, Namespaces oder Subnetze angewendet werden;
  • Regeln können Protokolle, benannte oder symbolische Portangaben enthalten.

Und so erweitert Calico diese Funktionen:

  • Richtlinien können auf jedes Objekt angewendet werden: Pod, Container, virtuelle Maschine oder Schnittstelle;
  • Die Regeln können eine spezifische Aktion enthalten (Verbot, Erlaubnis, Protokollierung);
  • Als Ziel oder Quelle der Regeln können Port, Portbereiche, Protokolle, HTTP- oder ICMP-Attribute, IP oder Subnetz (Version 4 oder 6) sowie beliebige Selektoren (Knoten, Hosts, Umgebungen) verwendet werden;
  • Zusätzlich kann der Datenverkehr durch DNAT-Einstellungen und Traffic-Forwarding-Richtlinien reguliert werden.

Die ersten Commits auf GitHub im Calico-Repository stammen aus dem Juli 2016, und bereits ein Jahr später belegte das Projekt führende Positionen in der Organisation des Netzwerkbetriebs von Kubernetes – wie beispielsweise die Ergebnisse einer Umfrage belegen, die von The New Stack durchgeführt wurde.:

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Viele große Managed-Lösungen mit K8s, wie Amazon EKS, Azure AKS, Google GKE und andere haben empfohlen, es zu verwenden.

Was die Leistung betrifft, so ist alles hervorragend. Bei den Tests ihres Produkts zeigte das Calico-Entwicklungsteam astronomische Ergebnisse, indem es über 50.000 Container auf 500 physischen Knoten mit einer Geschwindigkeit von 20 Containern pro Sekunde startete. Bei der Skalierung traten keine Probleme auf. Solche Ergebnisse wurden bereits beim Ankündigung der ersten Version genannt. Unabhängige Studien zur Bandbreite und Ressourcennutzung bestätigen ebenfalls die Leistung von Calico, die praktisch gleichwertig mit Flannel ist. Zum Beispiel:

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Das Projekt entwickelt sich sehr schnell, es wird in beliebten Managed-K8s-Lösungen, OpenShift, OpenStack unterstützt, und es besteht die Möglichkeit, Calico beim Bereitstellen eines Clusters mit Hilfe von kops, wobei auch von der Erstellung von Service-Mesh-Netzwerken die Rede ist (hier ist ein Beispiel für die Verwendung zusammen mit Istio).

Praxis mit Calico

Im allgemeinen Fall der Verwendung von Vanilla Kubernetes beschränkt sich die Installation von CNI auf die Verwendung der Datei calico.yaml, die von der offiziellen Website heruntergeladen wurde, mithilfe von kubectl apply -f.

In der Regel ist die aktuelle Version des Plugins mit den letzten 2-3 Versionen von Kubernetes kompatibel: Einstellungen in älteren Versionen werden nicht getestet und nicht garantiert. Den Angaben der Entwickler zufolge funktioniert Calico auf einem Linux-Kernel höher als 3.10 unter CentOS 7, Ubuntu 16 oder Debian 8, über iptables oder IPVS.

Isolation innerhalb der Umgebung

Um ein allgemeines Verständnis zu schaffen, betrachten wir einen einfachen Fall, um zu verstehen, wie sich Netzwerkrichtlinien in der Calico-Notation von den Standardrichtlinien unterscheiden und wie der Ansatz zur Erstellung von Regeln ihre Lesbarkeit und Konfigurationsflexibilität vereinfacht:

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Im Cluster sind 2 Webanwendungen bereitgestellt: eine auf Node.js und eine auf PHP, von denen eine Redis verwendet. Um den Zugriff auf Redis aus PHP zu sperren und gleichzeitig die Verbindung zu Node.js aufrechtzuerhalten, reicht es, die folgende Richtlinie anzuwenden:

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  name: allow-redis-nodejs
spec:
  podSelector:
    matchLabels:
      service: redis
  ingress:
  - from:
    - podSelector:
        matchLabels:
          service: nodejs
    ports:
    - protocol: TCP
      port: 6379

Im Grunde haben wir eingehenden Datenverkehr auf den Redis-Port aus Node.js erlaubt. Und wir haben ausdrücklich nichts anderes verboten. Sobald eine NetworkPolicy vorhanden ist, beginnen alle darin erwähnten Selektoren sich zu isolieren, sofern nicht anders angegeben. Dabei gelten die Isolierungsregeln nicht für andere Objekte, die nicht durch den Selektor abgedeckt sind.

Im Beispiel wird verwendet apiVersion von Kubernetes "out-of-the-box", aber nichts hindert daran, die gleichnamige Ressource aus der Calico-Bereitstellung. Die Syntax dort ist umfangreicher, daher muss die Regel für den oben beschriebenen Fall wie folgt umgeschrieben werden:

apiVersion: crd.projectcalico.org/v1
kind: NetworkPolicy
metadata:
  name: allow-redis-nodejs
spec:
  selector: service == 'redis'
  ingress:
  - action: Allow
    protocol: TCP
    source:
      selector: service == 'nodejs'
    destination:
      ports:
      - 6379

Die oben genannten Konstruktionen zum Erlauben oder Verhindern des gesamten Datenverkehrs über die Standard NetworkPolicy API enthalten komplexe, schwer verständliche Konstruktionen mit Klammern. Im Fall von Calico genügt es, um die Logik der Firewall-Regel umzukehren, einfach action: Allow auf action: Deny.

Isolierung nach Umgebungen

Stellen wir uns nun die Situation vor, dass eine Anwendung Geschäftsdaten für deren Sammlung in Prometheus und anschließende Analyse über Grafana generiert. In den Exports können sensible Daten enthalten sein, die standardmäßig wiederum für alle zugänglich sind. Lassen Sie uns diese Daten vor neugierigen Augen schützen:

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Prometheus wird in der Regel in eine separate Serviceumgebung ausgelagert – im Beispiel wird dies ein Namespace des folgenden Typs sein:

apiVersion: v1
kind: Namespace
metadata:
  labels:
    module: prometheus
  name: kube-prometheus

Feld metadata.labels ist hier nicht zufällig. Wie bereits oben erwähnt, namespaceSelector (wie auch podSelector) arbeitet mit Labels. Daher muss, um Metriken von allen Pods an einem bestimmten Port abzurufen, irgendein Label hinzugefügt werden (oder eines aus den bestehenden genommen) und dann eine Konfiguration wie folgt angewendet werden:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-metrics-prom
spec:
  podSelector: {}
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          module: prometheus
    ports:
    - protocol: TCP
      port: 9100

Im Falle der Verwendung von Calico-Richtlinien wäre die Syntax wie folgt:

apiVersion: crd.projectcalico.org/v1
kind: NetworkPolicy
metadata:
  name: allow-metrics-prom
spec:
  ingress:
  - action: Allow
    protocol: TCP
    source:
      namespaceSelector: module == 'prometheus'
    destination:
      ports:
      - 9100

Insgesamt kann man durch das Hinzufügen solcher Richtlinien für spezifische Bedürfnisse vor böswilligem oder unbeabsichtigtem Eingreifen in die Funktion von Anwendungen im Cluster schützen.

Das beste Vorgehen, so die Entwickler von Calico, ist der Ansatz "Verbot alles und öffne explizit, was nötig ist", verankert in offiziellen Dokumentation (diesem Ansatz folgen auch andere, insbesondere in der bereits erwähnten Artikel).

Anwendung zusätzlicher Calico-Objekte

Ich erinnere daran, dass man mit dem erweiterten API-Satz von Calico die Verfügbarkeit von Knoten regulieren kann, ohne sich auf Pods zu beschränken. Im folgenden Beispiel wird dies durch GlobalNetworkPolicy verhindert, dass ICMP-Anfragen im Cluster durchkommen (zum Beispiel Pings von einem Pod zu einem Knoten, zwischen Pods oder von einem Knoten zur IP eines Pods):

apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: block-icmp
spec:
  order: 200
  selector: all()
  types:
  - Ingress
  - Egress
  ingress:
  - action: Deny
    protocol: ICMP
  egress:
  - action: Deny
    protocol: ICMP

Im obigen Fall bleibt es den Knoten des Clusters möglich, über ICMP miteinander zu kommunizieren. Und dieses Problem wird durch GlobalNetworkPolicy, angewendet auf die Entität HostEndpoint:

apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: deny-icmp-kube-02
spec:
  selector: "role == 'k8s-node'"
  order: 0
  ingress:
  - action: Allow
    protocol: ICMP
  egress:
  - action: Allow
    protocol: ICMP
---
apiVersion: crd.projectcalico.org/v1
kind: HostEndpoint
metadata:
  name: kube-02-eth0
  labels:
    role: k8s-node
spec:
  interfaceName: eth0
  node: kube-02
  expectedIPs: ["192.168.2.2"]

Fall mit VPN

Abschließend möchte ich ein ganz reales Beispiel für die Nutzung der Funktionen von Calico im Fall der internen Cluster-Interaktion anführen, wenn der Standard-Satz an Richtlinien nicht ausreicht. Für den Zugriff von Clients auf die Webanwendung wird ein VPN-Tunnel verwendet, und dieser Zugriff wird streng kontrolliert und auf eine bestimmte Liste von zugelassenen Diensten beschränkt:

Calico für Netzwerke in Kubernetes: Einführung und ein bisschen Erfahrung

Kunden verbinden sich über den standardmäßigen UDP-Port 1194 mit dem VPN und erhalten beim Verbindungsaufbau Routen zu den Cluster-Subnets der Pods und Dienste. Die Subnetze werden vollständig gepusht, um Dienste bei Neustarts und Adressänderungen nicht zu verlieren.

Der Port in der Konfiguration ist standardmäßig, was einige Nuancen im Konfigurationsprozess der Anwendung und deren Migration in den Kubernetes-Cluster mit sich bringt. Zum Beispiel wurde der UDP-Pool im AWS LoadBalancer erst Ende letzten Jahres in einer begrenzten Anzahl von Regionen eingeführt, und NodePort kann nicht verwendet werden, da er auf allen Knoten des Clusters weitergeleitet wird und es unmöglich ist, die Anzahl der Serverinstanzen zur Gewährleistung der Ausfallsicherheit zu skalieren. Zudem muss der standardmäßig gewählte Portbereich geändert werden...

Nach Durchsicht der möglichen Lösungen wurde Folgendes ausgewählt:

  1. Pods mit VPN sind für den Knoten im Modus hostNetwork, das heißt, sie verwenden die tatsächliche IP.
  2. Der Dienst wird nach außen über ClusterIPbereitgestellt. Auf dem Knoten wird physisch ein Port aktiviert, der von außen unter bestimmten Vorbedingungen (vor allem hinsichtlich einer realen IP-Adresse) zugänglich ist.
  3. Die Bestimmung des Knotens, auf dem der Pod läuft, liegt außerhalb unserer Erzählung. Ich kann nur sagen, dass der Dienst fest an einen Knoten gebunden werden kann oder ein kleiner Sidecar-Dienst geschrieben werden kann, der die aktuelle IP-Adresse des VPN-Dienstes überwacht und die DNS-Einträge von Kunden anpasst – je nachdem, wer welche Ideen hat.

Im Hinblick auf die Routenidentifikation können wir den Kunden im VPN eindeutig anhand seiner vom VPN-Server zugewiesenen IP-Adresse identifizieren. Unten ist ein einfaches Beispiel zur Einschränkung des Zugriffs für einen solchen Kunden auf Dienste, dargestellt am zuvor erwähnten Redis:

apiVersion: crd.projectcalico.org/v1
kind: HostEndpoint
metadata:
  name: vpnclient-eth0
  labels:
    role: vpnclient
    environment: production
spec:
  interfaceName: "*"
  node: kube-02
  expectedIPs: ["172.176.176.2"]
---
apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: vpn-rules
spec:
  selector: "role == 'vpnclient'"
  order: 0
  applyOnForward: true
  preDNAT: true
  ingress:
  - action: Deny
    protocol: TCP
    destination:
      ports: [6379]
  - action: Allow
    protocol: UDP
    destination:
      ports: [53, 67]

Hier wird der Zugang zu Port 6379 strikt untersagt, während der Betrieb des DNS-Dienstes erhalten bleibt, dessen Funktionalität häufig bei der Erstellung von Regeln leidet. Denn, wie bereits erwähnt, wird bei Vorhandensein eines Selektors automatisch eine restriktive Richtlinie angewendet, es sei denn, es ist etwas anderes angegeben.

Ergebnisse

Auf diese Weise kann das erweiterte Calico-API flexibel konfiguriert und die Routing-Strategien im Cluster und darüber hinaus dynamisch geändert werden. Im Allgemeinen kann die Nutzung so wirken, als würde man mit Kanonen auf Spatzen schießen, und die Implementierung eines L3-Netzwerks mit BGP- und IP-IP-Tunneln wirkt in einer einfachen Kubernetes-Installation in einem flachen Netzwerk monströs... Dennoch scheint das Werkzeug in anderen Bereichen durchaus funktionsfähig und nützlich zu sein.

Die Isolation eines Clusters zur Gewährleistung von Sicherheitsanforderungen ist nicht immer umsetzbar, und genau in solchen Fällen kommt Calico (oder eine ähnliche Lösung) ins Spiel. Die im Artikel angegebenen Beispiele (mit geringfügigen Anpassungen) werden in mehreren Installationen unserer Kunden in AWS verwendet.

P.S.

Lesen Sie auch in unserem Blog:

Quelle: habr.com

60GB SSD 8Gb DDR4