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:
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:
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:
- einem Deployment mit einem nginx Webserver
- einen Service vom Typ NodePort sowie
- 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.