Image

Kubernetes mit Terraform – Funktionsfähigkeit erweitern

23. Januar 2023

Share on:

Im letzten Kapitel haben wir ein Terraform Projekt deployed und einen funktionsfähigen, kleinen Cluster erzeugt. Nun möchten wir die Funktionsfähigkeit unseres Clusters erweitern, indem wir den Zugriff von Außen ermöglichen, externe Ressourcen des Cloudproviders nutzen und Infrastrukturen schaffen, um ein Monitoring des Clusters zu ermöglichen.

Im Allgemeinen möchte man manche der Services, die im Kubernetes Cluster laufen, von außen zugänglich machen. In der einfachsten Form ist dies über einen Service direkt möglich. Services arbeiten auf Layer 4 des OSI Modells, was bedeutet, dass TCP- und UDP Pakete direkt weitergeleitet werden. Ein Großteil der Services, die wir möglicherweise anbieten wollen, benutzen heutzutage aber das HTTP-Protokoll. Daher empfiehlt sich die Verwendung eines Loadbalancer, der mehr Möglichkeiten der Einflussnahme bietet. Hier kommen Ingress-Controller ins Spiel.

Für unser Beispiel konfigurieren wir einen nginx-Ingress-Controller.

Was es zu Beachten gibt

  • Um das Beispiel einfach zu halten, verwendet das im Github-Repo enthaltene Beispiel keinen HTTPS Endpoint und kein TLS. Jedweder Verkehr in und aus dem Cluster ist daher unverschlüsselt!

 

Mit terraform plan -out=k8s_svc.plan kann wieder ein terraform Plan erstellt werden. Anschließend kann der Plan mit terraform apply k8s_svc.plan ausgeführt werden und der Ingress Controller sollte nach einigen Augenblicken vorhanden sein.

Anbindung von externen Cloud-Ressourcen

An dieser Stelle lohnt es sich, kurz auf die Mechanismen einzugehen, die es ermöglichen, die vom Hoster zur Verfügung gestellte Infrastruktur im Cluster nutzbar zu machen.
Um Zugriff auf externe Ressourcen zu erhalten, laufen auf unserem Cluster zwei Dienste, die hier nochmal kurz vorgestellt werden:

Cloud Controller Manager

Für unseren Ingress Controller notwendig ist der Cloud Controller Manager, welcher Loadbalancer automatisch erzeugt, wenn diese von einem Service benötigt werden.
Auch in unserem Fall wird über eine passende Konfiguration des nginx-Ingress-Controller Service automatisch ein passender Loadbalancer erzeugt.
Dazu sind im Manifest des Ingress Controller Services die passenden Annotations enthalten:

apiVersion: v1
kind: Service
metadata:
[..]
annotations:
   load-balancer.hetzner.cloud/health-check-http-path: /healthz
   load-balancer.hetzner.cloud/health-check-protocol: http
   load-balancer.hetzner.cloud/name: kubelb
   load-balancer.hetzner.cloud/protocol: tcp
   load-balancer.hetzner.cloud/uses-proxyprotocol: 'true'
spec:
[..]

Der Cloud Controller Manager wertet diese Annotations aus und fordert dann vom Cloud Provider die notwendigen Ressourcen an.

In der Managementoberfläche des Cloud Providers kann man dann Informationen über den Loadbalancer einsehen:

Managementoberfläche des Cloud Providers

Diese Übersicht enthält alle wichtigen Informationen über den Loadbalancer. Für uns am Interessantesten ist die IPv4 Adresse, unter der der Loadbalancer erreichbar ist. Diese benötigen wir noch im Fortgang.

Container Storage Interface Driver

Der hcloud Container Storage Interface Driver erlaubt es, Persistent Volume Claims, also dauerhaft verfügbare Datenspeicher unserer Container auf dem vom Hoster zur Verfügung gestellten Cloudspeicher, abzubilden. Dies geschieht ganz automatisch beim Anlegen eines Pods. Eine Definition eines solchen Volumes, z. B. durch den zuvor instanzierten Prometheus, kann so aussehen:

volumeClaimTemplates:
- kind: PersistentVolumeClaim
  apiVersion: v1
  metadata:
    name: data
    creationTimestamp: null
  spec:
    accessModes:
    - ReadWriteOnce
    resources:
      requests:
        storage: 20G
    volumeMode: Filesystem
  status:
    phase: Pending
