Alle Artikel

Kubernetes-Kostenoptimierung auf AWS

So senken Sie die Kosten Ihrer Kubernetes-Microservices auf AWS: mit EC2-Auto-Scaling, Right-Sizing und zeitlich geplanten Kapazitäten.

·

Marten Wick

Bildschirm foto 2023 01 24 um 14 47 51 300x163

Hinweis: Dieser Beitrag wurde aus dem Englischen übersetzt. Fachbegriffe, Code und Befehle wurden bewusst im Original belassen.

EC2 Auto Scaling Groups sind eine Möglichkeit, auf AWS Kosten zu sparen. Diese Art der Skalierung hat jedoch einige Grenzen, die ich Ihnen in diesem Blogbeitrag aufzeigen und für die ich eine Lösung vorstellen möchte. Ebenso Thema: die automatische Skalierung von Deployments, die sich direkt auf die benötigte Anzahl an Nodes auswirkt.

Das Problem mit EC2 Auto Scaling Groups

Wie erwähnt, gibt es bei EC2 Auto Scaling Groups (EC2 ASG) einige Einschränkungen. Wer sie nutzen möchte, muss ein Launch Template anlegen, das die Größe jedes neu hochgefahrenen Nodes festlegt. Genau hier stoßen wir auf Probleme durch die feste Ausstattung neuer Mitglieder der Scaling Group.

Die neue Instanz kann sein:

  1. zu klein für den auszuführenden Pod
    --> Die Anwendung ist im Cluster nicht schedulebar

  1. zu groß für den auszuführenden Pod
    --> Die Instanz verbraucht mehr Ressourcen als nötig. Das führt am Monatsende zu höheren Kosten.

Die Lösung: Karpenter

Und hier kommt Karpenter ins Spiel. Es handelt sich um ein von AWS gestartetes Open-Source-Projekt, das derzeit nur für AWS verfügbar ist. Karpenter bietet einen intelligenten Weg, Ihren Kubernetes-Cluster zur richtigen Zeit auf die richtige Größe zu skalieren.

Was es tut:

  • Beobachten von Pods, die der Kubernetes-Scheduler als nicht schedulebar markiert hat

  • Auswerten der von den Pods geforderten Scheduling-Bedingungen (Resource Requests, NodeSelectors, Affinities, Tolerations und Topology Spread Constraints)

  • Bereitstellen von Nodes, die die Anforderungen der Pods erfüllen

  • Entfernen der Nodes, sobald sie nicht mehr benötigt werden

