Benutzerdefiniertes Verbindungslimit für Load Balancer

Benutzerdefiniertes Verbindungslimit für Load Balancer

Du würdest gerne ein eigenes Limit für eingehende Verbindungen an deinem Load Balancer festlegen?
In diesem Tutorial lernst du wie das geht.

Über das Verbindungslimit

Das Verbindungslimit (engl. „connection limit“) beschreibt die maximal zugelassene Anzahl an Verbindungen pro Sekunde, die für einen Load Balancer Listener (offenen Frontend-Port) erlaubt sind. Du fragst dich vielleicht, wieso sollte man überhaupt ein Verbindungslimit festlegen? Der naheliegendste Grund ist, um eine Überflutung von HTTP-Anfragen an die Kubernetes Apps vorzubeugen. Mit einer stärkeren Limitierung werden die überschüssigen Verbindungen abgelehnt, wodurch verhindert wird, dass die Kapazitäten des Clusters überlastet werden. Wenn du in deinem Cluster bereits Vorkehrungen für solche Fälle getroffen hast und eine Flut an Anfragen z.B. mittels Autoscaling ausgleichen kannst, so ist es natürlich auch möglich, das Verbindungslimit am Load Balancer weiter zu erhöhen.

Probleme bei hohen Verbindungslimits

In früheren Versionen unseres Load Balancer Dienstes „Octavia“ war die Anzahl an erlaubten Verbindungen nicht limitiert. Allerdings führte das unter bestimmten Umständen zu Problemen bei den Load Balancern. Wenn mehr als drei Listener am Load Balancer konfiguriert werden und kein Verbindungslimit gesetzt ist, dann führt dies dazu, dass die HAProxy-Prozesse des Load Balancers abstürzen. Das Problem wird in diesem Bug-Report näher beschrieben. Um zu vermeiden, dass unsere Kunden in dieses Problem laufen, haben wir uns entschieden, das standardmäßige Verbindungslimit auf 50000 Verbindungen festzulegen. Dieses Tutorial zeigt dir, wie du das Limit selber anpassen kannst.

 

Service Annotations für Load Balancer

Da die Kubernetes Cluster von NWS in unserer OpenStack Cloud betrieben werden, wird die Interaktion zwischen Kubernetes und OpenStack über den „Cloud Provider OpenStack“ realisiert. In der Dokumentation des Providers findet man eine Sektion, die alle verfügbaren Kubernetes Service Annotations für Load Balancers beinhaltet. Die Annotation, die wir benötigen, um ein benutzerdefiniertes Verbindungslimit setzen zu können ist „loadbalancer.openstack.org/connection-limit“.

Setzen der Annotation

Die Annotation wird im Kubernetes Cluster an einem Service Objekt des Load Balancers gesetzt. Du solltest in deiner Serviceliste einen Service vom Typ „LoadBalancer“ finden.

~ $ kubectl get service
NAME                                 TYPE           CLUSTER-IP       EXTERNAL-IP       PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.254.194.107   193.111.222.333   80:30919/TCP,443:32016/TCP   2d
ingress-nginx-controller-admission   ClusterIP      10.254.36.121    <none>            443/TCP                      2d
kubernetes                           ClusterIP      10.254.0.1       <none>            443/TCP                      2d17h
my-k8s-app                           ClusterIP      10.254.192.193   <none>            80/TCP                       47h

In meinem Beispiel wurde der Service automatisch eingerichtet, als ich einen Nginx Ingress Controller per Helm installiert habe. Ich kann den Service einfach editieren und die gewünschte Annotation hinzufügen:

~ $ kubectl edit service ingress-nginx-controller

Nun setzt man die Annotation für das Verbindungslimit auf den gewünschten Wert (in dem Beispiel „100000“):

apiVersion: v1
kind: Service
metadata:
  annotations:
    loadbalancer.openstack.org/connection-limit: "100000"
    meta.helm.sh/release-name: ingress-nginx
    meta.helm.sh/release-namespace: default
  creationTimestamp: "2021-09-08T08:40:59Z"
...

Die Änderungen sichert man und verlässt anschließend den Editormodus. Man erhält als Bestätigung die folgende Meldung:

service/ingress-nginx-controller edited

Wenn nicht, dann sollte man prüfen, ob man einen Tippfehler bei der Annotation hat und ob der gewünschte Wert auch in doppelten Anführungszeichen gesetzt ist. Sobald die Anpassung des Services erfolgreich durchgeführt wurde, dauert es für gewöhnlich 10 bis 15 Sekunden, bis die Änderungen an dem OpenStack Load Balancer durchgeführt wurden. Du kannst den Status deines Load Balancers im Webinterface von NWS prüfen. Der „Provisioning Status“ steht auf „PENDING_UPDATE“ während die Änderungen ausgeführt werden und wechselt zurück auf „ACTIVE“ sobald diese abgeschlossen wurden.

 

Fazit

In bestimmten Fällen kann es nützlich sein, ein benutzerdefiniertes Verbindungslimit an einem Load Balancer zu setzen und wie wir eben sehen konnten, ist die Umsetzung ziemlich einfach zu bewerkstelligen. Man sollte allerdings beachten, dass sehr hohe Limits in Kombination mit einer großen Anzahl an Listenern nach wie vor zu Problemen führen kann.

Automatische Fedora CoreOS Updates für Dein Kubernetes

Automatische Fedora CoreOS Updates für Dein Kubernetes

Du willst automatische Fedora CoresOS Updates für Dein Kubernetes? Und was haben Zincati und libostree damit zu tun? Hier bekommst Du schnell einen Überblick!

Als Betriebssystem vieler Kubernetes Cluster kommt Fedora CoreOS zum Einsatz. Dieses auf Container spezialisierte Betriebssystem punktet vor allem mit einfachen, automatischen Updates. Anders als gewohnt wird hier nicht Paket für Paket aktualisiert. Fedora CoreOS erstellt zuerst ein neues, aktualisiertes Image des Systems und finalisiert das Update mit einem Reboot. Für einen reibungslosen Ablauf sorgt rpm-ostree in Kombination mit Cincinnati und Zincati.

