Monitoring K8s

Nadat de Kubernetes Cluster ‘up-and-running’ is en er pods met applicaties op draaien, willen we de status van het cluster gaan monitoren. Datgene dat gemonitored kan worden dient ‘gescraped‘ te worden en voor Kubernetes is Prometheus daar de uitgelezen tool voor. Scrapen van de systeem-eigenschappen van de node(s) zelf kan heel goed met ‘node-exporter‘ en ‘arm-exporter‘, Kubernetes metrics worden opgehaald via ‘k8s-metrics‘. Om het grafisch inzichtelijk te maken wordt ‘Grafana‘ gebruikt.

Dit artikel beschrijft de installatie van monitoring op een Raspberry Pi K3s-cluster. Andere clusters die beheerd worden door Rancher kunnen gebruik maken van de ‘Apps & Marketplace‘ om Monitoring te installeren. Zie daarvoor de pagina over Monitoring met Rancher 2.5>

  • node-exporter
  • arm-exporter
  • k8s-metrics
  • prometheus
  • grafana

We maken een namespace genaamd monitoring om de diverse onderdelen in onder te brengen:

$ kubectl create namespace monitoring

Node-Exporter

Hiervoor dient de volgende yaml-file (node-exporter.yml) in Kubernetes geladen te worden:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
  labels:
    k8s-app: node-exporter
spec:
  selector:
    matchLabels:
      name: node-exporter
  template:
    metadata:
      labels:
        name: node-exporter
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      containers:
      - name: node-exporter
        image: nokkie/node-exporter:arm64
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
$ kubectl apply -f node-exporter.yml

Hiermee wordt een Daemonset gemaakt zodat elke node de metrics kan publiceren op port 9100. De service hiervoor wordt later gemaakt.

ARM-exporter

De ARM exporter publiceert ARM-specifieke metrics en daarvoor dient de volgende yaml-file (arm-exporter.yml):

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: arm-exporter
  namespace: monitoring
  labels:
    k8s-app: arm-exporter
spec:
  selector:
    matchLabels:
      name: arm-exporter
  template:
    metadata:
      labels:
        name: arm-exporter
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      containers:
      - name: arm-exporter
        image: carlosedp/arm_exporter
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
$ kubectl apply -f arm-exporter.yml

ARM metrics worden gepubliceerd via een DaemonSet op port 9143 en de service daarvoor wordt later ingesteld.

Kubernetes Metrics

De k82-metrics stellen we in via een yaml file (k8s-metrics.yml):

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: k8s-metrics
  namespace: monitoring
  labels:
    k8s-app: k8s-metrics
spec:
  selector:
    matchLabels:
      name: k8s-metrics
  template:
    metadata:
      labels:
        name: k8s-metrics
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      containers:
      - name: k8s-metrics
        image: k8s.gcr.io/kube-state-metrics/kube-state-metrics:v2.1.1
        resources:
          limits:
            memory: 250Mi
          requests:
            cpu: 100m
            memory: 200Mi
$ kubectl apply -f k8s-metrics.yml

De DaemonSet publiceert de metrics op port 8081 en samen met de vorige exporters stellen we die beschikbaar met service.yml:

apiVersion: v1
kind: Service
metadata:
  name: node-exporter-svc
  namespace: monitoring
spec:
  ports:
  - name: node-exporter-port
    port: 9100
    protocol: TCP
    targetPort: 9100
  selector:
    name: node-exporter
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: arm-exporter-svc
  namespace: monitoring
spec:
  ports:
  - name: arm-exporter-port
    port: 9243
    protocol: TCP
    targetPort: 9243
  selector:
    name: arm-exporter
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: k8s-metrics-svc
  namespace: monitoring
spec:
  ports:
  - name: k8s-metrics-port
    port: 8081
    protocol: TCP
    targetPort: 8081
  selector:
    name: k8s-metrics
  sessionAffinity: None
  type: ClusterIP
$ kubectl apply -f service.yml

Tot zover hebben we de volgende workloads:

$ kubectl -n monitoring get ds,svc
NAME                                      DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/arm-exporter               3         3         3       3            3                     111m
daemonset.apps/k8s-metrics                3         3         3       3            3                     111m
daemonset.apps/node-exporter              3         3         3       3            3                     111m
NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)          AGE
service/arm-exporter-svc     ClusterIP      10.43.99.2                  9243/TCP         111m
service/k8s-metrics-svc      ClusterIP      10.43.194.75                8081/TCP         111m
service/node-exporter-svc    ClusterIP      10.43.181.137               9100/TCP         111m

Prometheus

De installatie van Prometheus gaat via een aantal stappen. Eerst wordt gebruik gemaakt van een ClusterRole die door een ServiceAccount gebruikt kan worden. De clusterrole.yml-file:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prometheus
rules:
- apiGroups: [""]
  resources:
  - nodes
  - nodes/proxy
  - services
  - endpoints
  - pods
  verbs: ["get", "list", "watch"]
- apiGroups:
  - extensions
  resources:
  - ingresses
  verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: prometheus
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus
subjects:
- kind: ServiceAccount
  name: default
  namespace: monitoring
$ kubectl apply -f clusterrole.yml

De volgende stap is een ConfigMap (configmap.yml) die gebruikt gaat worden als een configuratie voor Prometheus middels een volume. Hierin staan de targets voor Prometheus waaruit de metrics gelezen worden. Deze yaml-file ziet er zo uit:

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-server-conf
  labels:
    name: prometheus-server-conf
  namespace: monitoring