serviceName: prometheus

Auf Basis dieses Templates erstellt der CSI-Driver dann automatisch ein Cloud-Storage Volume:

Cloud-Storage Volume

Automatisch generiertes Cloud-Storage Volume.

 

Anlegen einer Beispielanwendung im Kubernetes

Wie kann dieser Loadbalancer Ingress Controller nun getestet werden? Dafür deployen wir zunächst einen minimalen Webserver, der seinerseits den Ingress-Controller nutzt. Hierfür kann folgendes Listing verwendet werden. Es besteht aus lediglich drei Entitäten:

  1. einem Deployment mit einem nginx Webserver
  2. einen Service vom Typ NodePort sowie
  3. einen Ingress, welcher unter anderem einen Hostname enthält, der vom Ingress-Controller zur Weiterleitung der Requests verwendet wird
apiVersion: apps/v1
kind: Deployment
metadata:
   name: nginx
   namespace: nginx-basic
   labels:
      app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
       app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
        image: nginx
     ports:
        - containerPort: 80
---

apiVersion: v1
kind: Service
metadata:
   name: nginx-service
   namespace: nginx-basic
spec:
   selector:
     app: nginx
   type: NodePort
   ports:
   - protocol: TCP
     port: 80
     targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
   name: nginx-ingress
   namespace: nginx-basic
spec:
   rules:
     - host: nginx.kube.home
     http:
     paths:
     - path: /
       pathType: Prefix
       backend:
         service:
           name: nginx-service
           port:
           number: 80

Als erstes speichern wir das Listing in einer Datei ab. Danach erstellen wir mittels kubectl einen Namespace, in welchen wir die Komponenten deployen. Hierbei legen wir zunächst den Namespace an und applizieren die zuvor gespeicherte Datei:

$ kubectl create ns nginx-basic
namespace/nginx-basic created
$ kubectl apply -f nginxbasic.yml
deployment.apps/nginx created
service/nginx-service created
ingress.networking.k8s.io/nginx-ingress created
$

Jetzt können wir testen, ob das Setup bis hierhin fehlerfrei war, indem wir per curl auf die IPv4 Adresse des Loadbalancers zugreifen. Der Ingress-Controller verwendet den HOST-HEADER, um zwischen verschiedenen Services zu unterscheiden. Da wir keinen DNS Eintrag für den Loadbalancer erstellt haben, müssen wir den Header manuell mitschicken.

$ curl -H "Host: nginx.kube.home" http://167.233.10.11 
<!DOCTYPE html> 
<html> 
<head> 
<title>Welcome to nginx!</title> 
<style> 
html { color-scheme: light dark; } 
body { width: 35em; margin: 0 auto; 
font-family: Tahoma, Verdana, Arial, sans-serif; } 
</style> 
</head> 
<body> 
<h1>Welcome to nginx!</h1> 
<p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> 
<p>For online documentation and support please refer to 
<a href="http://nginx.org/">nginx.org</a>.<br/> 
Commercial support is available at 
<a href="http://nginx.com/">nginx.com</a>.</p> 
<p><em>Thank you for using nginx.</em></p> 
</body> 
</html> 
$

