Zum Inhalt springen

Plattform-Vorlagen

Auftrag

«Die Gemeinde Hinterbüel hat vom Webshop gehört und will ihre Formulare auch auf Containern betreiben», schreibt Simone Wyss. «Wenn das jedes Mitglied will, könnt ihr nicht jedes Mal von vorne anfangen. Und die Gemeinde darf dem Veloatelier am Samstag nicht den Cluster wegnehmen.»

Lernziele

Nach diesem Kapitel kannst du:

  • Erklären, was eine Plattform-Vorlage ist und was sie einem Mitglied gibt
  • Namespaces mit ResourceQuota, LimitRange und NetworkPolicy begrenzen und trennen
  • Eine Vorlage mit Kustomize für mehrere Mitglieder verwenden
  • Mit einem ApplicationSet für jeden Ordner in Git eine Application erzeugen
  • Den Antrag eines Mitglieds als Pull Request prüfen und annehmen

Ein Plattform-Team bietet einen geprüften Weg an, den andere Teams selbst gehen, oft Golden Path genannt. Es muss dann nicht für jedes Team eine Umgebung von Hand bauen. Die Vorlage legt fest, was jedes Mitglied bekommt. Das Mitglied beantragt seinen Platz, statt auf einen Termin zu warten. In Teil 3 lösten Gruppen, VDC und Quotas in OpenNebula diese Aufgabe. Im Cluster übernehmen das Namespaces und drei Arten von Objekten.

Teil 3 in OpenNebulaTeil 4 in Kubernetes
GruppeNamespace
Quota der GruppeResourceQuota
Grösse der Vorlage ubuntuLimitRange
VDC und eigenes NetzNetworkPolicy

Eine ResourceQuota begrenzt, was alle Pods eines Namespaces zusammen anfordern dürfen.

resourcequota.yaml
# Upper limit of the whole namespace, like the quota of the group in OpenNebula
apiVersion: v1
kind: ResourceQuota
metadata:
name: member
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
pods: "10"
services.nodeports: "0"
# ServiceLB of k3s would open the port of a LoadBalancer service on every node, even without a node port
services.loadbalancers: "0"
persistentvolumeclaims: "2"

Requests sind, was der Scheduler für einen Container reserviert, wie die Spalte CPU einer VM in OpenNebula. Limits sind die Obergrenze, die ein Container nie überschreitet. Eine CPU entspricht einem Kern, 250m einem Viertel davon. services.nodeports: "0" und services.loadbalancers: "0" verbieten Mitgliedern, eigene Ports auf den Nodes zu öffnen. Ohne die zweite Zeile öffnete ServiceLB im Lab für einen Service vom Typ LoadBalancer ohne NodePort trotzdem einen Port auf beiden Nodes. Der Weg der Mitglieder nach aussen ist der Ingress. Alle Ressourcen, die eine Quota begrenzen kann, nennt die Dokumentation von Kubernetes 1.36 zu ResourceQuota.

Sobald eine Quota CPU und Speicher begrenzt, lehnt Kubernetes jeden Pod ohne Requests und Limits ab. Die LimitRange setzt dafür Standardwerte und eine Obergrenze pro Container. Wie Standardwerte und Grenzen zusammenwirken, zeigt die Dokumentation zu LimitRange.

limitrange.yaml
# Default values for containers without requests and limits, the quota needs them
apiVersion: v1
kind: LimitRange
metadata:
name: member
spec:
limits:
- type: Container
defaultRequest:
cpu: 50m
memory: 64Mi
default:
cpu: 250m
memory: 128Mi
max:
cpu: "1"
memory: 1Gi

Ohne Regel erreicht jeder Pod im Cluster jeden anderen, auch über Namespaces hinweg. Eine NetworkPolicy legt fest, wer einen Pod ansprechen darf. In k3s setzt ein eingebauter Controller solche Regeln durch. Regeln für eingehende und ausgehende Verbindungen beschreibt die Dokumentation zu NetworkPolicy.