data:
  prometheus.yml: |-
    global:
      scrape_interval: 5s
      evaluation_interval: 5s
    rule_files:
      - /etc/prometheus/prometheus.rules
    scrape_configs:
      - job_name: 'node-exporter'
        kubernetes_sd_configs:
        - role: endpoints
        relabel_configs:
        - source_labels: [__meta_kubernetes_endpoints_name]
          regex: 'node-exporter-svc'
          action: keep
      - job_name: 'arm-exporter'
        kubernetes_sd_configs:
        - role: endpoints
        relabel_configs:
        - source_labels: [__meta_kubernetes_endpoints_name]
          regex: 'arm-exporter-svc'
          action: keep
      - job_name: 'k8s-metrics'
        kubernetes_sd_configs:
        - role: endpoints
        relabel_configs:
        - source_labels: [__meta_kubernetes_endpoints_name]
          regex: 'k8s-metrics-svc'
          action: keep

De namen (endpoints) voor de targets worden gevonden met een role ‘endpoints’ die de ‘regex’ van de services gebruikt. Met Kubectl vind je die zo:

$ kubectl -n monitoring get ep
NAME                 ENDPOINTS                                          AGE
arm-exporter-svc     10.42.0.45:9243,10.42.1.50:9243,10.42.3.100:9243   5h4m
k8s-metrics-svc      10.42.0.46:8081,10.42.1.51:8081,10.42.3.101:8081   5h4m
node-exporter-svc    10.42.0.44:9100,10.42.1.49:9100,10.42.3.99:9100    5h4m

De deployment.yml voor Prometheus zelf ziet er zo uit:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-deployment
  namespace: monitoring
  labels:
    app: prometheus-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus-server
  template:
    metadata:
      labels:
        app: prometheus-server
    spec:
      nodeName: rpi-2
      containers:
        - name: prometheus
          image: prom/prometheus
          args:
            - "--config.file=/etc/prometheus/prometheus.yml"
            - "--storage.tsdb.path=/prometheus/"
          ports:
            - containerPort: 9090
          volumeMounts:
            - name: prometheus-config-volume
              mountPath: /etc/prometheus/
            - name: prometheus-storage-volume
              mountPath: /prometheus/
      volumes:
        - name: prometheus-config-volume
          configMap:
            defaultMode: 420
            name: prometheus-server-conf
  
        - name: prometheus-storage-volume
          emptyDir: {}
$ kubectl apply -f deployment.yml

Ik laat deze bewust op node rpi-2 deployen zodat de Grafana deployment later op een andere node deployed wordt. Al had ik dat ook met een pod(Ant)iAffinity kunnen doen. 😉

Een service wordt aangemaakt van het type LoadBalancer maar dat had ook een NodePort kunnen zijn, afhankelijk van de configuratie. In dit geval is MetalLB geinstalleerd dus kunnen we de LoadBalancer gebruiken. Een prometheus-service.yml manifest:

apiVersion: v1
kind: Service
metadata:
  name: prometheus-service
  namespace: monitoring
spec:
  selector: 
    app: prometheus-server
  type: LoadBalancer
  ports:
    - port: 8088
      targetPort: 9090 
$ kubectl apply -f prometheus-service.yml

Prometheus kan nu via een webbrowser benaderd worden op het IP adres van de loadbalancer en port 8088, bijvoorbeeld: http://192.168.1.88:8088

Via de menu-optie Status -> Targets kunnen de endpoint bekeken worden:

en via ‘Graph’ kunnen expression uitgevoerd worden en in een tabel of een grafiek weergegeven worden:

Grafana

Dashboards met de grafische weergave van de metrics van de targets kunnen gemaakt worden met Grafana. Deze maakt gebruik van datasources en de datasource van Prometheus kunnen we aan Grafana meegeven via een ConfigMap. Die ziet er dan alsvolgt uit (grafana-cm.yml):

apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources
  namespace: monitoring
data:
  prometheus.yaml: |-
    {
        "apiVersion": 1,
        "datasources": [
            {
              "access":"proxy",
              "editable": true,
              "name": "prometheus",
              "orgId": 1,
              "type": "prometheus",
              "url": "http://prometheus-service.monitoring.svc:8080",
              "version": 1
            }
        ]
    }
$ kubectl apply -f grafana-cm.yml

De grafana-deployment.yml ziet er zo uit:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      name: grafana
      labels:
        app: grafana
    spec:
      nodeName: rpi-1
      containers:
      - name: grafana
        image: grafana/grafana:latest
        ports:
        - name: grafana
          containerPort: 3000
        resources:
          limits:
            memory: "1Gi"
            cpu: "1000m"
          requests: 
            memory: 500M
            cpu: "500m"
        volumeMounts:
          - mountPath: /var/lib/grafana
            name: grafana-storage
          - mountPath: /etc/grafana/provisioning/datasources
            name: grafana-datasources
            readOnly: false
      volumes:
        - name: grafana-storage
          emptyDir: {}
        - name: grafana-datasources
          configMap:
              defaultMode: 420
              name: grafana-datasources
$ kubectl apply -f grafana-deployment.yml

En zoals je ziet luistert Grafana op port 3000 dus daar maken we nog even een grafana.svc.yml voor:

apiVersion: v1
kind: Service
metadata:
  name: grafana
  namespace: monitoring
spec:
  selector: 
    app: grafana
  type: LoadBalancer  
  ports:
    - port: 3000
      targetPort: 3000
$ kubectl apply -f grafana-svc.yml

Grafana kan via een webbrowser op het IP adres van de loadbalancer en port 3000 benaderd worden. Username/password is standaard admin/admin en kan dan direct gewijzigd worden.

Een tutorial voor Grafana kan hier gevonden worden.