Der nginx-Ingress-Controller bietet eine Vielzahl von Konfigurationsmöglichkeiten, auf die hier nicht in den Einzelheiten eingegangen werden kann, es lohnt sich aber, sich damit zu beschäftigen(z. B. hier: https://www.omerlh.info/2021/10/22/nginx-ingress-the-security-hero-we-need/). Abschließend können wir den Demo-Webserver mit dem Befehl

kubectl delete namespace nginx-basic

wieder löschen.

Prometheus

Bisher besitzen wir für den Kubernetes Cluster noch kein Monitoring. Dies wollen wir nun ändern, um ein umfassenderes Bild über den Zustand des Clusters zu erhalten. Hierfür befindet sich im Service Repo ein Modul für Prometheus, welches die Parameter des Clusters umfangreich überwacht und die Metriken aus einzelnen Containern exportieren kann. Dies kann problemlos und mit minimalem Konfigurationsaufwand erfolgen.

Um Prometheus zu installieren entfernen wir die Kommentare in der main.tf Datei im Hauptverzeichnis des Repos:

module "prometheus" {
source = "./modules/prometheus"
kubeconfig_path = var.kubeconfig_path
cluster_name = var.cluster_name
}

Danach kann Prometheus einfach über

terraform plan -out=k8s_svc.plan

mit anschließendem

terraform apply k8s_svc.plan

installiert werden.

Das fügt unserem Cluster eine ganze Reihe von Komponenten hinzu:

  • Node-Exporter: Node-Exporter sammelt Metriken vom Clusternode ein und stellt diese Prometheus zur Verfügung. Dazu bindet der Node-Exporter das Dateisystem des Nodes in den Container ein, um Zugriffe auf dessen Dateisystem, insbesondere /sys und /proc, zu erhalten.
  • Kube-State-Metrics: Kube-State-Metrics greift auf die APIs des Kubernetes Clusters zu, um dessen Betriebszustand zu erfassen.
  • Prometheus: Prometheus sammelt die Daten von Node-Exporter und Kube-State-Metrics ein und speichert sie in einer Time-Series Datenbank. Darüber hinaus ermöglicht es Prometheus, über eine Browser-Schnittstelle auf diese Daten zuzugreifen.

Zugriff auf Prometheus

Prometheus verfügt über einen Ingress, da wir den Service nicht außerhalb des Clusters verfügbar machen wollen. Um auf Prometheus zuzugreifen, verwenden wir deshalb kubectl port-forward. Dazu müssen wir zuerst über die Kommandozeile den Namen des Prometheus Pods herausfinden:

$ kubectl get pods --namespace prometheus-metrics
NAME READY STATUS RESTARTS AGE kube-state-metrics-7df95cd467-gnfzh 1/1 Running 0 21h node-exporter-5zv2l 1/1 Running 0 21h node-exporter-7nfkk 1/1 Running 0 21h node-exporter-9t4br 1/1 Running 0 21h prometheus-0 1/1 Running 0 21h
$

Prometheus stellt seine Oberfläche normalerweise auf Port 9090 zur Verfügung. Wir leiten also diesen Port auf die lokale Maschine weiter:

$ kubectl port-forward prometheus-0 9090:9090 --namespace=prometheus-metrics Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090

Anschließend steht Prometheus auf http://localhost:9090 zur Verfügung:

Monitoring von Diensten durch Prometheus

Prometheus verwendet eine Reihe von benutzerdefinierten Regeln, um Zugriff auf die Metriken zu erhalten, die es einlesen soll. Im Folgenden wollen wir uns diese Regel ansehen.
Zunächst soll die Regel in Gänze dargestellt werden, bevor wir anschließend auf die relevanten Parameter eingehen:

- job_name: kubernetes-pods
  honor_timestamps: true
  scrape_interval: 15s
  scrape_timeout: 10s
  metrics_path: /metrics
  scheme: http
  follow_redirects: true
  enable_http2: true
  relabel_configs:
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] separator: ;
    regex: "true"
    replacement: $1
    action: keep
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] separator: ;
    regex: (.+)
    target_label: __metrics_path__
    replacement: $1
    action: replace
  - source_labels: [__address__,
  __meta_kubernetes_pod_annotation_prometheus_io_port]
    separator: ;
    regex: ([^:]+)(?::\d+)?;(\d+)
    target_label: __address__
    replacement: $1:$2
    action: replace
  - separator: ;
    regex: __meta_kubernetes_pod_label_(.+)
    replacement: $1
    action: labelmap
  - source_labels: [__meta_kubernetes_namespace]
    separator: ;
    regex: (.*)
    target_label: kubernetes_namespace
    replacement: $1
    action: replace
  - source_labels: [__meta_kubernetes_pod_name]
    separator: ;
    regex: (.*)
    target_label: kubernetes_pod_name
    replacement: $1
    action: replace
    metric_relabel_configs:
  - source_labels: [namespace]
    separator: ;
    regex: (.+)
    target_label: kubernetes_namespace
    replacement: $1
    action: replace
    kubernetes_sd_configs:
  - role: pod
    kubeconfig_file: ""
    follow_redirects: true
    enable_http2: true
      namespaces:
       own_namespace: false
      names:
       - prometheus-metrics
       - ingress-nginx

