Isolierte Stages innerhalb eines Kubernetes-Clusters erstellen

Staging in Kubernetes

Michael Frembs

© Shutterstock/Redchanka

Wie viele wissen, werden in der Entwicklung oft mehrere Stages verwendet, etwa dev, test und prod. Wenn man nun die Transformation in die Cloud angeht, stellt sich oft die Frage, wie viele Kubernetes-Cluster installiert werden sollen. Soll für jede Stage ein eigener Kubernetes-Cluster installiert werden, oder kann man mehrere Stages zusammenführen und sich somit Installations- und Konfigurationsaufwand sparen? Im Folgenden wird aufgezeigt, wie innerhalb eines Kubernetes-Clusters voneinander isolierte Stages erstellt werden können. Im Anschluss werden die Vor- und Nachteile mehrere Stages innerhalb eines Kubernetes-Clusters diskutiert.

Während der Entwicklung einer Anwendung werden unterschiedliche Anforderungen an die jeweiligen Stages gestellt. Während in der dev-Stage alles auf den Entwickler ausgelegt ist – dieser also viele Rechte hat, wenig Rücksicht auf andere nehmen muss und somit die Anwendung auch mal nicht funktionieren kann bzw. nicht ansprechbar sein darf –, sieht es in der prod-Stage komplett anders aus. Auf dieser Stage laufen die Anwendungen, die für die Nutzer gedacht sind. Das heißt, diese Anwendungen müssen hochverfügbar sein (Zero Downtime). Außerdem bringen diese Nutzer produktive, personenbezogene Daten mit, die nicht für jedermanns Augen gedacht sind. Mehrere Stages sind also nicht nur eine Best Practice, sondern unabdingbar.

Namespaces

Wenn man beim Sprung in die Cloud nun Kubernetes einsetzt, muss man fast unweigerlich Namespaces zur Gruppierung und Sortierung der Ressourcen verwenden. Die offizielle Dokumentation schreibt dazu: „Kubernetes supports multiple virtual clusters backed by the same physical cluster. These virtual clusters are called namespaces“. Kubernetes bietet hierfür ein RESTful API an. Dementsprechend findet jede Konfiguration über eine Ressource statt, diese werden in Namespaces abgelegt und gruppiert. Vorstellbar – wenn auch zu grobgranular – wäre ein Namespace pro Stage. Empfehlenswerter wäre ein Namespace pro Stage und Produkt bzw. Anwendung, vor allem wenn der Kubernetes-Cluster von mehreren Teams genutzt wird.

Pro Namespace können unterschiedliche Konfigurationen vorgenommen werden. Es ist unter anderem möglich, Quotas zu definieren. Dadurch wird der Ressourcenverbrauch (RAM und CPU) aller laufenden Pods innerhalb des Namespace gedeckelt. Den dev-Namespaces können z. B. weniger Ressourcen zur Verfügung gestellt werden als den test-Namespaces. Des Weiteren ist es möglich, auch Limits auf Anzahl von beispielsweise Pods zu setzen. So kann man die ReplicaSets bzw. AutoScaler eingrenzen. Übrigens: Sollte auf dem Namespace Quotas auf Ressourcen gesetzt werden, lohnt sich ein Blick in die LimitRanges, da ansonsten jeder Pod angeben muss, welche Ressourcen er benötigt (was aber sowieso empfehlenswert ist).

DevOpsCon Istio Cheat Sheet

Free: BRAND NEW DevOps Istio Cheat Sheet

Ever felt like service mesh chaos is taking over? As a follow-up to our previous cheat sheet featuring the most important commands and functions, DevOpsCon speaker Michael Hofmann has put together the 8 best practices you should keep in mind when using Istio.

Neben Quotas können auch die Zugriffsmöglichkeiten auf Namespace-Ebene eingeschränkt werden. Dank RBAC kann feingranular pro Ressource und Zugriffsmethode definiert werden, wer (sowohl LDAP-Nutzer als auch technische Service-Accounts) was machen darf. Das gibt Entwicklern die Möglichkeit, auf der dev-Stage nahezu Admin-Rechte und ab test-Stage eingeschränkte Rechte zu haben.

Isolierung

Namespaces lösen zwar die ersten Probleme, aber noch nicht alle. Standardmäßig kann in Kubernetes jede Applikation auf jede andere zugreifen  – auch außerhalb des Namespace. Kubernetes unterstützt Pods und Services mit einem eigenen DNS. Der Zugriff funktioniert über die Angabe des Service-Namens und gegebenenfalls des Namespace. Dies ist bei der Aufteilung in Stages – mit wenigen Ausnahmen – so nicht erwünscht. Applikationen in der dev-Stage sollen nicht Applikationen aus der test-Stage aufrufen können und vice versa. Bei Kubernetes bieten die Network Policies eine Lösung an. Um Network Policies zu verstehen, sind drei Regeln wichtig:

  • Wie bereits erwähnt: Gibt es keine Regel, ist der Zugriff erlaubt.
  • Der Zugriff wird verhindert, wenn es für das Ziel-Pod Regeln gibt, das Quell-Pod aber nicht im Ingress-Teil dieser Regel vorkommt.
  • Der Zugriff wird verhindert, wenn es für das Quell-Pod Regeln gibt, das Ziel-Pod aber nicht im Egress-Teil dieser Regel vorkommt.