Bevor wir einen genaueren Blick auf die Komponenten werfen, klären wir zuerst wie Du automatische Updates für Dein NWS Kubernetes Cluster aktivieren kannst.

 

Wie aktivierst Du automatische Updates für Dein NWS Kubernetes Cluster?

 

Im NWS Portal kannst Du einfach zwischen verschiedenen Update-Mechanismen wählen. Klicke dazu auf “Update Fedora CoreOS” im Kontextmenü Deines Kubernetes Clusters und wähle zwischen immediate, periodic und lock-based.

Shows settings for automatic periodic updatesImmediate spielt Updates sofort auf all Deinen Kubernetes Knoten ein und finalisiert das Update mit einem Reboot.

Periodic aktualisiert Deine Nodes nur während eines frei wählbaren Wartungsfensters. Neben der Wochentage kannst Du den Startzeitpunkt und die Länge des Wartungsfensters festlegen.

Lock-based verwendet zur Koordinierung der Updates das FleetLock-Protokoll. Hier wird über einen Lockmanager das Finalisieren der Updates koordiniert. Damit wird sichergestellt, dass Nodes nicht gleichzeitig Updates finalisieren und rebooten. Zudem wird bei Problemen der Update Prozess gestoppt und weitere Nodes führen kein Update durch.

Disable deaktiviert automatische Updates.

So weit, so gut! Aber was ist jetzt rpm-ostree und Zincati?

 

 

Updates mal anders!

 

Durch die Einführung Container-basierter Anwendungen konnte auch eine Vereinheitlichung und Vereinfachung der darunter liegenden Betriebssysteme stattfinden. Zuverlässige, automatische Updates und die Steuerung dieser – durch den Betreiber der Anwendung – verringern zusätzlich den Aufwand für Wartung und Koordination.

 

rpm-ostree erstellt die Images

rpm-ostree ist ein Hybrid aus libostree und libdnf und somit eine Mischung aus Image- und Paketsystem. libostree bezeichnet sich selbst als git for operating system binaries, wobei jeder Commit einen bootfähigen Dateibaum enthält. Eine neue Version von Fedora CoreOS entspricht somit einem rpm-ostree Commit, gepflegt und bereitgestellt durch das CoreOS-Team. libdnf bietet die bekannten Features zur Paketverwaltung, wodurch die von libostree zur Verfügung gestellte Basis durch die Benutzer erweiterbar ist.

Taints und Tolerations Nodes, auf denen keine Container gestartet werden können bzw. nicht erreichbar sind, bekommen von Kubernetes einen sogenannten Taint (z.B. not-ready oder unreachable). Als Gegenstück werden Pods auf solchen Nodes mit einer Toleration versehen. Das passiert auch bei einem Fedora CoreOS Update. Pods werden bei einem Reboot automatisch mit tolerationSeconds=300 markiert, wodurch nach 5 Minuten Deine Pods auf anderen Nodes neu gestartet werden. Mehr zu Taints und Tolerations findest Du natürlich in der Kubernetes Dokumentation.

 

Cincinnati und Zincati verteilen die Updates

Zum Verteilen der rpm-ostree Commits kommt Cincinnati und Zincati zum Einsatz. Letzteres ist ein Client, der regelmäßig den Fedora CoreOS Cincinnati Server nach Updates fragt. Sobald ein passendes Update vorliegt, bereitet rpm-ostree einen neuen, bootfähigen Dateibaum vor. Je nach gewählter Strategie finalisiert Zincati das Update durch einen Reboot des Nodes.

 

 

Was sind die Vorteile?

 

Einfacher Rollback

Mit libostree ist es einfach, den alten Zustand wieder herzustellen. Hierfür muss man nur in den vorherigen rpm-ostree Commit booten. Dieser ist auch als Eintrag im Grub Bootloader-Menü zu finden.

Geringer Aufwand

Fedora CoreOS kann sich ohne manuelles Eingreifen aktualisieren. In Kombination mit Kubernetes werden auch die Anwendungen automatisch auf die aktuell verfügbaren Nodes verschoben.

Flexible Konfiguration

Zincati bietet eine einfache und flexible Konfiguration, mit der hoffentlich jeder Anwender eine passende Update Strategie findet.

Bessere Qualität

Durch den schlanken Image-basierten Ansatz kann jede Version als Ganzes leichter und genauer getestet werden.

 

Ob sich dieser Hybrid aus Image- und Paket-basiertem Betriebssystem durchsetzt, wird die Zukunft zeigen. Fedora CoreOS – als Basis für unser NMS Managed Kubernetes – vereinfacht den Update-Prozess erheblich und ermöglicht unseren Kunden trotzdem eine einfache Steuerung.

X-Forward-For und Proxy-Protocol

X-Forward-For und Proxy-Protocol

Du willst wissen wie Du in Deinem Kubernetes Cluster an die IP-Adressen Deiner Clients kommst? In fünf Minuten hast Du den Überblick!

Vom HTTP-Client zur Anwendung

Im Tutorial zum nginx-Ingress-Controller zeigen wir wie man eine Anwendung öffentlich erreichbar macht. Im Fall der NETWAYS Cloud bedient sich dein Kubernetes Cluster an einem Openstack-Loadbalancer, welcher die Client-Anfragen an einen nginx-Ingress-Controller im Kubernetes Cluster weitersendet. Dieser verteilt dann alle Anfragen an die entsprechenden Pods.

Bei all dem Herumschieben und Weiterleiten der Anfragen gehen ohne weitere Konfiguration die Verbindungsdetails der Clients verloren. Da das Problem nicht erst seit Kubernetes auftritt, wird auf die alt bewährten Lösungen X-Forward-For oder Proxy-Protocol zurückgegriffen.

Um im Buzzword-Bingo zwischen Service, Loadbalancer, Ingress, Proxy, Client und Anwendung nicht die Übersicht zu verlieren, kannst Du Dir im folgenden Beispiel den Weg einer HTTP-Anfrage vom Client bis zur Anwendung durch die Komponenten eines Kubernetes Clusters ansehen.

Der Weg vom HTTP-Request zur Anwendung im Kubernetes-Cluster

 

Client IP-Adressen mit X-Forward-For