Wichtig für die Service Discovery ist der letzte Teil der Konfiguration unter kubernetes_sd_configs. Hiermit wird definiert, welches Kubernetes Objekt beobachtet werden soll (Kubernetes Pods) und welche Namespaces überwacht werden sollen (prometheus metrics und ingress-nginx). Dies reicht allerdings noch nicht aus, da auch im zu überwachenden Pod noch Konfigurationen vorgenommen werden müssen. Eine nähere Betrachtung des Ingress-Controllers verdeutlicht das:

$ kubectl get pods --namespace=ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-d4hvn 0/1 Completed 0 25h
ingress-nginx-admission-patch-lnnbw 0/1 Completed 1 25h
ingress-nginx-controller-77759fbd45-mqgmt 1/1 Running 0 21h

$ kubectl describe pod ingress-nginx-controller-77759fbd45-mqgmt --namespace=ingress-nginx

Name:         ingress-nginx-controller-77759fbd45-mqgmt
Namespace:    ingress-nginx
Priority:     0
Node:         worker-2/10.98.0.2
Start Time:   Mon, 30 May 2022 14:02:44 +0200
Labels:       app.kubernetes.io/component=controller
              app.kubernetes.io/instance=ingress-nginx
              app.kubernetes.io/name=ingress-nginx
              pod-template-hash=77759fbd45
Annotations:  prometheus.io/port: 10254
              prometheus.io/scheme: http
              prometheus.io/scrape: true
[...]

Durch die drei Annotationen weiß Prometheus, wo und ob im Pod Metriken zu finden sind.

Folgendes Beispiel zeigt, wie einfach Prometheus eingesetzt werden kann, um Metriken eines Dienstes zu überwachen und wie flexibel das auf neue Dienste erweitert werden kann. Im Wesentlichen muss in Prometheus nur das vorhandene Ruleset erweitert oder angepasst sowie eine passende Annotation an den Pod erweitert werden. Im konkreten Beispiel erfolgt ersteres in der Config Map des Prometheus Services und letzteres im Pod-Template des Nginx Deployments:

modules/prometheus/main.tf:
[...]
resource "kubernetes_manifest" "configmap_prometheus_metrics_prometheus_config" {
  depends_on = [kubernetes_manifest.namespace_prometheus_metrics]
  manifest = {
    "apiVersion" = "v1"
    "data" = {
    "prometheus.yaml" = <<-EOT
    # Global config
    global:
[...]
    - job_name: 'kubernetes-pods'
      kubernetes_sd_configs:
    - role: pod
      namespaces:
       names:
        - prometheus-metrics
        - ingress-nginx
[...]
modules/nginx_ic/main.tf:
resource "kubernetes_manifest"
  "deployment_ingress_nginx_ingress_nginx_controller" { 
  depends_on = [kubernetes_manifest.namespace_ingress_nginx] 
  manifest = {
    "apiVersion" = "apps/v1"
    "kind" = "Deployment"
    "metadata" = {
    "labels" = {
    "app.kubernetes.io/component" = "controller" "app.kubernetes.io/instance" = "ingress-nginx" "app.kubernetes.io/name" = "ingress-nginx" "app.kubernetes.io/part-of" = "ingress-nginx" "app.kubernetes.io/version" = "1.2.0" }
    "name" = "ingress-nginx-controller" "namespace" = "ingress-nginx"
  }
"spec" = {
  "revisionHistoryLimit" = 10
  "selector" = {
  "matchLabels" = {
  "app.kubernetes.io/component" = "controller" "app.kubernetes.io/instance" = "ingress-nginx" "app.kubernetes.io/name" = "ingress-nginx" }
}
"template" = {
  "metadata" = {
  "labels" = {
  "app.kubernetes.io/component" = "controller" "app.kubernetes.io/instance" = "ingress-nginx" "app.kubernetes.io/name" = "ingress-nginx" }
  "annotations" = {
  "prometheus.io/scrape" = "true"
  "prometheus.io/port" = "10254"
  "prometheus.io/scheme" = "http"
}
}
[...]

Fazit

In diesem Kapitel haben wir zwei sehr wichtige Komponenten in den Kubernetes Cluster integriert:
Zuerst den nginx Ingress Controller, der die Verbindung von Services ins Internet ermöglicht und als zweite Komponente Prometheus, mit dem wir Metriken des Containers überwachen können.
Im nächsten Kapitel erweitern wir den Cluster um eine Funktion zur Aggregation von Log-Dateien und letztendlich um die Möglichkeit, die gesammelten Daten mittels Grafana grafisch auszuwerten.