(Quelle: https://karpenter.sh/v0.19.3/)

Zusammengefasst: Karpenter verwaltet die Größe des Clusters auf intelligente und kostensparende Weise und senkt so die monatliche Cloud-Rechnung.

Karpenter

Einrichtung

Da die Einrichtung in der offiziellen Dokumentation sehr gut beschrieben ist, arbeiten Sie zum Aufsetzen und Vorbereiten Ihres Testclusters bitte die Schritte unter https://karpenter.sh/preview/getting-started/getting-started-with-eksctl/ durch. Ich betrachte hier nur die Definitionsdatei der Provisioner und gehe dann zur Demonstration über.

Provisioner und AWSNodeTemplate

Der Provisioner versetzt Karpenter in die Lage, neue Nodes für den Cluster zu definieren und hochzufahren; das „AWSNodeTemplate” referenziert Subnetz und Security Groups des Clusters.


Im obigen Beispiel werden die Instanzen als Spot-Instanzen platziert. In der Provisioner-YAML-Datei lassen sich weitere Anforderungen definieren, etwa eine Liste zulässiger Instanztypen (weitere Optionen unter https://karpenter.sh/preview/c…). Definieren Sie diese Liste nicht, wählt Karpenter automatisch den passenden Typ für die wartenden Pods. Zusätzlich können Sie die Größe des neuen Nodes im Abschnitt „limits” begrenzen und dort etwa CPU- oder Speichergröße festlegen. Der Wert „ttlSecondsAfterEmpty” gibt die Zeit in Sekunden an, nach der ein leerer und nicht mehr benötigter Node beendet und aus dem Cluster entfernt wird.

Anwendung

Nachdem wir die Voraussetzungen und Karpenter selbst eingerichtet haben, können wir damit unseren Testcluster skalieren. Wir prüfen die Bereitstellung und Beendigung von Nodes mit einem Deployment, das Karpenter zum Handeln zwingt.

Node-Bereitstellung

Zunächst wird das Deployment ohne Replicas erstellt. Das beeinflusst die Cluster-Last nicht, es werden also keine neuen Nodes benötigt.

Terminal, das ein Kubernetes-Deployment namens inflate mit null Replicas und einer CPU-Anforderung von einem Core anwendetTerminal-Ausgabe von kubectl get pods, nodes und deployments mit zwei bereiten Knoten und dem Deployment inflate mit null von null Pods

Nachdem wir das Deployment auf 5 skalieren, muss Karpenter einen neuen Node bereitstellen, damit die neuen Pods Platz finden.

Node-Beendigung

Es gibt zwei Wege, Node-Instanzen aus Ihrem Kubernetes-Cluster zu entfernen: automatisch und manuell.

Die automatische Beendigung übernimmt Karpenter selbst, sobald ein Node keine Workloads mehr ausführt. Erkennt Karpenter einen ungenutzten bzw. leeren Node, beendet es ihn nach der in der Provisioner-Definition festgelegten Zeit (Wert von ttlSecondsAfterEmpty). Für die Details werfen wir einen Blick in die Logs des Controller-Containers im Karpenter-Pod:

Karpenter-Controller-Log, das zeigt, wie ein leerer Knoten deprovisioniert, abgeriegelt und gelöscht wird

Das hat hervorragend funktioniert!

Sehen wir uns nun das manuelle Löschen von Nodes an. Es kann nötig sein, Pods, die über mehrere Nodes verteilt sind, auf einem Node zusammenzuführen. Zu Demonstrationszwecken simuliere ich dieses Szenario, indem ich das Deployment so skaliere, dass drei zusätzliche Nodes entstehen. Sobald alles läuft, werden diese neuen Nodes entfernt – Karpenter sollte daraufhin alle Pods auf einem größeren einzelnen Node zusammenführen.

Drei kubectl-scale-Befehle, die das Deployment inflate auf ein, zwei und vier Replicas hochskalierenTerminal-Ausgabe mit vier laufenden inflate-Pods, verteilt auf fünf Worker-Knoten nach dem Hochskalieren

Da die Skalierungsbefehle mit kurzen Pausen dazwischen ausgeführt werden, sind nun drei zusätzliche Nodes bereitgestellt. Wie die Controller-Logs zeigen, wurden zwei neue „large”- und ein „xlarge”-Node bereitgestellt:

Karpenter-Logzeile mit einem neu bereitgestellten Knoten des Instanztyps c5d.largeKarpenter-Logzeile mit einem neu bereitgestellten Knoten des Instanztyps c6id.xlargeKarpenter-Logzeile mit einem neu bereitgestellten Knoten des Instanztyps c6id.large

Wenn wir diese Nodes löschen, sollte Karpenter gezwungen sein, einen neuen Node für die wartenden Pods hochzufahren.

kubectl-delete-node-Befehl, der die drei zusätzlich bereitgestellten Worker-Knoten entfernt

Nach kurzer Zeit steht ein neuer einzelner Node bereit. Ein Blick in die Controller-Logs zeigt, dass er als Instanztyp „2xlarge” dimensioniert wurde, um die wartenden Pods aufzunehmen.

Terminal-Ausgabe nach dem Löschen der Knoten: Die vier inflate-Pods wurden auf drei verbleibende Knoten neu verteilt

kube-green

Automatisierte Skalierung von Deployments

Wie bereits erwähnt, ist die automatische Skalierung von Deployments Teil dieses Blogbeitrags – probieren wir also kube-green aus!

Der Hauptgrund, Deployments automatisch zu skalieren, besteht darin, bekannte Lastspitzen abzufangen und Ausführungskosten zu sparen. kube-green ist ein Werkzeug, um Deployments zu skalieren und CronJobs auszusetzen. Letzteres ist hier nicht Thema.

kube-green einrichten

Als Voraussetzung muss der cert-manager im Cluster vorhanden sein:

kubectl-apply-Befehl, der cert-manager in Version 1.10.1 aus dem offiziellen Release-Manifest installiertTerminal-Ausgabe mit den laufenden Pods und Deployments von cert-manager, cainjector und Webhook im Namespace cert-manager

Die Einrichtung von kube-green besteht aus einem einfachen kubectl-apply-Befehl, der in wenigen Sekunden durchläuft.

kubectl-apply-Befehl, der kube-green aus dem aktuellen Release-Manifest installiertTerminal-Ausgabe mit dem laufenden Pod und Deployment des kube-green Controller Managers im Namespace kube-green

Sobald alles läuft, ist kube-green einsatzbereit!

Anwendung

Die Konfiguration von kube-green ist einfach und besteht aus einer einzigen CRD namens „SleepInfo”. Diese Ressource verwaltet alle Deployments in dem Namespace, in dem sie ausgerollt wurde.

Nachfolgend werfen wir einen Blick auf die Struktur der CRD:

YAML einer kube-green-SleepInfo-Ressource, die Deployments werktags zwischen 20:00 und 08:00 Uhr in der Zeitzone Europe/Berlin pausiert und das Deployment api-gateway ausnimmt
<strong>weekdays</strong>:            Monday-Sunday --> 1-7
                     Every day --> * 
<strong>sleepAt:</strong>             HH:MM 
                     Every hour/minute --> * 
<strong>wakeUpAt:</strong>            (Optional) HH:MM 
                     Every hour/minute --> *  
<strong>timeZone:</strong>            (Optional) Default is UTC. 
                     Define other time zone in IANA specification   
<strong>suspendDeployments:</strong>  (Optional) Default true 
<strong>suspendCronJobs:</strong>     (Optional) Default false

<strong>weekdays</strong>:            Monday-Sunday --> 1-7
                     Every day --> * 
<strong>sleepAt:</strong>             HH:MM 
                     Every hour/minute --> * 
<strong>wakeUpAt:</strong>            (Optional) HH:MM 
                     Every hour/minute --> *  
<strong>timeZone:</strong>            (Optional) Default is UTC. 
                     Define other time zone in IANA specification   
<strong>suspendDeployments:</strong>  (Optional) Default true 
<strong>suspendCronJobs:</strong>     (Optional) Default false

<strong>weekdays</strong>:            Monday-Sunday --> 1-7
                     Every day --> * 
<strong>sleepAt:</strong>             HH:MM 
                     Every hour/minute --> * 
<strong>wakeUpAt:</strong>            (Optional) HH:MM 
                     Every hour/minute --> *  
<strong>timeZone:</strong>            (Optional) Default is UTC. 
                     Define other time zone in IANA specification   
<strong>suspendDeployments:</strong>  (Optional) Default true 
<strong>suspendCronJobs:</strong>     (Optional) Default false

Beispiel

Zu Testzwecken habe ich ein Beispiel erstellt, das die oben genannten Deployments im Default-Namespace nutzt (Deployment „inflate”). Gestoppt um 11:19 Uhr und zwei Minuten später wieder gestartet.

Terminal, das eine Beispiel-SleepInfo-Ressource anwendet, die Deployments um 11:19 Uhr schlafen legt und um 11:21 Uhr wieder startet

Wenn „SleepInfo” zum ersten Mal greift, wird ein Secret angelegt, das die zuletzt gültige Replica-Anzahl und den Namen des gestoppten Deployments enthält – ebenso wie den letzten Operationstyp und dessen Zeitpunkt.

Terminal-Ausgabe des Secrets sleepinfo-example als YAML mit den gespeicherten Deployment-Replicas, dem Operationstyp und dem geplanten ZeitpunktBefehl, der das Feld deployment-replicas aus dem SleepInfo-Secret liest und aus Base64 dekodiert
Output: [{"name":"inflate","replicas"
Output: [{"name":"inflate","replicas"
Output: [{"name":"inflate","replicas"
Befehl, der das Feld scheduled-at aus dem SleepInfo-Secret liest und aus Base64 dekodiert
Befehl, der das Feld scheduled-at aus dem SleepInfo-Secret liest und aus Base64 dekodiert
Output: 2022-12-09T10
Output: 2022-12-09T10
Output: 2022-12-09T10

Wie festgelegt, stellt kube-green zwei Minuten später die Replica-Anzahl des Deployments wieder her und entfernt das nicht mehr benötigte Datenobjekt „deployment-replicas” aus dem Secret, wobei der letzte Operationstyp und dessen Zeitpunkt gespeichert bleiben.

Terminal-Ausgabe des Secrets sleepinfo-example nach dem Aufwecken, in dem der Eintrag mit den gespeicherten Deployment-Replicas entfernt wurdeBefehl, der das Feld operation-type aus dem SleepInfo-Secret liest und aus Base64 dekodiert
Befehl, der das Feld scheduled-at aus dem SleepInfo-Secret liest und aus Base64 dekodiert
Output: 2022-12-09T10
Output: 2022-12-09T10
Output: 2022-12-09T10

Wie zu sehen ist, wurde das Hochskalieren des Deployments erfolgreich durchgeführt.

Terminal mit dem aktuellen Datum und vier inflate-Pods, die rund zweieinhalb Minuten zuvor neu gestartet sind, nachdem kube-green das Deployment wieder hochskaliert hat

Die Kombination von kube-green und Karpenter

Der nächste Schritt liegt auf der Hand: die Kombination beider Werkzeuge. Wenn kube-green die Deployments herunterskaliert, greift Karpenter ein und entfernt leere und ungenutzte Nodes aus dem Cluster.

Da dies demselben Ablauf entspricht wie das manuelle Skalieren im Karpenter-Abschnitt, verzichte ich auf eine vertiefte Betrachtung. Glauben Sie mir, wenn ich sage: In Kombination hat es ausgesprochen gut funktioniert 😊

Teilen