Verwendest Du HTTP, kann die Client IP-Adresse im X-Forward-For (XFF) gespeichert und weiter transportiert werden. XFF ist ein Eintrag im HTTP-Header und wird von den meisten Proxy-Servern unterstützt. Im Beispiel setzt der Loadbalancer dazu die Client-IP-Adresse in den XFF-Eintrag und schickt die Anfrage weiter. Alle weiteren Proxy-Server und die Anwendungen können dadurch im XFF-Eintrag erkennen, von welcher Adresse die Anfrage ursprünglich gesendet wurde.

In Kubernetes konfiguriert man den Loadbalancer über Annotations im Service Objekt. Setzt man dort loadbalancer.openstack.org/x-forwarded-for: true wird der Loadbalancer entsprechend konfiguriert. Wichtig ist jetzt natürlich auch noch, dass der nächste Proxy den X-Forward-For Header nicht wieder überschreibt. Im Fall eines nginx kann man dazu die Option use-forwarded-headers in dessen ConfigMap setzen.

Service

---
kind: Service
apiVersion: v1
metadata:
  name: loadbalanced-service
  annotations:
    loadbalancer.openstack.org/x-forward-for: "true"
spec:
  selector:
    app: echoserver
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP

nginx ConfigMap

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx
data:
  use-forwarded-headers: "true"

Da der HTTP-Header verwendet wird, ist es nicht möglich HTTPS-Verbindungen mit der Client-IP-Adresse anzureichern. Hier muss man entweder das TLS/SSL-Protokoll am Loadbalancer terminieren oder auf das Proxy-Protocol zurückgreifen.

 

Client-Informationen mit Proxy-Protocol

Verwendet man X-Forwarded-For ist man offensichtlich auf HTTP beschränkt. Um auch HTTPS und anderen Anwendungen hinter Loadbalancern und Proxys den Zugriff auf die Verbindungsoption der Clients zu ermöglichen, wurde das sogenannte Proxy-Protocol erfunden. Technisch wird dazu vom Loadbalancer ein kleiner Header mit den Verbindungsinformationen des Clients hinzugefügt. Der nächste Hop (hier nginx) muss natürlich ebenfalls das Protokoll verstehen und entsprechend behandeln. Neben klassischen Proxys unterstützten auch andere Anwendungen wie MariaDB oder postfix das Proxy-Protocol.

Um das Proxy-Protocol zu aktivieren, musst Du das Service Objekt mit der Annotation loadbalancer.openstack.org/proxy-protocol versehen. Für den annehmenden Proxy muss das Protocol ebenfalls aktiviert werden.

Service Loadbalancer

---
kind: Service
apiVersion: v1
metadata:
  name: loadbalanced-service
  annotations:
    loadbalancer.openstack.org/proxy-protocol: "true"
spec:
  selector:
    app: echoserver
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP

Nginx ConfigMap

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx
data:
  use-proxy-protocol: "true"

In den meisten Fällen wirst Du aber auf das Helm-Chart des nginx-Ingress-Controller zurückgreifen. Dort ist eine entsprechende Konfiguration sogar noch einfacher.

 

nginx-Ingress-Controller und Helm

Benutzt man Helm, um den nginx-ingress-controller zu installieren, ist die Konfiguration sehr übersichtlich. Das Proxy-Protocol wird über die eine Helm-Values-Datei sowohl für den nginx als auch für den Loadbalancer aktiviert:

nginx-ingress.values:

---
controller:
  config:
    use-proxy-protocol: "true"
  service:
    annotations:
      loadbalancer.openstack.org/proxy-protocol: true
    type: LoadBalancer

 

$ helm install my-ingress stable/nginx-ingress -f nginx-ingress.values

Ob alles so funktioniert wie erwartet, kannst Du am einfachsten mit Hilfe des Google Echoserver testen. Dabei handelt sich um eine kleine Anwendung, die den HTTP-Request einfach an den Client zurück gibt. Wie im Tutorial zum nginx-Ingress-Controller beschrieben, benötigen wir dazu ein Deployment mit Service und Ingress. Ersteres startet den Echoserver, der Service macht diesen im Cluster erreichbar und der Ingress konfiguriert den nginx so, dass die Requests an das Deployment weitergeleitet werden.

 

Deployment

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echoserver
spec:
  selector:
    matchLabels:
      app: echoserver
  replicas: 1
  template:
    metadata:
      labels:
        app: echoserver
    spec:
      containers:
      - name: echoserver
        image: gcr.io/google-containers/echoserver:1.8
        ports:
          - containerPort: 8080
Service

---
apiVersion: v1
kind: Service
metadata:
  name: echoserver-svc
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: echoserver

Ingress

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: echoserver-ingress
spec:
  rules:
  - host: echoserver.nws.netways.de
    http:
      paths:
        - backend:
            serviceName: echoserver-svc
            servicePort: 80

Zum Testen fakest Du am besten noch deine /etc/hosts damit echoserver.nws.netways.de auf die öffentlichen IP-Adresse deines nginx-Ingress-Controller deutet. curl echoserver.nws.netways.de zeigt dir dann alles an, was der Echoserver von Deinem Client weiß, inklusive der IP-Adresse im X-Forward-For Header.

 

Fazit

Im Kubernetes Cluster ist das Proxy-Protocol für die meisten Anwendungsfälle wohl die bessere Wahl. Die bekannten Ingress-Controller unterstützen das Proxy-Protocol und TLS/SSL-Verbindungen können im K8s-Cluster konfiguriert und terminiert werden. Welche Informationen bei Deiner Anwendung ankommen, findest Du am schnellsten mit Googles Echoserver raus.

Kubernetes Alerting mit Prometheus Alertmanager

Kubernetes Alerting mit Prometheus Alertmanager

In einem vorangegangenen Post erläuterte Sebastian, wie man mit dem Prometheus Operator sein Kubernetes Cluster monitoren kann. Dieser Beitrag baut darauf auf und zeigt, wie man Benachrichtigungen per E-Mail und als Push Notifications mit dem Alertmanager einrichten kann.

Mit Helm den Monitoring Stack installieren