Zusammengefasst heißt das: Über Network Policies kann man nur Zugriffe erlauben, nicht aber verbieten. In der Praxis wird in der Regel defensiv sowohl auf Quell- als auch auf Ziel-Pod eine Network Policy definiert. Eine NetworkPolicy ist an einen Namespace gebunden und besteht aus drei Teilen:

  1. Der Information, welche Pods innerhalb eines Namespaces von den folgenden Regeln betroffen sind,
  2. Ingress-Regeln, die die eingehende Kommunikation der Pods betreffen,
  3. Egress-Regeln, die die ausgehende Kommunikation der Pods betreffen.

Dabei werden in der Regel die Kommunikationspartner per Labelselektion auf Pod- bzw. Namespace-Ebene ausgesucht. In unserem Fall ist also eine Regel notwendig, die erstens alle Pods innerhalb des eigenen Namespace betrifft, auf die zweitens jeder Pod innerhalb der Namespaces mit dem Label stage: dev zugreifen kann (in den Namespaces muss das entsprechende Label gesetzt sein) sowie drittens jeder Pod Pods in den anderen dev-Namespaces aufrufen kann. Eine kleine Stolperfalle gibt es noch: Bei Teil drei (Egress) muss der Kubernetes-DNS-Port 53 extra freigeschaltet werden, ansonsten können die URLs nicht mehr aufgelöst werden.
Zusammengefasst kann eine Beispielkonfiguration so aussehen wie in Listing 1 dargestellt.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dev-stage
  namespace: my-app-dev
spec:
  # 1)
  podSelector: {}
  # 2)
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          stage: dev
  # 3)  
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          stage: dev
  - ports:
    - port: 53
      protocol: "UDP"
    - port: 53
      protocol: "TCP"

Eine nicht unwichtige Anmerkung zu den Network Policies: Diese funktionieren nur, wenn das installierte Network-Plug-in diese auch unterstützt, wie das z. B bei Calico der Fall ist.

Wenn die obige Policy angewendet wird, kann man von außerhalb nicht mehr zugreifen. Das liegt darin begründet, dass die Verbindung vom Ingress-Controller, der im Namespace kube-system liegt, geblockt wird. Hierfür muss also auch eine entsprechende Regel erstellt werden. Das geschieht idealerweise über die Kombination eines Namespace- und eines PodSelectors.

Wie eben gezeigt, haben Labels in Kubernetes einen sehr hohen Stellenwert. Über Labels können Ressourcen gruppiert und selektiert werden. Unter anderem werden die Deployments/Pods mit Labels markiert, die dann in Services zusammengeführt werden. Sollten innerhalb eines Clusters mehrere Stages verwendet werden, ist es empfehlenswert, jede Ressource mit einem stage-Label zu versehen – auch, wenn das im ersten Moment gegebenenfalls etwas übertrieben erscheinen mag. Sollte diese Information später doch noch einmal benötigt werden, wird es schwer, dies nachzupflegen.

Physikalische Trennung

Der bisherige Ansatz bringt eine Lösung für Netzwerkisolation. Wer noch eine physikalische Trennung der CPU-/RAM-Ressourcen haben möchte, kann die Stages auch auf unterschiedliche Worker-Nodes aufteilen. Schauen wir uns dazu zuerst die Kubernetes-Architektur an (Abb. 1).

Abb. 1: Kubernetes-Architektur

Kubernetes-Architektur

Kubernetes besteht aus einem Master Node und mehreren Worker Nodes. Alle Anfragen an Kubernetes gehen über den API-Server, der auf dem Master Node installiert ist. Der Master Node überwacht die Gesundheitszustände der Worker Nodes. Des Weiteren verteilt der Master Node die Pods auf die jeweiligen Worker Nodes. Und genau in diese Logik kann man eingreifen. Man kann Kubernetes anweisen, auf welchen Nodes die Pods gestartet werden sollen.

Das bringt interessante Optionen mit sich. So kann man gezielt Worker Nodes für Stages isoliert zur Verfügung stellen. Außerdem ist es auch möglich, der dev-Stage beispielsweise schwächere Maschinen zur Verfügung zu stellen als der test-Stage. Oder es kann eine explizite Performance-Stage erschaffen werden, auf der nur die Anwendung installiert ist, deren Performance wiederholbar und mit minimierten äußeren Einflussfaktoren ausführbar und messbar ist.

