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
Vorlagen
Abschnitt betitelt „Vorlagen“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 OpenNebula | Teil 4 in Kubernetes |
|---|---|
| Gruppe | Namespace |
| Quota der Gruppe | ResourceQuota |
Grösse der Vorlage ubuntu | LimitRange |
| VDC und eigenes Netz | NetworkPolicy |
ResourceQuota
Abschnitt betitelt „ResourceQuota“Eine ResourceQuota begrenzt, was alle Pods eines Namespaces zusammen anfordern dürfen.
# Upper limit of the whole namespace, like the quota of the group in OpenNebulaapiVersion: v1kind: ResourceQuotametadata: name: memberspec: 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.
LimitRange
Abschnitt betitelt „LimitRange“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.
# Default values for containers without requests and limits, the quota needs themapiVersion: v1kind: LimitRangemetadata: name: memberspec: limits: - type: Container defaultRequest: cpu: 50m memory: 64Mi default: cpu: 250m memory: 128Mi max: cpu: "1" memory: 1GiNetworkPolicy
Abschnitt betitelt „NetworkPolicy“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.
# Pods of a member accept traffic only from their own namespace and from the ingress of k3s in kube-systemapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: memberspec: podSelector: {} policyTypes: - Ingress ingress: - from: - podSelector: {} - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-systempodSelector: {} 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.
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.
Kustomize
Abschnitt betitelt „Kustomize“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.
# The template of a member namespace. A folder in members/ sets the namespace, kustomize also renames the Namespace objectapiVersion: kustomize.config.k8s.io/v1alpha1kind: Componentresources: - namespace.yaml - resourcequota.yaml - limitrange.yaml - networkpolicy.yamlEin 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.
# Namespace of Veloatelier Matte, the web shop needs more CPU than the template givesapiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationnamespace: velocomponents: - ../../templates/memberpatches: - target: kind: ResourceQuota patch: |- - op: replace path: /spec/hard/requests.cpu value: "2"kubectl kustomize ~/platform/members/veloDer Befehl zeigt die fertigen Objekte, ohne etwas zu ändern. Damit prüfst du ein Mitglied, bevor es in Git landet.
ApplicationSet
Abschnitt betitelt „ApplicationSet“Statt für jedes Mitglied eine Application zu schreiben, lässt du sie von einem ApplicationSet nach einem Muster erzeugen.
# One application per folder in members/, a new member is a new folderapiVersion: argoproj.io/v1alpha1kind: ApplicationSetmetadata: name: members namespace: argocdspec: 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: trueDer 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.
kubectl -n argocd get applicationsetskubectl -n argocd get applicationskubectl -n velo get resourcequota,limitrange,networkpolicykubectl -n velo describe resourcequota memberDer Antrag der Gemeinde
Abschnitt betitelt „Der Antrag der Gemeinde“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.
cd ~/platformgit switch -c gemeindemkdir -p members/gemeindecat > members/gemeinde/kustomization.yaml <<'EOF'# Namespace der Gemeinde Hinterbüel mit den Grenzen der VorlageapiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationnamespace: gemeindecomponents: - ../../templates/memberEOFkubectl kustomize members/gemeindegit add membersgit commit -m "Namespace für die Gemeinde Hinterbüel"git push -u origin gemeindeIn 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.


git switch maingit pullkubectl get namespace gemeindekubectl -n argocd get applicationsIm 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.

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.
An der Grenze
Abschnitt betitelt „An der Grenze“Was die Quota bewirkt, zeigt ein Deployment, das zu viel anfordert.
kubectl -n gemeinde create deployment fill --image=busybox:1.37 --replicas=12 -- sleep 3600kubectl -n gemeinde get deployment fillkubectl -n gemeinde get events --field-selector reason=FailedCreatekubectl -n gemeinde delete deployment fillkubectl -n gemeinde wait --for=delete pod -l app=fill --timeout=60sIm 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=2Wie 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 von Teil 4
Abschnitt betitelt „Der Check von Teil 4“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.
python3 ~/cc/checks/check.py platformAufgabe 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.