Featured image of post Sauvegarder des dashboards Grafana dans Kubernetes, en s’amusant (j’explique les RBAC, Job et les CronJob)

Sauvegarder des dashboards Grafana dans Kubernetes, en s’amusant (j’explique les RBAC, Job et les CronJob)

Ecrit par ~ zwindler ~

Note : Pour les allergiques à Kubernetes, cet article peut être vu comme une entrée en la matière, car je vais progressivement explorer certains concepts, de manière très progressive.

Note 2 : par convention dans cet article, j’ai essayé de nommer tous les objets Kubernetes avec une majuscule à tous les mots (exemple : ServiceAccount) pour que ce soit plus clair.

Grafana

Je ne vous ferai pas l’affront de vous présenter Grafana, l’outil de visualisation open source de la société éponyme. C’est un outil que j’utilise depuis un moment et qui est quasiment incontournable dans toutes les solutions de monitoring modernes (ou non ? xD).

D’ailleurs, j’ai écrit déjà quelques articles sur Grafana au fil des années, c’est un outil aussi simple qu’irremplaçable (jusqu’à ce qu’on trouve mieux, bien entendu).

Si vous avez déjà utilisé au moins une fois Grafana, vous savez donc que les visualisations sont organisées comme suit :

  • des dossiers
  • dans lesquels on range des dashboards
  • qui contiennent des panels (nos visualisations)

On peut donc créer des dashboards à gogo en fonction du besoin, les cloner, les modifier, les importer depuis un magasin sur grafana.com (assez inutilisable maintenant, mais bon) et en afficher le code JSON.

Et ça, c’est super cool, parce que si mon Grafana brûle (mes machines persos ne sont pas hébergées dans les conditions les plus pros possibles… 😱), je n’ai pas envie de tout perdre. Mais aller régulièrement copier / coller le code JSON de chaque dashboard, c’est pénible.

Because I’m API

Un truc qui est cool avec Grafana, c’est que comme tout logiciel un peu pro qui se respecte, il dispose d’une API. Je ne vais pas juger de sa qualité globale d’API (vous connaissez peut-être bien mieux que moi), mais elle a le mérite d’exister.

Récemment, je me suis donc demandé quelle quantité d’effort, il faudrait déployer pour sauvegarder de manière automatisée des dashboards Grafana avec l’API et Kubernetes (oui hein, vous n’allez pas y couper).

Partons donc du principe que j’ai un cluster Kubernetes sur lequel j’ai déployé la chart Helm officielle de Grafana (rappel, Helm, c’est à la fois un package manager et un moyen de déployer des applications dans Kubernetes).

À partir de là, de quoi j’ai besoin ?

  • Je suis le roi des fainéants, je ne veux pas avoir à aller créer les accès dans Grafana moi même (via 3 clics clics). Il faut donc que j’automatise la création d’un token pour communiquer avec l’API
  • Une fois que j’ai mon token, je veux créer une tâche périodique qui va se connecter à l’API, lister les dashboards disponibles et les sauvegarder sur un bucket s3 chez Scaleway (il y a 75 Go gratuit dans le free-tier, ça suffira largement)

Se préparer à créer un token

Bon, là en apparence, on se retrouve face à un problème d’œuf / poule. Comment je fais pour me connecter à l’API si je n’ai pas un accès à l’API ?

Heureusement, on va tricher… grâce à Kubernetes 😏

Quand on déploie Grafana dans Kubernetes, la chart officielle va créer un Secret (au sens Kubernetes du terme) qui contient le login / password du compte local administrateur de Grafana.

apiVersion: v1
kind: Secret
metadata:
  creationTimestamp: "2024-10-01T13:11:48Z"
  name: grafana
  namespace: grafana
  resourceVersion: "1234567"
  uid: deadbeef-dead-cafe-baba-123456789012
data:
  admin: YWRtaW4=
  password: TmV2ZXJHb25uYUdpdmVZb3VVcAo=
type: Opaque

S’il est déconseillé de l’utiliser au jour le jour (mieux vaut utiliser des comptes nominatifs, idéalement via un Identity Provider), c’est super pratique ici !

Il suffit de créer un Job Kubernetes avec suffisamment de droits pour lire ce Secret, qui va se connecter (une seule fois) sur l’API avec le compte admin, créer un token, le récupérer, et le stocker dans un autre Secret !

Sans rentrer à fond dans les détails de comment fonctionne le RBAC (Role Based Access Control) dans Kubernetes, voilà à quoi ça ressemble :

---
apiVersion: v1
# le compte de service (ServiceAccount) qui va être utilisé par le Job et qui a besoin de droits particuliers
kind: ServiceAccount
metadata:
  name: grafana-backup-sa
  namespace: grafana
---
apiVersion: rbac.authorization.k8s.io/v1
# le rôle, c'est la politique de sécurité qui va nous permettre de faire des trucs
kind: Role
metadata:
  namespace: grafana
  name: grafana-backup-role
rules:
# ici je n'autorise qu'à lire le secret contenant l'admin/password de grafana, pas plus
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["grafana"]
  verbs: ["get"]
# ici, je vais autoriser le fait de modifier un secret qui s'appelle grafana-backup
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["grafana-backup"]
  verbs: ["get", "update", "patch"]
# ici, je vais autoriser le fait de créer des secrets
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["create"]
---
# ici, je lie simplement ServiceAccount à un Role pour lui donner les droits dont j'ai besoin
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: grafana-backup-rolebinding
  namespace: grafana
subjects:
- kind: ServiceAccount
  name: grafana-backup-sa
  namespace: grafana
roleRef:
  kind: Role
  name: grafana-backup-role
  apiGroup: rbac.authorization.k8s.io

Note : vous aurez remarqué que je n’ai pas autorisé la création ET la modification du Secret grafana-backup en une seule étape, mais en deux. Ceci est dû à une limitation de l’API de Kubernetes, qui ne permet pas de limiter la création d’un objet via son “ressource name”. Je vous laisse aller lire la doc officielle si ça vous intéresse.

Sachez que je me suis un peu “embêté” à gérer la politique de droits et qu’on aurait pu faire bien plus simple. Mais c’est pour respecter le principe en sécurité informatique du moindre privilège (c’est important).

Bon… on va le créer, ce token ??

Maintenant qu’on a géré les aspects de sécurité (ça limitera les possibilités d’un attaquant qui réussirait à prendre la main sur notre Pod / container), on peut créer la tâche qui va créer le token.

Ici, il n’y a pas de raison de laisser tourner un container ad vitam une fois que le token est créé, on peut l’éteindre. Pour ça, il existe un objet dans Kubernetes qui s’appelle le Job (avec une gestion d’erreur et de retry intégré). Voilà à quoi il va ressembler :

---
apiVersion: batch/v1
kind: Job
metadata:
  name: grafana-backup-api-token-job
  namespace: grafana
spec:
  template:
    spec:
      # on lui donne bien le bon ServiceAccount sinon on a fait tout ce RBAC pour rien T_T
      serviceAccountName: grafana-backup-sa 
      containers:
      - name: grafana-create-api-token
        # une petite astuce pour de la bidouille, merci Jérôme :-*
        image: nixery.dev/shell/curl/jq/kubectl:latest
        # on "monte" le Secret Kubernetes comme des fichiers dans notre container
        volumeMounts:
        - name: grafana-secret-volume
          mountPath: /etc/grafana-secret
          readOnly: true
        command:
        - /bin/sh
        - -c
        - |
          # Comme on a monté le secret dans notre container, c'est super facile de récupérer
          # l'utilisateur et le mot de passe admin sans avoir à le stocker nulle part (surtout pas sur github) 
          GRAFANA_ADMIN_USER=$(cat /etc/grafana-secret/admin)
          GRAFANA_ADMIN_PASS=$(cat /etc/grafana-secret/password)

          # On peut maintenant faire une requête HTTP via curl sur l'API de Grafana
          GRAFANA_API_TOKEN=$(curl -s -X POST http://grafana:80/api/auth/keys \
            -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASS" \
            -H "Content-Type: application/json" \
            -d '{
              "name": "Kubernetes Backup Token",
              "role": "Admin",
              "secondsToLive": 31536000
            }' | jq -r '.key')

          # On termine par stocker le token dans le nouveau Secret grafana-backup
          kubectl create secret generic grafana-backup \
            --from-literal=GRAFANA_API_TOKEN=$GRAFANA_API_TOKEN \
            --dry-run=client -o yaml | kubectl apply -f -          
      restartPolicy: OnFailure
      volumes:
      - name: grafana-secret-volume
        secret:
          secretName: grafana