networkpolicy.yaml
# Pods of a member accept traffic only from their own namespace and from the ingress of k3s in kube-system
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: member
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system

podSelector: {} meint alle Pods des Namespaces. Sie nehmen nur Verbindungen von Pods im eigenen Namespace und aus kube-system an, wo Traefik läuft. Im Lab bekam ein Pod im Namespace default vom Webshop «Connection refused», ein Pod in velo erreichte ihn. Ein frisch gestarteter Pod scheiterte dabei mit seinen ersten zwei Anfragen. Der Controller braucht nach dem Start eines Pods einige Sekunden, bis er dessen Adresse in seine Regeln aufgenommen hat.

Terminal-Fenster
kubectl -n default run np-test --rm -i --restart=Never --image=busybox:1.37 -- sh -c "sleep 5; wget -qO- -T 5 http://webshop.velo/healthz"
kubectl -n velo run np-test --rm -i --restart=Never --image=busybox:1.37 -- sh -c "sleep 5; wget -qO- -T 5 http://webshop.velo/healthz"

Das sleep 5 gibt dem Controller Zeit, den neuen Pod in seine Regeln aufzunehmen.

Zusammen mit einer vierten Datei für den Namespace bilden diese drei Dateien die Vorlage in templates/member. Kustomize, in kubectl und Argo CD eingebaut, setzt daraus für jedes Mitglied die passenden Objekte zusammen, ohne die Vorlage zu kopieren. Wie eine Component in eine Kustomization kommt, beschreibt die Anleitung zu Components von Kustomize.

kustomization.yaml
# The template of a member namespace. A folder in members/ sets the namespace, kustomize also renames the Namespace object
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- namespace.yaml
- resourcequota.yaml
- limitrange.yaml
- networkpolicy.yaml

Ein Mitglied ist ein Ordner in members mit einer einzigen Datei. Die Zeile namespace setzt den Namespace aller Objekte und benennt das Objekt Namespace der Vorlage gleich mit um. Das Veloatelier braucht für den Webshop mehr CPU, als die Vorlage vorsieht. Ein Patch ersetzt diesen einen Wert.

kustomization.yaml
# Namespace of Veloatelier Matte, the web shop needs more CPU than the template gives
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: velo
components:
- ../../templates/member
patches:
- target:
kind: ResourceQuota
patch: |-
- op: replace
path: /spec/hard/requests.cpu
value: "2"
Terminal-Fenster
kubectl kustomize ~/platform/members/velo

Der Befehl zeigt die fertigen Objekte, ohne etwas zu ändern. Damit prüfst du ein Mitglied, bevor es in Git landet.

Statt für jedes Mitglied eine Application zu schreiben, lässt du sie von einem ApplicationSet nach einem Muster erzeugen.