In Kubernetes gibt es dafür zwei Möglichkeiten, um ans Ziel zu kommen. Die erste Variante ist der Node Selector, der allerdings von der Node Affinity abgelöst wird, sobald diese Produktreife erhält. Auch Nodes können mit Labels ausgestattet werden. Für uns heißt das, dass alle Nodes einer Stage mit demselben stage-Label versehen werden. Anschließend kann über Pods (bzw. Pod-Template im Deployment) die Node Affinity angegeben werden. Eine auf das Wesentliche gekürzte Konfiguration könnte so aussehen wie in Listing 2.

apiVersion: apps/v1beta1
kind: Deployment
spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: stage
                operator: In
                values:
                - dev

Die eben vorgestellte Lösung bringt einen Nachteil mit sich: Vergisst man die Affinity im Deployment, wird der Pod auf irgendeinem Node installiert. In diesem Fall gibt es noch eine dritte Lösung, die stattdessen angewandt werden kann: Taints. Bei Taints ist die Logik umgekehrt. Ein Pod kann auf einem Node nur dann installiert werden, wenn dieser den Taint ausdrücklich toleriert. Wenn nun jeder Node mit einem stage-Taint versehen ist, kann ein Pod nicht mehr aus Versehen auf einem falschen Node deployt werden. Vergisst ein Deployment/Pod die Angabe, kann dieser auf keinem Node installiert werden.

Dazu muss jeder Node per kubectl taint nodes stage=:NoSchedule gekennzeichnet werden. Anschließend sieht ein Deployment beispielsweise so aus wie in Listing 3. Es ist nur eine der beiden Lösungen (Node Affinity oder Taints) notwendig.

apiVersion: apps/v1beta1
kind: Deployment
spec:
  template:
    spec:
      tolerations:
      - key: "stage"
        operator: "Equal"
        value: "dev"
        effect: "NoSchedule"

Die Zuteilung auf einzelne Nodes bringt Vor- und Nachteile. So hat man eine harte Aufteilung auf unterschiedliche Maschinen, die man über Firewalls voneinander trennen kann, allerdings wird dadurch etwas von der Flexibilität genommen. Wenn beispielswiese auf der test-Stage noch Ressourcen verfügbar wären, auf der dev aber nicht mehr, kann so auf der dev-Stage nichts mehr installiert werden.

Diskussion

Der Artikel zeigt auf, wie man mit Hilfe von Namespaces, RBAC, Quotas, Network Policies und Node Affinities/Taints mehrere Stages innerhalb eines Clusters erstellen kann. Geht man den vollen Weg, sieht die Übersichtsgrafik aus wie in Abbildung 2.

Abb. 2: Stages innerhalb eines Clusters

Fazit

Auf den ersten Blick ist zu erkennen, dass die dev- und test-Nodes zwar voneinander getrennt sind, beide aber immer noch einen gemeinsamen Master haben. Das bringt die Kubernetes-Architektur mit sich. Möchte man auch diese trennen, hat man zwei Kubernetes-Cluster. Das führt uns auch zu einem Angriffspunkt der Lösung. Gibt es Probleme mit dem Master bzw. dem API-Server – wie es bereits Ende 2018 der Fall war (CVE-2018–1002105) –, kann wieder auf die anderen Stages zugegriffen werden. Das bedeutet, dass die vorgestellte Lösung nur verwendet werden sollte, wenn die einzelnen Parteien sich freundlich gegenüberstehen. Bei einer Aufteilung auf Stages kann in der Regel davon ausgegangen werden. Diese Ansätze aber für Multi-Tenancy zu verwenden, ist nicht ratsam.

Es ist empfehlenswert, für die prod-Stage einen eigenen, isolierten Cluster aufzusetzen. Nur so kann man sichergehen, dass bei einer fehlerhaften Konfiguration in einer der vorherigen Stages die prod-Stage nicht in Mitleidenschaft gezogen wird. Außerdem können dadurch Updates des Kubernetes-Clusters auf der dev-Stage getestet und verifiziert werden.

Die Lösung ist, wie bereits angedeutet, auch fehleranfällig, wenn nicht alle Labels sauber gesetzt werden. Wird beispielsweise in einem Namespace ein Label falsch oder nicht gesetzt, wird dieser von der Network Policy abgewiesen. Es ist also auch hier wichtig, dass sorgfältig gearbeitet wird und die Prozesse möglichst automatisiert werden. Dafür bringt die Möglichkeit, mehrere Stages in einem Kubernetes-Cluster zu realisieren, eine Erleichterung des Kubernetes-Betriebs mit sich, da dieser weniger Cluster betreiben, installieren, verwalten und pflegen muss.

Geschrieben von
Michael Frembs
Michael Frembs
Michael Frembs ist Teil des Softwarearchitektenteams und Senior Consultant bei der ARS Computer und Consulting GmbH. Seit mehreren Monaten liegt sein Fokus auf Cloud-native-Entwicklung, Kubernetes und Cloud-Services.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: