Stack TICK sur k3s

Monitoring d'un cluster kubernetes

Suite aux vidéos de Xavki sur l'installation d'une stack Node-exporter/Prometheus/Grafana que j'ai trouvé très interessantes, j'ai voulu mettre à jour mon monitoring en m'y inspirant. Cependant je préfères la stack TICK (Télégraf, Influxdb, Chronograf et Kapacitor), la stack de influxdata, principalement pour l'intégration complète et puissante des notifications directement dans chronograf. Le fait aussi de n'avoir qu'un seul agent (Télégraf), au lieu de plusieurs exporter (docker-exporter, node-exporter etc ...).
Dernier point, je n'ai pas trouvé beaucoup d'information sur le sujet, donc cela m'a permis de beaucoup monté en compétence, notamment sur la gestion des rôles avec la création d'un rbac pour ça.
Nous allons donc voir ici, comment installer la stack TICK, le tout dans un cluster kubernetes, et sans HELM ou autre outils facilitant l'installation.

Je passerais rapidement sur beaucoup de chose, si vous n'avez pas les bases, je vous invite à regarder les vidéos de xavki.

Organisation

J'ai une certaine organisation, je ne sais pas si c'est une bonne pratique mais c'est la mienne.
Déjà j'ai un namespace par stack, et chaque déploiement de la stack se passera dedans. Pour l'organisation des fichiers, je crée un répertoire par stack/namespace, dedans j'ai mes fichiers qui seront globaux à la stack, et ensuite un répertoire par déploiement avec les fichiers nécessaires à celui là. Donc en gros nous aurons :

  • monitor
    • ns.yml
    • telegraf
      • 10-volumes.yml
      • 20-daemonset.yml
    • kapacitor
      • 10-volumes.yml
      • 20-deployment.yml
      • 30-services.yml
    • chronograf
      • .....
    • influxdb
      • .....

Architecture

Pour ce tutoriel, j'utilise ma production (pas bien), en gros j'ai un cluster k3s avec un master et 3 workers, tout les fichiers seront stockés sur ma VM qui sert de fileserver avec un serveur NFS.

J'ai donc 5 VMs :

  • k3s-master : 192.168.1.121
  • k3s-node1 : 192.168.1.131
  • k3s-node2 : 192.168.1.132
  • k3s-node3 : 192.168.1.133
  • filer : 192.168.1.105

Création du namespace

Pour le namespace, c'est plutôt simple, nous créons notre fichier ns.yml avec ceci dedans :

apiVersion: v1
kind: Namespace
metadata:
  name: monitor

et on applique :

$ kubectl apply -f ns.yml
namespace/monitor created

Déploiement de influxdb

Comme dit j'ai mon organisation, donc dans mon répertoire monitor, je crée un autre répertoire influxdb, dans lequel je vais mettre tout les fichiers suivants qui concernent influxdb.

10-volume.yml

Nous avons besoin d'un volume, comme dit je stock tout sur mon serveur NFS, donc je commence par créer un fichier 10-volumes.yml :

apiVersion: v1
kind: PersistentVolume
metadata:
  name: data-influxdb-pv
  namespace: monitor
spec:
  storageClassName: data-influxdb
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.1.105
    path: "/data/influxdb"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-influxdb-pvc
  namespace: monitor
spec:
  storageClassName: data-influxdb
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 500Mi

A adapter pour votre besoin

20-deployment.yml

kind: Deployment
apiVersion: apps/v1
metadata:
  namespace: monitor
  name: influxdb
  labels:
    app: influxdb

spec:
  replicas: 1
  selector:
    matchLabels:
      app: influxdb
  template:
    metadata:
      labels:
        app: influxdb
        namespace: monitor
    spec:
      containers:
        - name: influxdb
          image: influxdb:1.8-alpine
          ports:
            - containerPort: 8086
          volumeMounts:
            - mountPath: /var/lib/influxdb
              name: data
          resources:
            requests:
              memory: 300M
              cpu: 0.2
            limits:
              memory: 800M
              cpu: 0.5
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: data-influxdb-pvc

Rien de particulier, on a juste un volume (NFS) monté dans /var/lib/influxdb.

30-services.yml

apiVersion: v1
kind: Service
metadata:
  name: influxdb-web
  namespace: monitor
spec:
  ports:
    - protocol: TCP
      name: web
      port: 8086
  selector:
    app: influxdb
---
apiVersion: v1
kind: Service
metadata:
  name: influxdb-externe
  namespace: monitor
spec:
  ports:
    - protocol: TCP
      name: web
      port: 8086
  type: LoadBalancer
  selector:
    app: influxdb

J'ajoute ici un service de type loadbalancer afin de pouvoir y accéder depuis l'exterieur du cluster, car j'ai d'autres machines à monitorer.

On applique

$ kubectl apply -f .
persistentvolume/config-influxdb-pv created
persistentvolumeclaim/config-influxdb-pvc created
deployment.apps/influxdb created
service/influxdb-web created
service/influxdb-externe created

Et normalement on a bien notre influxdb :

$ kubectl get all -n monitor
NAME                             READY   STATUS    RESTARTS   AGE
pod/influxdb-6d45d54c68-dsm69    1/1     Running   0          69m
pod/svclb-influxdb-externe-bgfz2   1/1     Running   0          85s
pod/svclb-influxdb-externe-bprrk   1/1     Running   0          85s
pod/svclb-influxdb-externe-7rx8h   1/1     Running   0          85s
pod/svclb-influxdb-externe-vdsbc   1/1     Running   0          85s

NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/influxdb-web     ClusterIP   10.43.62.69     <none>        8086/TCP   3h36m
service/influxdb-externe   LoadBalancer   10.43.211.61    192.168.1.131   8086:30874/TCP   86s

NAME                                    DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/svclb-influxdb-externe   4         4         4       4            4           <none>          27m

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/influxdb     1/1     1            1           3h36m

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/influxdb-6d45d54c68    1         1         1       3h36m

Comme dit je suis sur k3s, qui utilise klipperlb pour la gestion du LoadBalancing.

Telegraf

Là ça va être la partie la plus complexe, et celle dont j'ai le plus galéré.
Toujours dans ma logique, je suis dans un répertoire telegraf dédié à l'application elle-même.

00-rbac.yml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: telegraf
rules:
- apiGroups: [""]
  resources:
  - nodes
  - nodes/proxy
  - services
  - endpoints
  - pods
  - persistentvolumes
  - persistentvolumeclaims
  verbs: ["get", "list"]
- apiGroups:
  - apps
  resources:
  - deployments
  - daemonsets
  - replicasets
  - statefulsets
  verbs: ["get", "list"]
- nonResourceURLs: ["/metrics"]
  verbs: ["get"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: telegrafaccount
  namespace: monitor
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: telegraf
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: telegraf
subjects:
- kind: ServiceAccount
  name: telegrafaccount
  namespace: monitor

On commence par les autorisations, le but étant de donner certains droit à telegraf dans le conteneur. Ici nous lui donnons la possibilités de récupérer plusieurs informations, mais en aucun cas de créer quelques choses.

12-configmap.yml

Ici nous avons une configuration static, donc nous allons utiliser un configmap :

apiVersion: v1
kind: ConfigMap
metadata:
  name: telegraf-config
  namespace: monitor
data:
  telegraf.conf: |
    [global_tags]

    [agent]
      interval = "10s"
      round_interval = true
      metric_batch_size = 1000
      metric_buffer_limit = 10000
      collection_jitter = "0s"
      flush_interval = "10s"
      flush_jitter = "0s"
      precision = ""
      hostname = "${NODE_NAME}"
      omit_hostname = false
    
    [[outputs.influxdb]]
      urls = [ "http://influxdb-web:8086" ]
      database = "telegraf"
    
    [[inputs.cpu]]
      percpu = true
      totalcpu = true
      collect_cpu_time = false
      report_active = false
    
    [[inputs.disk]]
      ignore_fs = ["tmpfs", "devtmpfs", "devfs", "iso9660", "overlay", "aufs", "squashfs", "nfs"]
    
    [[inputs.diskio]]
    
    [[inputs.kernel]]
    
    [[inputs.mem]]
    
    [[inputs.processes]]
    
    [[inputs.swap]]
    
    [[inputs.system]]
   
    [[inputs.kubernetes]]
      url = "https://kubernetes.default.svc.cluster.local/api/v1/nodes/$NODE_NAME/proxy"
      insecure_skip_verify = true

    [[inputs.kube_inventory]]
      url = "https://kubernetes.default.svc.cluster.local"
      insecure_skip_verify = true
      namespace= ""

C'est la configuration de telegraf, donc je vous invite à en regarder la docs (cf annexes).
Nous avons ici quelques spécificité, comme le hostname = "${NODE_NAME}, ceci est une variable que l'on récupère au niveau du déploiement (enfin du daemonset, nous verrons ceci).
Autrement nous avons l'url de kubernetes, en gros nous tapons sur le namespace default pour pouvoir accéder à l'API.

20-daemonset.yml

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: telegraf
  namespace: monitor
  labels:
    name: telegraf
spec:
  selector:
    matchLabels:
      name: telegraf
  template:
    metadata:
      labels:
        name: telegraf
    spec:
      hostPID: true
      hostIPC: true
      serviceAccountName: telegrafaccount
      containers:
        - resources:
            requests:
              cpu: 0.15
              memory: 128M
          env:
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          securityContext:
            privileged: true
          image: telegraf:1.16-alpine
          name: telegraf
          volumeMounts:
            - name: dev
              mountPath: /host/dev
            - name: proc
              mountPath: /host/proc
            - name: sys
              mountPath: /host/sys
            - name: rootfs
              mountPath: /rootfs
            - name: config
              mountPath: /etc/telegraf
      volumes:
        - name: config
          configMap:
            name: telegraf-config
        - name: proc
          hostPath:
            path: /proc
        - name: dev
          hostPath:
            path: /dev
        - name: sys
          hostPath:
            path: /sys
        - name: rootfs
          hostPath:
            path: /

Alors là nous créons un daemonset au lieu d'un deployment, ceci permets d'avoir une instance par noeud.
En plus de ceci, nous utilisons donc le serviceAccount créé tout à l'heure.
Le but étant de monitorer aussi bien l'hôte que kubernetes lui même, j'ai donc ajouté hostPID et hostIPC, ainsi que les points de montage /proc, /dev, /sys et /. J'aurais pu également utiliser hostNetwork, mais ceci fait sortir mon conteneur du réseau du cluster, je verrais par la suite pour le mettre ou non, mais cela ajoutera des modifications à la configuration de telegraf.
Ensuite dans le env, j'ajoute une variable NODE_NAME, pour récupérer le nom de l'hôte sur lequel le pod tourne, ce qui me permets d'overwrite le hostname, et au lieu d'avoir un tag host avec le nom du pod, j'ai bien le nom du noeud.

Au lieu de faire ceci, j'aurais pu effectivement installer telegraf sur l'hôte directement, mais je trouvais ça interressant de le faire via kubernetes.

On applique

$ kubectl apply -f .
clusterrole.rbac.authorization.k8s.io/telegraf created
serviceaccount/telegrafaccount created
clusterrolebinding.rbac.authorization.k8s.io/telegraf created
persistentvolume/telegraf-pv created
persistentvolumeclaim/telegraf-pvc created
configmap/telegraf-config created
daemonset.apps/telegraf created

Ce qui donne :

$ kubectl get all -n monitor
NAME                               READY   STATUS    RESTARTS   AGE
pod/influxdb-6d45d54c68-dsm69      1/1     Running   0          99m
pod/svclb-influxdb-externe-bgfz2   1/1     Running   0          27m
pod/svclb-influxdb-externe-bprrk   1/1     Running   0          27m
pod/svclb-influxdb-externe-7rx8h   1/1     Running   0          27m
pod/svclb-influxdb-externe-vdsbc   1/1     Running   0          27m
pod/telegraf-kk8j2                 1/1     Running   0          31s
pod/telegraf-mz47w                 1/1     Running   0          31s
pod/telegraf-lm727                 1/1     Running   0          31s
pod/telegraf-hjnxj                 1/1     Running   0          30s

NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
service/influxdb-web       ClusterIP      10.43.62.69    <none>          8086/TCP         4h5m
service/influxdb-externe   LoadBalancer   10.43.211.61   192.168.1.131   8086:30874/TCP   27m

NAME                                    DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/svclb-influxdb-externe   4         4         4       4            4           <none>          27m
daemonset.apps/telegraf                 4         4         4       4            4           <none>          31s

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/influxdb    1/1     1            1           4h5m

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/influxdb-6d45d54c68    1         1         1       4h5m

Kapacitor

Je passerai très rapidement dessus, avec simplement les fichiers de conf

10-volumes.yml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: data-kapacitor-pv
  namespace: monitor
spec:
  storageClassName: data-kapacitor
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.1.105
    path: "/data/kapacitor"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-kapacitor-pvc
  namespace: monitor
spec:
  storageClassName: data-kapacitor
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 500Mi

12-config.yml

apiVersion: v1
kind: ConfigMap
metadata:
  name: kapacitor-config
  namespace: monitor
data:
  kapacitor.conf: |
    data_dir = "/var/lib/kapacitor"

    [replay]
      dir = "/var/lib/kapacitor/replay"

    [storage]
      boltdb = "/var/lib/kapacitor/kapacitor.db"

20-deployment.yml

kind: Deployment
apiVersion: apps/v1
metadata:
  namespace: monitor
  name: kapacitor
  labels:
    app: kapacitor

spec:
  replicas: 1
  selector:
    matchLabels:
      app: kapacitor
  template:
    metadata:
      labels:
        app: kapacitor
        namespace: monitor
    spec:
      containers:
        - name: kapacitor
          image: kapacitor:1.5-alpine
          ports:
            - containerPort: 9092
          env:
            - name: KAPACITOR_INFLUXDB_0_URLS_0
              value: "http://influxdb-web:8086"
          volumeMounts:
            - mountPath: /etc/kapacitor/
              name: config
            - mountPath: /var/lib/kapacitor/
              name: data
          resources:
            requests:
              memory: 150M
              cpu: 0.2
            limits:
              memory: 500M
              cpu: 0.5
      volumes:
      - name: config
        configMap:
          name: kapacitor-config
      - name: data
        persistentVolumeClaim:
          claimName: data-kapacitor-pvc

30-service.yml

apiVersion: v1
kind: Service
metadata:
  name: kapacitor-web
  namespace: monitor
spec:
  ports:
    - protocol: TCP
      name: web
      port: 9092
  selector:
    app: kapacitor

On applique

$ kubectl apply -f .
persistentvolume/data-kapacitor-pv created
persistentvolumeclaim/data-kapacitor-pvc created
configmap/kapacitor-config created
deployment.apps/kapacitor created
service/kapacitor-web created

$ kubectl get all -n monitor
NAME                               READY   STATUS    RESTARTS   AGE
pod/influxdb-6d45d54c68-dsm69      1/1     Running   0          107m
pod/svclb-influxdb-externe-bgfz2   1/1     Running   0          35m
pod/svclb-influxdb-externe-bprrk   1/1     Running   0          35m
pod/svclb-influxdb-externe-7rx8h   1/1     Running   0          35m
pod/svclb-influxdb-externe-vdsbc   1/1     Running   0          35m
pod/telegraf-kk8j2                 1/1     Running   0          9m12s
pod/telegraf-mz47w                 1/1     Running   0          9m12s
pod/telegraf-lm727                 1/1     Running   0          9m12s
pod/telegraf-hjnxj                 1/1     Running   0          9m11s
pod/kapacitor-f6c7dfcb9-pvnwr      1/1     Running   0          37s

NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
service/influxdb-web       ClusterIP      10.43.62.69     <none>          8086/TCP         4h14m
service/influxdb-externe   LoadBalancer   10.43.211.61    192.168.1.131   8086:30874/TCP   35m
service/kapacitor-web      ClusterIP      10.43.167.162   <none>          9092/TCP         38s

NAME                                    DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/svclb-influxdb-externe   4         4         4       4            4           <none>          35m
daemonset.apps/telegraf                 4         4         4       4            4           <none>          9m12s

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/influxdb    1/1     1            1           4h14m
deployment.apps/kapacitor   1/1     1            1           38s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/influxdb-6d45d54c68   1         1         1       4h14m
replicaset.apps/kapacitor-f6c7dfcb9   1         1         1       38s

Chronograf

Pareil, je montre juste les fichiers de déploiement, car ça reste un service plutôt basique.

10-volumes.yml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: data-chronograf-pv
  namespace: monitor
spec:
  storageClassName: data-chronograf
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.1.105
    path: "/data/config/chronograf"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-chronograf-pvc
  namespace: monitor
spec:
  storageClassName: data-chronograf
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 500Mi

20-deployment.yml

kind: Deployment
apiVersion: apps/v1
metadata:
  namespace: monitor
  name: chronograf
  labels:
    app: chronograf

spec:
  replicas: 1
  selector:
    matchLabels:
      app: chronograf
  template:
    metadata:
      labels:
        app: chronograf
        namespace: monitor
    spec:
      containers:
        - name: chronograf
          image: chronograf:1.8-alpine
          ports:
            - containerPort: 8888
          env:
            - name: INFLUXDB_URL
              value: http://influxdb-web:8086
            - name: KAPACITOR_URL
              value: http://kapacitor-web:9092
          volumeMounts:
            - mountPath: /var/lib/chronograf/
              name: data
          resources:
            requests:
              memory: 150M
              cpu: 0.2
            limits:
              memory: 500M
              cpu: 0.5
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: data-chronograf-pvc

30-service.yml

apiVersion: v1
kind: Service
metadata:
  name: chronograf-web
  namespace: monitor
spec:
  ports:
    - protocol: TCP
      name: web
      port: 8888
  selector:
    app: chronograf
---
apiVersion: v1
kind: Service
metadata:
  name: chronograf-externe
  namespace: monitor
spec:
  ports:
    - protocol: TCP
      name: web
      port: 8888
  type: LoadBalancer
  selector:
    app: chronograf

On applique

$ kubectl apply -f .
persistentvolume/data-chronograf-pv created
persistentvolumeclaim/data-chronograf-pvc created
deployment.apps/chronograf created
service/chronograf-web created
service/chronograf-externe created

$ kubectl get all -n monitor
NAME                                 READY   STATUS    RESTARTS   AGE
pod/influxdb-6d45d54c68-dsm69        1/1     Running   0          114m
pod/svclb-influxdb-externe-bgfz2     1/1     Running   0          42m
pod/svclb-influxdb-externe-bprrk     1/1     Running   0          42m
pod/svclb-influxdb-externe-7rx8h     1/1     Running   0          42m
pod/svclb-influxdb-externe-vdsbc     1/1     Running   0          42m
pod/telegraf-kk8j2                   1/1     Running   0          15m
pod/telegraf-mz47w                   1/1     Running   0          15m
pod/telegraf-lm727                   1/1     Running   0          15m
pod/telegraf-hjnxj                   1/1     Running   0          15m
pod/kapacitor-f6c7dfcb9-pvnwr        1/1     Running   0          7m
pod/chronograf-85567dd664-s2xdd      1/1     Running   0          34s
pod/svclb-chronograf-externe-2gfsk   1/1     Running   0          33s
pod/svclb-chronograf-externe-kg4jq   1/1     Running   0          33s
pod/svclb-chronograf-externe-jxmzf   1/1     Running   0          33s
pod/svclb-chronograf-externe-hcvz7   1/1     Running   0          33s

NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
service/influxdb-web         ClusterIP      10.43.62.69     <none>          8086/TCP         4h21m
service/influxdb-externe     LoadBalancer   10.43.211.61    192.168.1.131   8086:30874/TCP   42m
service/kapacitor-web        ClusterIP      10.43.167.162   <none>          9092/TCP         7m1s
service/chronograf-web       ClusterIP      10.43.144.238   <none>          8888/TCP         34s
service/chronograf-externe   LoadBalancer   10.43.185.201   192.168.1.133   8888:31980/TCP   33s

NAME                                      DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/svclb-influxdb-externe     4         4         4       4            4           <none>          42m
daemonset.apps/telegraf                   4         4         4       4            4           <none>          15m
daemonset.apps/svclb-chronograf-externe   4         4         4       4            4           <none>          33s

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/influxdb     1/1     1            1           4h21m
deployment.apps/kapacitor    1/1     1            1           7m1s
deployment.apps/chronograf   1/1     1            1           34s

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/influxdb-6d45d54c68     1         1         1       4h21m
replicaset.apps/kapacitor-f6c7dfcb9     1         1         1       7m1s
replicaset.apps/chronograf-85567dd664   1         1         1       34s

Normalement ça fonctionne

Normalement vous devriez pouvoir y accéder via http://ipexterne:8888, et vous devriez avoir la possibilité de créer vos jolies dashboard.
Ici la partie interressant est vraiment l'installation de telegraf, qui est plus complexe que le reste :

chronograf1

chronograf2

Annexe