Neben der von Sebastian gezeigten Methode zum deployen des Prometheus Operators gibt es noch eine weitere Variante, mit der sich ein kompletter Prometheus-Stack im Kubernetes Cluster aufsetzen lässt. Und zwar mit Hilfe des Package Managers Helm.
Die Sourcen des Helm Charts für den Prometheus Stack kann man sich auf Github ansehen.
Dort findet man auch eine Datei die alle Konfigurationsoptionen und Standardwerte des Monitoring-Stacks enthält. Zu den Bestandteilen zählt neben dem Prometheus-Operator und Grafana auch der Prometheus Node Exporter, Kube State Metrics und der Alertmanager. Neu ist an dieser Stelle Kube State Metrics und der Alertmanager. Kube State Metrics bindet sich an die Kubernetes API und kann somit Metriken über sämtliche Ressourcen im Cluster abgrasen. Der Alertmanager kann anhand eines Regelwerks für ausgewählte Metriken alarmieren. Bei dem Setup per Helm kann jede Komponente über die Optionen in der Values Datei konfiguriert werden. Es macht Sinn für Prometheus und Alertmanager jeweils ein PVC zu verwenden um somit die Metriken und History von Alarmen, sowie die Alarmunterdrückungen persistent zu haben.

Mit folgenden Values wird in meinem Fall für beides ein Persistent Volume Claim (PVC) angelegt. Der Name der Storage-Class kann natürlich variieren.

alertmanager:
  alertmanagerSpec:
    storage:
      volumeClaimTemplate:
        spec:
          storageClassName: nws-storage
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 10Gi

prometheus:
  prometheusSpec:
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: nws-storage
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 10Gi

Ich speichere mir die Konfiguration im Verzeichnis „prom-config“ als Datei „stack.values“ ab. Nun kann man auch schon den Stack deployen. Zuerst muss man die passenden Repos einbinden:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add stable https://kubernetes-charts.storage.googleapis.com/
helm repo update

Anschließend wird der Stack erstellt:

helm install nws-prometheus-stack prometheus-community/kube-prometheus-stack -f prom-config/stack.values

Mittels Port-Forwarding kann man per Browser das Grafana Webinterface aufrufen und sich mit den Credentials admin/prom-operator anmelden.

kubectl port-forward service/nws-prometheus-stack-grafana 3002:80

Im Browser anschließend http://localhost:3002/ aufrufen.

Es sind bereits einige vordefinierte Dashboards mit dabei. Um für alle Dashboards (Scheduler, Etcd, etc.) auch Metriken angezeigt zu bekommen, sind allerdings weitere Optionen in den Values von Prometheus zu setzen. In diesem Post werde ich allerdings nicht darauf eingehen, wie sich diese konfigurieren lassen.
Stattdessen möchte ich wie Eingangs erwähnt das Einrichten von Alarmen demonstrieren. Hier kommt der Alertmanager ins Spiel. In Grafana sucht man jedoch vergebens danach. Ein Blick ins Prometheus Web UI macht da mehr Sinn.
Also erstmal wieder einen Port-Forward dafür einrichten:

kubectl port-forward service/nws-prometheus-stack-kube-prometheus 3003:9090

Dann im Browser http://localhost:3003/ aufrufen.

Unter Alerts sieht man sämtliche Alarmregeln, die bei dem Stack bereits vorkonfiguriert dabei sind. Per Klick auf eine beliebige Regel kann man deren Definition sehen. Konfigurieren lässt sich jedoch auch hier nichts.
Über die Buttons „Inactive“, „Pending“ und „Firing“ können die Regeln mit dem jeweiligen Status ein-, bzw. ausgeblendet werden.

Wir gehen zur nächsten Weboberfläche – Alertmanager. Auch hierfür benötigen wir einen Port-Forward.

kubectl port-forward service/nws-prometheus-stack-kube-alertmanager 3004:9093