Normalement, avec ce bon script code BASH, j’ai réconcilié les amateurs de bash avec Kubernetes.

Comment ça, “non” ?

Il ne me reste plus qu’à appliquer le manifest YAML sur mon cluster Kubernetes et le Secret grafana-backup contenant la clé d’API devrait apparaitre sur notre cluster :

kubectl apply -f job.yaml

kubectl get secrets
NAME                                      TYPE                DATA   AGE
grafana-backup                            Opaque              1      1m

kubectl describe secret grafana-backup
Name:         grafana-backup
Namespace:    grafana
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
GRAFANA_API_TOKEN:  104 bytes

Première victoire :)

RBAC, again

On peut s’attaquer à la deuxième partie du problème : comment récupérer puis sauvegarder les JSON ?

Ben, avec une nouvelle couche de moindres privilèges, pardi !!

Vous connaissez la musique maintenant. On crée un ServiceAccount, on ne lui donne le droit qu’à lire les Secrets qui nous intéressent via un Role / RoleBinding pour ne pas faciliter le travail d’un attaquant qui aurait compromis mon Pod.

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: grafana-backup-cron-sa
  namespace: grafana
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: grafana
  name: grafana-backup-cron-role
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["grafana-backup", "grafana-backup-s3-credentials"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: grafana-backup-cron-rolebinding
  namespace: grafana
subjects:
- kind: ServiceAccount
  name: grafana-backup-cron-sa
  namespace: grafana
roleRef:
  kind: Role
  name: grafana-backup-cron-role
  apiGroup: rbac.authorization.k8s.io

Petite subtilité, ici, je vais aussi devoir créer un Secret supplémentaire pour que mon container puisse s’authentifier sur le bucket S3 sur lequel je vais déposer mes JSONs. Je ne rentre pas dans les détails, on sort du cadre de l’article, vous pouvez aller lire la doc officielle de scaleway qui est plutôt bien faite (Using Object Storage with the AWS-CLI).

Voilà à quoi ressemble le Secret grafana-backup-s3-credentials :

apiVersion: v1
kind: Secret
metadata:
  name: grafana-backup-s3-credentials
  namespace: grafana
type: Opaque
stringData:
  # le Secret se compose de 2 entrées, qui seront montés comme des fichiers textes 
  # dans le container ; pratique pour des fichiers de config avec des secrets !!
  credentials: |
    [default]
    aws_access_key_id=SCxxxxxxxxxxxxxxxxxx
    aws_secret_access_key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx    
  config: |
    [default]
    region = fr-par
    output = json
    services = scw-fr-par
    [services scw-fr-par]
    s3 =
      endpoint_url = https://s3.fr-par.scw.cloud    

Et pour finir, on veut donc créer une tâche planifiée, qui va s’occuper de réaliser notre sauvegarde régulièrement. Dans Kubernetes, il s’agit de l’objet CronJob, qui va lancer périodiquement des Job (le même qu’on a vu précédemment).

---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: grafana-backup-cronjob
  namespace: grafana
spec:
  schedule: "0 */2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: grafana-backup-cron-sa
          containers:
          - name: grafana-backup
            image: nixery.dev/shell/curl/jq/awscli:latest
            volumeMounts:
            - name: grafana-backup-s3
              mountPath: /.aws/credentials
              subPath: credentials
            - name: grafana-backup-s3
              mountPath: /.aws/config
              subPath: config
          restartPolicy: OnFailure
            env:
            - name: GRAFANA_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: grafana-backup
                  key: GRAFANA_API_TOKEN
            command:
            - /bin/sh
            - -c
            - |
              # Ici, on récupère un gros JSON listant les dashboards et on utilise jq pour extraire l'identifiant unique "uid"
              dashboards=$(curl -s -H "Authorization: Bearer $GRAFANA_API_TOKEN" 'http://grafana:80/api/search?query=&type=dash-db' | jq -r '.[] | .uid')

              # On boucle ensuite sur la liste des uid
              for uid in $dashboards; do
                # On récupère le JSON qu'on copie en local
                dashboard_json=$(curl -s -H "Authorization: Bearer $GRAFANA_API_TOKEN" http://grafana:80/api/dashboards/uid/$uid)
                echo "$dashboard_json" > /tmp/dashboard-$uid.json

                # Puis on utilise aws-cli pour uploader le fichier sur notre bucket s3
                aws s3 cp /tmp/dashboard-$uid.json s3://grafana-backup/grafana-backups/dashboard-$uid.json --profile default
              done              
          volumes:
          - name: grafana-backup-s3
            secret:
              secretName: secret-grafana-backup-credentials

Toutes les deux heures, nos dashboards seront tous sauvegardés sur notre bucket chez Scaleway. Si la fonction de versioning a été activé sur le bucket, on pourra même garder un historique des versions.

Deuxième victoire :)

Conclusion

Ok, écrire ces scripts et les manifests qui vont avec a surement pris un peu plus de temps que d’aller manuellement créer un token dans l’UI de Grafana, puis de le donner en dur dans un gros script bash.

Cependant, il y a quand même quelques différences avec cette approche :

  1. A part pour les crédentials du bucket S3 (et encore on aurait pu utiliser minIO dans Kubernetes), on a manipulé aucun identifiant. Toute cette gestion des Secrets est déléguée à Kubernetes, loin de l’admin (avec les limitations que ça peut avoir). On n’aura pas de leaks sur github de secrets git-és par erreur et si on gère son RBAC correctement, seuls les Pods qui sont habilités à lire ses Secrets pourront le faire.
  2. Tout est géré “as-code” et automatisé. Le faire une fois à la main va plus vite. Le faire 100 fois, car on a 100 clusters identiques, c’est quand même dommage.
  3. On profite des fonctions de Kubernetes de gestion des erreurs et de retries des Jobs et des Cronjobs. C’est du code (bash ou autre) en moins à gérer si on veut fiabiliser l’outil dans une utilisation plus “industrielle”.

Que je vous aie convaincu ou pas, au-delà de l’exemple, le but était surtout d’introduire quelques concepts de Kubernetes (le RBAC et les Jobs, comment on construit des manifests, …) et j’espère que ça vous a plu.

Bonus

Si vous trouvez ces scripts trop limités pour vos besoins, vous pouvez aller voir les outils suivants, plus poussés. Attention cependant, certains ne sont pas/plus maintenu, et je n’ai pas audité le code. A vos risques et périls donc :

Licensed under CC BY-SA 4.0

Vous aimez ce blog ou cet article ? Partagez-le avec vos amis !   Twitter Linkedin email Facebook

Vous pouvez également vous abonner à la mailing list des articles ici

L'intégralité du contenu appartenant à Denis Germain (alias zwindler) présent sur ce blog, incluant les textes, le code, les images, les schémas et les supports de talks de conf, sont distribués sous la licence CC BY-SA 4.0.

Les autres contenus (thème du blog, police de caractères, logos d'entreprises, articles invités...) restent soumis à leur propre licence ou à défaut, au droit d'auteur. Plus d'informations dans les Mentions Légales

Généré avec Hugo
Thème Stack conçu par Jimmy