members.yaml
# One application per folder in members/, a new member is a new folder
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: members
namespace: argocd
spec:
goTemplate: true
generators:
- git:
repoURL: http://FRONTEND:3000/wolkenwerk/platform.git
revision: main
directories:
- path: members/*
# Look for new member folders every minute, the default is three minutes
requeueAfterSeconds: 60
template:
metadata:
name: 'member-{{.path.basename}}'
spec:
project: default
source:
repoURL: http://FRONTEND:3000/wolkenwerk/platform.git
targetRevision: main
path: '{{.path.path}}'
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: true
selfHeal: true

Der Generator git durchsucht das Repo nach Ordnern unter members und erzeugt für jeden eine Application member-<ordner>. Kommt ein Ordner dazu, entsteht ein Namespace. Das ApplicationSet prüft das Repo normalerweise alle drei Minuten, requeueAfterSeconds: 60 verkürzt das auf eine Minute. Alle Optionen des Generators nennt die Dokumentation von Argo CD 3.5 unter Git Generator.

Ein Namespace aus der Vorlage Oben liegt die Vorlage templates/member, eine Component von Kustomize mit dem Namespace member, einer ResourceQuota mit requests.cpu 1, einer LimitRange mit dem Standard 250m pro Container und einer NetworkPolicy, die nur den eigenen Namespace und kube-system durchlässt. In der Mitte setzt der Ordner members/velo den Namespace velo und ersetzt mit einem Patch requests.cpu durch 2. Das ApplicationSet members liest den Ordner mit dem Generator git und erzeugt die Application member-velo. Argo CD rendert sie mit Kustomize, unten stehen die Objekte im Cluster. Achte auf die Unterschiede zur Vorlage. Der Namespace heisst velo und die Quota erlaubt 2 CPU, alles andere kommt unverändert aus der Vorlage. VORLAGE templates/member Component MITGLIED members/velo Kustomization ARGO CD IM CLUSTER von Argo CD angewendet components liest erzeugt rendert mit Kustomize Namespace Platzhalter name: member ResourceQuota Grenze des Namespaces requests.cpu: 1 LimitRange Standard pro Container default cpu: 250m NetworkPolicy eigener Namespace und kube-system Namespace namespace: velo Patch requests.cpu: 2 members ApplicationSet git: members/* member-velo Application members/velo Namespace umbenannt name: velo ResourceQuota Wert aus dem Patch requests.cpu: 2 LimitRange Standard pro Container default cpu: 250m NetworkPolicy Namespace velo und kube-system
Terminal-Fenster
kubectl -n argocd get applicationsets
kubectl -n argocd get applications
kubectl -n velo get resourcequota,limitrange,networkpolicy
kubectl -n velo describe resourcequota member

Die Gemeinde beantragt ihren Namespace mit einem Pull Request. In der Praxis arbeitet das Mitglied in einem eigenen Branch oder Fork. Im Lab spielst du beide Seiten.

Terminal-Fenster
cd ~/platform
git switch -c gemeinde
mkdir -p members/gemeinde
cat > members/gemeinde/kustomization.yaml <<'EOF'
# Namespace der Gemeinde Hinterbüel mit den Grenzen der Vorlage
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: gemeinde
components:
- ../../templates/member
EOF
kubectl kustomize members/gemeinde
git add members
git commit -m "Namespace für die Gemeinde Hinterbüel"
git push -u origin gemeinde

In Gitea erscheint danach der Vorschlag, einen Pull Request für den Branch gemeinde zu öffnen. Als Plattform-Team prüfst du, ob das Mitglied nur seinen Namen setzt oder auch die Vorlage ändert. Ein Antrag, der templates/member ändert, betrifft alle Mitglieder und braucht mehr Aufmerksamkeit. Ist alles in Ordnung, führst du den Pull Request zusammen und holst den neuen Stand. Öffnen, Prüfen und Schliessen eines Pull Requests beschreibt die Dokumentation von Gitea 28.

Nach dem Zusammenführen steht der Pull Request in Gitea auf «Merged», der Reiter «Files Changed» zeigt seinen Diff.

Der Pull Request #1 der Gemeinde in Gitea. Oben steht Merged, platform hat den Branch gemeinde in main zusammengeführt. Der Verlauf zeigt die Beschreibung, den Commit, den Merge und das Löschen des Branches.
Der Reiter Files Changed desselben Pull Requests. Der Antrag fügt nur die Datei members/gemeinde/kustomization.yaml mit sechs Zeilen hinzu, die Vorlage templates/member bleibt unberührt.
Terminal-Fenster
git switch main
git pull
kubectl get namespace gemeinde
kubectl -n argocd get applications

Im Lab stand der Namespace gemeinde mit Quota, LimitRange und NetworkPolicy zehn Sekunden nach dem Zusammenführen. Weil das ApplicationSet nur jede Minute nachschaut, kann es auch eine gute Minute dauern. Niemand hat dafür kubectl apply ausgeführt.

Argo CD führt die Gemeinde danach als eigene Application.

Die Applications in Argo CD nach dem Pull Request der Gemeinde. member-gemeinde und member-velo stammen aus dem ApplicationSet, root und webshop aus Kapitel 4.4. Alle vier sind Healthy und Synced mit dem Branch main.

Für Meilenstein 2 bekommt die Gemeinde die Grenzen aus deinen persönlichen Werten. Du ergänzt ihre Datei in einem zweiten Pull Request um einen Patch wie beim Veloatelier. Ein Patch darf mehrere Operationen enthalten, je eine Zeile - op: replace für limits.cpu und für limits.memory. Mit kleineren Grenzen stoppt der Test unten früher, im Lab mit 1 CPU schon nach vier Pods.

Was die Quota bewirkt, zeigt ein Deployment, das zu viel anfordert.

Terminal-Fenster
kubectl -n gemeinde create deployment fill --image=busybox:1.37 --replicas=12 -- sleep 3600
kubectl -n gemeinde get deployment fill
kubectl -n gemeinde get events --field-selector reason=FailedCreate
kubectl -n gemeinde delete deployment fill
kubectl -n gemeinde wait --for=delete pod -l app=fill --timeout=60s

Im Lab liefen mit den Werten der Vorlage 8 von 12 Pods. Jeder Pod bekam von der LimitRange ein Limit von 250m CPU, nach acht Pods war die Quota von zwei CPU erreicht.

Error creating: pods "fill-56886798df-vjq4b" is forbidden: exceeded quota: member, requested: limits.cpu=250m, used: limits.cpu=2, limited: limits.cpu=2

Wie bei der Quota in OpenNebula bleiben die anderen Mitglieder unberührt. Der Webshop des Veloateliers läuft weiter, egal was die Gemeinde startet.

Der Check dieses Teils prüft die drei Werkzeuge und die zwei VMs im State von OpenTofu. Dazu kommen der Cluster mit den Adressen der Pods, Gitea mit dem Repo, die Applications in Argo CD, der Webshop hinter Traefik und die Namespaces der Mitglieder mit Quota, LimitRange und NetworkPolicy. Starte ihn auf dem Front-end, sobald der Namespace der Gemeinde steht.

Terminal-Fenster
python3 ~/cc/checks/check.py platform

Aufgabe Lab

Rolle die Vorlage für das Veloatelier aus, beantrage den Namespace der Gemeinde als Pull Request und nimm ihn an. Zeige die Quota an ihrer Grenze und die Trennung durch die NetworkPolicy. Lass zum Schluss den Check platform laufen und lade seinen Bericht in dein Abgabe-Repo.

Lösung

kubectl -n argocd get applications zeigt member-velo und member-gemeinde als Synced und Healthy. kubectl -n velo describe resourcequota member zeigt requests.cpu mit der Grenze 2, die Gemeinde hat die Grenze 1 der Vorlage. Mit den Werten der Vorlage bleibt das Deployment fill bei 8 Pods stehen, mit den Limits aus deinen persönlichen Werten bei 4 oder 6 Pods. Ein Test-Pod in gemeinde bekommt vom Webshop in velo «Connection refused». Der Check endet mit «19 von 19 Prüfungen der Basis erfüllt».

Aufgabe Überbuchen

Zähle die Quotas der Mitglieder zusammen und vergleiche sie mit dem, was der Cluster hat. Ist es ein Fehler, wenn die Summe grösser ist?

Lösung

Mit Velo und Gemeinde ergeben die Requests zusammen 3 CPU und 2 Gi Speicher, die Limits 4 CPU und 4 Gi. Die zwei VMs haben 8 vCPU und 12 GB, davon braucht k3s selbst einen Teil. Eine grössere Summe der Limits ist kein Fehler, solange nicht alle gleichzeitig an ihre Grenze gehen, genau wie bei den Quotas in Kapitel 3.2. Bei den Requests wäre Überbuchen riskanter, denn der Scheduler startet keinen Pod mehr, wenn die reservierten Werte die Nodes füllen.

Texte und Abbildungen stehen unter CC BY-SA 4.0 und der Code unter der MIT-Lizenz. Die Quellen liegen auf GitHub.