Das Webinterface (unter http://localhost:3004/ erreichbar) ähnelt dem von Prometheus. Unter Alerts hat man hingegen eine History der bereits abgefeuerten Alarme. Silences zeigt eine Übersicht der unterdrückten Alerts und Status enthält neben Versionsinfos und dem Betriebsstatus auch die aktuelle Config des Alertmanagers. Leider lässt sich diese Config hier jedoch auch nicht ändern.

Wie konfigurieren wir nun also den Alertmanager?

Den Alertmanager konfigurieren

Manche werden es schon erahnt haben: indem man die Values des Helm-Charts anpasst. Wir schreiben also eine neue Values-Datei, die ausschließlich die Alertmanager-Config enthält. Die Doku zeigt uns welche Optionen es gibt und nach welchem Schema sich diese setzen lassen. Wir fangen mit einem grundlegenden Beispiel an und fügen erstmal globale SMTP-Settings hinzu:

alertmanager-v1.values

alertmanager:
  config:
    global:
      resolve_timeout: 5m
      smtp_from: k8s-alertmanager@example.com
      smtp_smarthost: mail.example.com:587
      smtp_require_tls: true
      smtp_auth_username: k8s-alertmanager@example.com
      smtp_auth_password: xxxxxxxxx
    route:
      receiver: 'k8s-admin'
      routes: []
    receivers:
    - name: 'k8s-admin'
      email_configs:
      - to: k8s-admin@example.com

Dadurch kann der Alertmanager dann schon mal E-Mails herausschicken. Zudem ist es natürlich auch notwendig mindestens einen Empfänger zu definieren. Unter „receivers“ kann man wie in meinem Beispiel einfach Namen und E-Mailadresse hinterlegen. Bitte darauf achten, dass die Texteinrückung stimmt, ansonsten kann es beim Ausrollen der Config zu Startproblemen des Alertmanagers kommen.

Damit nun die Zustellung der Alerts auch wirklich stattfindet muss man unter „route“ die Routen festlegen. Wenn man erstmal alle feuernden Alarme an einen Kontakt rausschicken möchte, dann kann man das wie in meinem Beispiel machen. Was man noch so alles mit Routen anstellen kann, das sehen wir uns in einem späteren Beispiel genauer an.

Mit einem Helm Upgrade können wir unsere neue Konfiguration für den Alertmanager deployen. Wir übernehmen dabei alle bereits gesetzten Werte mittels „–reuse-values“:

helm upgrade --reuse-values -f prom-config/alertmanager-v1.values nws-prometheus-stack prometheus-community/kube-prometheus-stack

Und wie testen wir das jetzt?

Alerts testen

Wer nicht gleich einen Node abschießen möchte kann einfach mal den Alertmanager neu starten. Beim Start feuert nämlich die Alert-Rule „Watchdog“. Diese ist nur dazu da, um die ordnungsgemäße Funktion der Alertingpipeline zu testen.
So kannst du den Neustart durchführen:

kubectl rollout restart statefulset.apps/alertmanager-nws-prometheus-stack-kube-alertmanager

Kurz nach dem Neustart sollte auch schon die E-Mail ankommen. Falls nicht, dann erst einmal prüfen ob der Alertmanager noch startet. Wenn er in einem Crash-Loop hängt, dann kann man über die Logs des Pods herausfinden, was hier schief läuft. Ist eine Konfig-Option falsch gesetzt, kannst du diese in den Values nochmal anpassen und erneut mit einem Helm Upgrade deployen. Wer sich bei einem Optionskey vertippt hat, kann per Helm Rollback auch auf einen vorherigen Stand zurück rollen:

helm rollback nws-prometheus-stack 1

Damit haben wir schon mal ein rudimentäres Monitoring für den Kubernetes Cluster selbst. Natürlich kann man noch weitere Receiver hinterlegen und somit auch mehrere Kontakte benachrichtigen.

Eigene Metriken und Alert Rules hinzufügen

Jetzt sehen wir uns noch an, für was die Routen nützlich sein können. Allerdings werden wir uns dazu erst noch ein paar Pods zum Testen hochfahren und verschiedene Namespaces anlegen. Zudem sehen wir uns auch noch kurz den Prometheus Blackbox Exporter an.

Szenario: In einem K8s-Cluster werden verschiedene Environments per Namespaces betrieben – z.B. development und production.Wenn die sample-app des Namespaces development ausfällt, dann soll nicht die Bereitschaft alarmiert werden.Die Bereitschaft ist nur für Ausfälle der sample-app des Production-Namespaces zuständig und bekommt neben E-Mails auch Benachrichtigungen aufs Mobiltelefon. Die Entwickler werden gesondert per Mail über Probleme bei der sample-app aus dem development Namespace informiert.

Wir definieren die beiden Namespaces „nws-production“ und „nws-development“ in namespaces-nws.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: nws-development
  labels:
    name: nws-development
---
apiVersion: v1
kind: Namespace
metadata:
  name: nws-production
  labels:
    name: nws-production
kubectl apply -f ./prom-config/namespaces-nws.yaml

Nun starten wir uns zwei sample-apps, die uns abwechselnd in einem 60 Sekunden Intervall HTTP 200 und HTTP 500 zurück liefern. Ich verwendet hier ein einfaches Image, das ich mir für diesen Zweck erstellt habe (Sourcen auf GitHub).

sample-app.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nws-sample-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nws-sample-app
  template:
    metadata:
      labels:
        app: nws-sample-app
    spec:
      containers:
      - name: nws-sample-app
        image: gagaha/alternating-http-response
        ports:
        - containerPort: 80
kubectl apply -f prom-config/sample-app.yaml -n nws-production
kubectl apply -f prom-config/sample-app.yaml -n nws-development

Anschließend exposen wir die App im Cluster:

kubectl expose deployment nws-sample-app -n nws-production
kubectl expose deployment nws-sample-app -n nws-development

Nun brauchen wir allerdings noch einen Komponente, die die Verfügbarkeit dieser Apps per HTTP Request abfragen kann und als Metriken für Prometheus bereitstellt. Es bietet sich hierfür der Prometheus Blackbox Exporter an. Damit kann man neben HTTP/HTTPS Anfragen auch Verbindungen mit den Protokollen DNS, TCP und ICMP prüfen.

Zuerst müssen wir den Blackbox Exporter im Cluster deployen. Ich bediene mich auch hier wieder dem offiziellen Helm Chart.

helm install nws-blackbox-exporter prometheus-community/prometheus-blackbox-exporter

Nun müssen wir Prometheus noch mitteilen, wie er an den Blackbox Exporter kommt. Interessant sind hier die Targets. Hier tragen wir die HTTP Endpoints ein, die vom Blackbox Exporter abgefragt werden sollen. Wir schreiben die zusätzliche Konfiguration in eine Datei und deployen sie per Helm Upgrade:
prom-blackbox-scrape.yaml

prometheus:
  prometheusSpec:
    additionalScrapeConfigs:
     - job_name: 'nws-blackbox-exporter'
       metrics_path: /probe
       params:
         module: [http_2xx]
       static_configs:
        - targets:
           - http://nws-sample-app.nws-production.svc
           - http://nws-sample-app.nws-development.svc
       relabel_configs:
        - source_labels: [__address__]
          target_label: __param_target
        - source_labels: [__param_target]
          target_label: instance
        - target_label: __address__
          replacement: nws-blackbox-exporter-prometheus-blackbox-exporter:9115
helm upgrade --reuse-values -f prom-config/prom-blackbox-scrape.yaml nws-prometheus-stack prometheus-community/kube-prometheus-stack

Wenn wir dann nochmal den Port-Forward für prometheus starten, können wir unter http://localhost:3003/targets die beiden neuen Targets des nws-blackbox-exporters sehen. Somit kommen schon mal Metriken zu Prometheus rein. Wir müssen allerdings auch neue Alert Rules definieren, damit für diese Metriken alarmiert werden kann.

Wir editieren die Rules direkt per Kubectl:

kubectl edit prometheusrules nws-prometheus-stack-kube-k8s.rules

Vor den k8s.rules fügen wir unsere neue Regel hinzu:

...
spec:
  groups:
  - name: blackbox-exporter
    rules:
    - alert: HttpStatusCode
      annotations:
        description: |-
          HTTP status code is not 200-399
            VALUE = {{ $value }}
            LABELS: {{ $labels }}
        summary: HTTP Status Code (instance {{ $labels.instance }})
      expr: probe_http_status_code &amp;amp;lt;= 199 OR probe_http_status_code &amp;amp;gt;= 400
      for: 30s
      labels:
        severity: error
  - name: k8s.rules
    rules:
...

Jetzt müssen wir nur noch die Kontaktdaten der verschiedenen Empfänger und Routen definieren.

Unter route kann man in routes verschiedene Empfänger angeben. Diese Empfänger müssen natürlich weiter unten auch vorhanden sein. Es kann dann für eine Route auch Bedingungen definiert werden. Nur wenn die Bedingungen auch zutreffen, wird die Benachrichtigung an den angegebenen Empfänger verschickt.

Hier nun die Config für das Szenario:
alertmanager-v2.values

alertmanager:
  config:
    global:
      resolve_timeout: 5m
      smtp_from: k8s-alertmanager@example.com
      smtp_smarthost: mail.example.com:587
      smtp_require_tls: true
      smtp_auth_username: k8s-alertmanager@example.com
      smtp_auth_password: xxxxxxxxx
    route:
      receiver: 'k8s-admin'
      repeat_interval: 5m
      routes:
      - receiver: 'dev_mail'
        match:
          instance: http://nws-sample-app.nws-development.svc
      - receiver: 'bereitschaft'
        match:
          instance: http://nws-sample-app.nws-production.svc
    receivers:
    - name: 'k8s-admin'
      email_configs:
      - to: k8s-admin@example.com
    - name: 'dev_mail'
      email_configs:
      - to: devs@example.com
    - name: 'bereitschaft'
      email_configs:
      - to: bereitschaft@example.com
      pushover_configs:
      - user_key: xxxxxxxxxxxxxxxxxxxxxxxxxxx
        token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
helm upgrade --reuse-values -f prom-config/alertmanager-v2.values nws-prometheus-stack prometheus-community/kube-prometheus-stack

Am besten man löst danach wieder einen Neustart des Alertmanagers aus:

kubectl rollout restart statefulset.apps/alertmanager-nws-prometheus-stack-kube-alertmanager

Sofern die Konfiguration passt, sollten dann schon bald Alerts ankommen.

Fazit

Das Einrichten des Alertmanagers kann aufwändig sein. Man hat jedoch auch viele Konfigurationsmöglichkeiten und kann sich seine Benachrichtigungen über Regeln so zurechtlegen wie man es benötigt. Wer möchte kann auch noch die Templates der Nachrichten bearbeiten und somit das Format und die enthalten Informationen anpassen.

Logging mit Loki und Grafana in Kubernetes

Logging mit Loki und Grafana in Kubernetes

Die wichtigsten Bausteine zum Starten deiner Anwendung kennst du bereits aus unserer Tutorial-Serie. Für den Betrieb fehlen dir noch Metriken und Logs deiner Anwendungen? Nach diesem Blogpost kannst du letzteres abhaken.

Logging mit Loki und Grafana in Kubernetes – ein Überblick

Zum Sammeln und Verwalten deiner Logs bieten sich auch für Kubernetes eine der wohl bekanntesten, schwergewichtigen Lösungen an. Diese bestehen in der Regel aus Logstash oder Fluentd zum Sammeln, gepaart mit Elasticsearch zum Speichern und Kibana bzw. Graylog zur Visualisierung deiner Logs.

Neben dieser klassischen Kombination gibt es seit wenigen Jahren mit Loki und Grafana einen neuen, leichtgewichtigeren Stack! Die grundlegende Architektur unterscheidet sich kaum zu den bekannten Setups.

Promtail sammelt auf jedem Kubernetes Node die Logs aller Container und sendet diese an eine zentrale Loki-Instanz. Diese aggregiert alle Logs und schreibt diese in ein Storage-Backend. Zur Visualisierung wird Grafana verwendet, welches sich die Logs direkt von der Loki-Instanz holt.

worker node & master node

Der größte Unterschied zu den bekannten Stacks liegt wohl im Verzicht auf Elasticsearch. Dies spart Ressourcen und Aufwand, da somit auch kein dreifach replizierter Volltext-Index gespeichert und administriert werden muss. Und gerade wenn man beginnt, seine Anwendung aufzubauen, hört sich ein schlanker und einfacher Stack vielsprechend an. Wächst die Anwendungslandschaft, werden einzelne Komponenten von Loki in die Breite skaliert, um die Last auf mehrere Schultern zu verteilen.

Kein Volltextindex? Wie funktioniert das?

Zur schnellen Suche verzichtet Loki natürlich nicht auf einen Index, aber es werden nur noch Metadaten (ähnlich wie bei Prometheus) indexiert. Der Aufwand, den Index zu betreiben, wird dadurch sehr verkleinert. Für dein Kubernetes-Cluster werden somit hauptsächlich Labels im Index gespeichert und deine Logs werden automatisch anhand derselben Metadaten organisiert wie auch deine Anwendungen in deinem Kubernetes-Cluster. Anhand eines Zeitfensters und den Labels findet Loki schnell und einfach deine gesuchten Logs.

Zum Speichern des Index kann aus verschiedenen Datenbanken gewählt werden. Neben den beiden Cloud-Datenbanken BigTable und DynamoDB kann Loki seinen Index auch lokal in Cassandra oder BoltDB ablegen. Letztere unterstützt keine Replikation und ist vor allem für Entwicklungsumgebungen geeignet. Loki bietet mit boltdb-shipper eine weitere Datenbank an, welche aktuell noch in Entwicklung ist. Diese soll vor allem Abhängigkeiten zu einer replizierten Datenbank entfernen und regelmäßig Snapshots des Index im Chunk-Storage speichern (siehe unten).

Ein kleines Beispiel

Ein pod produziert mit stdout und stderr zwei Log-Streams. Diese Log-Streams werden in sogenannte Chunks zerlegt und komprimiert, sobald eine gewisse Größe erreicht wurde oder ein Zeitfenster abgelaufen ist.

Ein Chunk enthält somit komprimierte Logs eines Streams und wird auf eine maximale Größe und Zeiteinheit beschränkt. Diese komprimierten Datensätze werden dann im Chunk-Storage gespeichert.

Label vs. Stream

Eine Kombination aus exakt gleichen Labels (inkl. deren Werte) definiert einen Stream. Ändert man ein Label oder dessen Wert, entsteht ein neuer Stream. Die Logs aus stdout eines nginx-Pods befinden sich z.B. in einem Stream mit den Labels: pod-template-hash=bcf574bc8, app=nginx und stream=stdout.

Im Index von Loki werden diese Chunks mit den Labels des Streams und einem Zeitfenster verknüpft. Bei einer Suche muss im Index somit nur nach Labels und Zeitfenster gefiltert werden. Entspricht eine dieser Verknüpfungen den Suchkriterien, wird der Chunk aus dem Storage geladen und enthaltene Logs werden entsprechend der Suchanfrage gefiltert.

Chunk Storage

Im Chunk Storage werden die komprimierten und zerstückelten Log-Streams gespeichert. Wie beim Index kann auch hier zwischen verschiedenen Storage-Backends gewählt werden. Aufgrund der Größe der Chunks wird ein Object Store wie GCS, S3, Swift oder unser Ceph Object Store empfohlen. Die Replikation ist damit automatisch mit inbegriffen und die Chunks werden anhand eines Ablaufdatums auch selbständig vom Storage entfernt. In kleineren Projekten oder Entwicklungsumgebungen kann man aber natürlich auch mit einem lokalen Dateisystem beginnen.

Visualisierung mit Grafana

Zur Darstellung wird Grafana verwendet. Vorkonfigurierte Dashboards können leicht importiert werden. Als Query Language wird LogQL verwendet. Diese Eigenkreation von Grafana Labs lehnt sich stark an PromQL von Prometheus an und ist genauso schnell gelernt. Eine Abfrage besteht hierbei aus zwei Teilen:
Zuerst filtert man mithilfe von Labels und dem Log Stream Selector nach den entsprechenden Chunks. Mit = macht man hier immer einen exakten Vergleich und =~ erlaubt die Verwendungen von Regex. Wie üblich wird mit ! die Selektion negiert.
Nachdem man seine Suche auf bestimmte Chunks eingeschränkt hat, kann man diese mit einer Search Expression erweitern. Auch hier kann man mit verschiedenen Operatoren wie |= und |~ das Ergebnis weiter einschränken. Ein paar Beispiele zeigen wohl am schnellsten die Möglichkeiten:

Log Stream Selector:

{app = "nginx"}
{app != "nginx"}
{app =~ "ngin.*"}
{app !~ "nginx$"}
{app = "nginx", stream != "stdout"}
Search Expression:


{app = "nginx"} |= "192.168.0.1"
{app = "nginx"} != "192.168.0.1"
{app = "nginx"} |~ "192.*" 
{app = "nginx"} !~ "192$"

Weitere Möglichkeiten wie Aggregationen werden ausführlich in der offiziellen Dokumentation von LogQL erklärt.

Nach dieser kurzen Einführung in die Architektur und Funktionsweise von Grafana Loki legen wir natürlich gleich mit der Installation los. Viele weitere Informationen und Möglichkeiten zu Grafana Loki gibt es natürlich in der offiziellen Dokumentation.

Get it running!

Du willst Loki einfach ausprobieren?

Mit dem NWS Managed Kubernetes Cluster kannst du auf die Details verzichten! Mit einem Klick startest du deinen Loki Stack und hast dein Kubernetes Cluster immer voll im Blick!

 

Wie üblich mit Kubernetes ist ein laufendes Beispiel schneller deployed als die Erklärung gelesen. Mithilfe von Helm und einigen wenigen Variablen ist dein schlanker Logging Stack schnell installiert. Zuerst initialisieren wir zwei Helm-Repositories. Neben Grafana fügen wir auch noch das offizielle Helm stable Charts-Repository hinzu. Nach zwei kurzen helm repo add Befehlen haben wir Zugang zu den benötigten Loki und Grafana Charts.

Helm installieren

$ brew install helm
$ apt install helm
$ choco install kubernetes-helm

Dir fehlen die passenden Quellen? Auf helm.sh findest du eine kurze Anleitung für dein Betriebssystem.

 

$ helm repo add loki https://grafana.github.io/loki/charts
$ helm repo add stable https://kubernetes-charts.storage.googleapis.com/

 

Loki und Grafana installieren

Für deinen ersten Loki-Stack benötigst du keine weitere Konfiguration. Die Default-Werte passen sehr gut und helm install erledigt den Rest. Vor der Installation von Grafana setzen wir zuerst noch dessen Konfiguration mithilfe der bekannten Helm-Values-Dateien. Speichere diese mit dem Namen grafana.values ab.

Neben dem Passwort für den Administrator wird auch das eben installierte Loki als Datenquelle gesetzt. Zur Visualisierung importieren wir gleich noch ein Dashboard und die dafür benötigten Plugins. Somit installiert du gleich ein für Loki konfiguriertes Grafana und kannst direkt nach dem Deploy loslegen.

grafana.values:

---
adminPassword: supersecret

datasources:
  datasources.yaml:
    apiVersion: 1
    datasources:
    - name: Loki
      type: loki
      url: http://loki-headless:3100
      jsonData:
        maxLines: 1000

plugins:
  - grafana-piechart-panel

dashboardProviders:
  dashboardproviders.yaml:
    apiVersion: 1
    providers:
      - name: default
        orgId: 1
        folder:
        type: file
        disableDeletion: true
        editable: false
        options:
          path: /var/lib/grafana/dashboards/default

dashboards:
  default:
    Logging:
      gnetId: 12611
      revison: 1
      datasource: Loki

 

Die eigentliche Installation erfolgt mithilfe von helm install. Der erste Parameter ist ein frei wählbarer Name. Mit dessen Hilfe kannst du dir auch schnell einen Überblick verschaffen:

$ helm install loki loki/loki-stack
$ helm install loki-grafana stable/grafana -f grafana.values
$ kubectl get all -n kube-system -l release=loki

 

Nach dem Deployment kannst du dich als admin mit dem Passwort supersecret einloggen. Damit du direkt auf das Grafana Webinterface zugreifen kannst, fehlt dir noch noch ein port-forward:

$ kubectl --namespace kube-system port-forward service/loki-grafana 3001:80

Die Logs deiner laufenden Pods sollten sofort im Grafana sichtbar sein. Probier doch die Queries unter Explore aus und erkunde das Dashboard!

 

Logging mit Loki und Grafana in Kubernetes – das Fazit

Grafana Labs bietet mit Loki einen neuen Ansatz für ein zentrales Logmanagement. Durch die Verwendung von kostengünstigen und leicht verfügbaren Object Stores wird eine aufwändige Administration eines Elasticsearch-Clusters überflüssig. Das einfache und schnelle Deployment ist auch ideal für Entwicklungsumgebungen geeignet. Zwar bieten die beiden Alternativen Kibana und Graylog ein mächtiges Featureset, aber für manche Administratoren ist Loki mit seinem schlanken und einfachen Stack vielleicht verlockender.

Kubernetes Nodegroups verwalten

Kubernetes Nodegroups verwalten

Seit dieser Woche können unsere Kunden das „Nodegroup-Feature“ für ihre NWS Managed Kubernetes Cluster nutzen. Was sind Nodegroups und was kann ich damit bewerkstelligen? Das und mehr erklärt unser siebter Blogpost der Serie.

Was sind Nodegroups?

Mit Nodegroups ist es möglich, mehrere Kubernetes-Node-Gruppen zu erstellen und unabhängig voneinander zu verwalten. Eine Nodegroup beschreibt eine Anzahl von virtuellen Maschinen, die als Gruppe diverse Attribute besitzt. Im Wesentlichen wird bestimmt, welches Flavor – also welches VM-Modell – innerhalb dieser Gruppe verwendet werden soll. Es sind aber auch andere Attribute wählbar. Jede Nodegroup kann unabhängig von den anderen jederzeit vertikal skaliert werden.

Wieso Nodegroups?

Nodegroups eignen sich, um Pods gezielt auf bestimmten Nodes auszuführen. Es ist zum Beispiel möglich, eine Gruppe mit dem „Availability-Zone“-Attribut zu definieren. Neben der bereits bestehenden „default-nodegroup“, die die Nodes relativ willkürlich über alle Verfügbarkeitszonen verteilt, können weitere Nodegroups erstellt werden, die jeweils nur in einer Verfügbarkeitszone explizit gestartet werden. Innerhalb des Kubernetes Clusters kann man seine Pods in die entsprechenden Verfügbarkeitszonen bzw. Node-Gruppen aufteilen.
Ein weiteres mögliches Szenario ist es, seine Nodegroups nach schnellen und langsameren VMs einzuteilen. Services und Anwendungen, die keine besonderen Ansprüche erheben, können der langsameren und somit meist auch günstigeren Nodegroup zugewiesen werden, während Pods mit mehr Anspruch ausschließlich auf schnellen Nodes betrieben werden. Der eigenen Fantasie und den eigenen Voraussetzungen wird durch die gewonnene Flexibilität Genüge getan. Neue Nodegroups lassen sich bequem über das NWS Web-Interface anlegen:

Existierende Nodegroups anzeigen


Im ersten Bild sieht man unseren exemplarischen Kubernetes Cluster „k8s-ses“. Dieser verfügt aktuell über zwei Nodegroups: „default-master“ und „default-worker“.

 

Neue Nodegroup erstellen

Eine neue Nodegroup lässt sich über den Dialog „Create Nodegroup“ mit folgenden Optionen erstellen:

  • Name: Name der Nodegroup, der später als Label für K8s genutzt werden kann
  • Flavor: Größe der eingesetzten virtuellen Maschinen
  • Node Count: Anzahl der initialen Nodes, kann später jederzeit vergrößert und verkleinert werden.
  • Availability Zone: eine spezifische Verfügbarkeitszone
  • Minimum Node Count: Die Nodegroup darf nicht weniger Nodes als den definierten Wert beinhalten.
  • Maximium Node Count: Die Nodegroup kann auf nicht mehr als die angegebene Anzahl Nodes anwachsen.

Die zwei letzten Optionen sind insbesondere für AutoScaling ausschlaggebend und begrenzen somit den automatischen Mechanismus.


Anschließend sieht man die neue Nodegroup in der Übersicht. Die Provisionierung der Nodes dauert nur wenige Minuten. Jede Gruppe lässt sich zudem individuell in ihrer Anzahl jederzeit verändern oder auch wieder entfernen.

 

Nodegroups im Kubernetes Cluster verwenden

Innerhalb des Kubernetes Clusters kann man seine neuen Nodes, nachdem sie fertig provisioniert wurden und einsatzbereit sind, sehen.

 
kubectl get nodes -L magnum.openstack.org/role
NAME                                 STATUS   ROLES    AGE   VERSION   ROLE
k8s-ses-6osreqalftvz-master-0        Ready    master   23h   v1.18.2   master
k8s-ses-6osreqalftvz-node-0          Ready    <none>   23h   v1.18.2   worker
k8s-ses-6osreqalftvz-node-1          Ready    <none>   23h   v1.18.2   worker
k8s-ses-zone-a-vrzkdalqjcud-node-0   Ready    <none>   31s   v1.18.2   zone-a
k8s-ses-zone-a-vrzkdalqjcud-node-1   Ready    <none>   31s   v1.18.2   zone-a
k8s-ses-zone-a-vrzkdalqjcud-node-2   Ready    <none>   31s   v1.18.2   zone-a
k8s-ses-zone-a-vrzkdalqjcud-node-3   Ready    <none>   31s   v1.18.2   zone-a
k8s-ses-zone-a-vrzkdalqjcud-node-4   Ready    <none>   31s   v1.18.2   zone-a

Die Node-Labels magnum.openstack.org/nodegroup und magnum.openstack.org/role tragen bei Nodes, die der Gruppe zugehörig sind, den Namen der Nodegroup. Zudem gibt es noch das Label topology.kubernetes.io/zone, das den Namen der Availability Zone trägt.

Mit Hilfe des nodeSelectors können Deployments oder Pods den Nodes bzw. Gruppen zugeordnet werden:

 
nodeSelector:
  magnum.openstack.org/role: zone-a

 

Du möchtest Dich selbst davon überzeugen, dass Managed Kubernetes bei NWS so einfach ist? Dann probiere es gleich aus auf: https://nws.netways.de/de/kubernetes/