Paul Brissaud

Pourquoi devez-vous utiliser les opérateurs Kubernetes ?

Devops
Source photo : Unsplash

Kubernetes n'est pas seulement une solution pour mettre en cluster des machines exécutant des conteneurs ; il renferme tout un tas de composants notamment son API qui peut être étendu. Ces extensions appelées opérateur permettent de rajouter une couche d'abstraction et de pouvoir déployer facilement des applications complexes. Aujourd'hui, nous allons nous intéresser aux opérateurs et pourquoi vous devez absolument les utiliser à travers un exemple de déploiement de Prometheus.

Toute est une question de ressource

Quand on pense à Kubernetes, les termes de "Pod", "Ingress" ou "PersistentVolumeClaim" reviennent très souvent. Ce sont tous des ressources et leur définitions (c'est-à-dire leur schéma) sont renfermées dans le code source de Kubernetes. Ces ressources sont toutes centraliser en groupe d'API qui peuvent être divisés selon leur version.

kind: Job
apiVersion: batch/v1
metadata:
  name: example-job
spec:
  ...

Dans l’exemple ci-dessus, on peut voir que le type de resource appelé “Job” appartient au groupe d’API “batch/v1”.

Les opérateurs permettent de créer de nouveaux groupes et de nouvelles définition de ressources appelées CustomRessourceDefinition (CRD)

Déploiement de Prometheus sans Opérateur

Prometheus est un logiciel libre de surveillance informatique et générateur d'alertes. Il enregistre des métriques en temps réel dans une base de données de séries temporelles (avec une capacité d'acquisition élevée) en se basant sur le contenu de point d'entrée exposé à l'aide du protocole HTTP

Pour déployer, une instance simple de Prometheus, il nous faut au minimum définir plusieurs ressources :

  • Un ClusterRole et un ClusterRoleBinding : donnant des autorisations supplémentaires à Prometheus pour qu'il récupère des métriques de Kubernetes
  • Une ConfigMap : définissant les points d'entrées où venir chercher les données
  • Un Deployment: déployant le serveur Prometheus
---
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
---
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: 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: 'kubernetes-apiservers'
        kubernetes_sd_configs:
        
        - role: endpoints
        scheme: https
        tls_config:
          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
        relabel_configs:
        - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
          action: keep
          regex: default;kubernetes;https
      - job_name: 'kubernetes-nodes'
        scheme: https
        tls_config:
          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
        kubernetes_sd_configs:
        - role: node
        relabel_configs:
        - action: labelmap
          regex: __meta_kubernetes_node_label_(.+)
        - target_label: __address__
          replacement: kubernetes.default.svc:443
        - source_labels: [__meta_kubernetes_node_name]
          regex: (.+)
          target_label: __metrics_path__
          replacement: /api/v1/nodes/${1}/proxy/metrics     
      
      - job_name: 'kubernetes-pods'
        kubernetes_sd_configs:
        - role: pod
        relabel_configs:
        - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
          action: keep
          regex: true
        - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
          action: replace
          target_label: __metrics_path__
          regex: (.+)
        - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
          action: replace
          regex: ([^:]+)(?::\d+)?;(\d+)
          replacement: $1:$2
          target_label: __address__
        - action: labelmap
          regex: __meta_kubernetes_pod_label_(.+)
        - source_labels: [__meta_kubernetes_namespace]
          action: replace
          target_label: kubernetes_namespace
        - source_labels: [__meta_kubernetes_pod_name]
          action: replace
          target_label: kubernetes_pod_name
      
      - job_name: 'kubernetes-cadvisor'
        scheme: https
        tls_config:
          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
        kubernetes_sd_configs:
        - role: node
        relabel_configs:
        - action: labelmap
          regex: __meta_kubernetes_node_label_(.+)
        - target_label: __address__
          replacement: kubernetes.default.svc:443
        - source_labels: [__meta_kubernetes_node_name]
          regex: (.+)
          target_label: __metrics_path__
          replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
      
      - job_name: 'kubernetes-service-endpoints'
        kubernetes_sd_configs:
        - role: endpoints
        relabel_configs:
        - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
          action: keep
          regex: true
        - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
          action: replace
          target_label: __scheme__
          regex: (https?)
        - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
          action: replace
          target_label: __metrics_path__
          regex: (.+)
        - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
          action: replace
          target_label: __address__
          regex: ([^:]+)(?::\d+)?;(\d+)
          replacement: $1:$2
        - action: labelmap
          regex: __meta_kubernetes_service_label_(.+)
        - source_labels: [__meta_kubernetes_namespace]
          action: replace
          target_label: kubernetes_namespace
        - source_labels: [__meta_kubernetes_service_name]
          action: replace
          target_label: kubernetes_name
---
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:
      containers:
        - name: prometheus
          image: prom/prometheus
          args:
            - "--storage.tsdb.retention.time=12h"
            - "--config.file=/etc/prometheus/prometheus.yml"
            - "--storage.tsdb.path=/prometheus/"
          ports:
            - containerPort: 9090
          resources:
            requests:
              cpu: 500m
              memory: 500M
            limits:
              cpu: 1
              memory: 1Gi
          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: {}

Une fois déployée, je vais utiliser l’API de Prometheus pour checker si la configuration s’est bien déployée et si tous les point d’entrées sont actifs :

$ curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | .labels.job + ": " + .health'

"kubernetes-apiservers: up"
"kubernetes-cadvisor: up"
"kubernetes-nodes: up"
"kubernetes-service-endpoints: up"

La maintenance de ce déploiement

Vous êtes surement en train d'écrire un commentaire en me demandant où est le soucis avec ce déploiement. Je vous répondrais que, pour l'instant, il y n'en a pas. Pour l'instant .... Car imaginons que votre chef vous demande de rajouter une règle d'alerte ou un nouveau point d'entrée.

PANIQUE A BORD ! Pas de suite, mais vous allez comprendre votre douleur plus tard. Vous allez donc modifier votre ConfigMap pour ajouter ce qu'il faut. Cependant, pour que Prometheus soit au courant de cette modification, vous devez supprimez manuellement les pods ; en espérant que vous avez configurés un stockage persistent (ce qui n'est pas le cas dans mon exemple) sinon adieu les données !

Bon c'était galère mais pas insurmontable. Maintenant, imaginons que vous devez rajouter les points d'entrée de toutes les applications de vos développeurs. Et oui, vous êtes le seul à pouvoir le faire car la ConfigMap est dans un namespace où seul vous et votre responsable avez les droits pour la modifier. Vous commencez à comprendre la lourdeur de la tâche ? Toute la configuration va être centralisée en un seul point, vous devez la modifier manuellement à chaque changement, etc... C'est là où les opérateurs vont vous être utiles.

Un opérateur, qu'est-ce que c'est ?

Un opérateur est composé de deux éléments :

  • Des définitions de ressources personnalisées (CRDs) qui vont vous permettent de déclarer de nouvelles ressources
  • Un contrôleur qui va observer les évènements liés à ces nouvelles ressources (création, modification, suppression) et va réagir en conséquence.
Fonctionnement d'un opérateur Kubernetes. Source: https://blog.container-solutions.com/kubernetes-operators-explained
Fonctionnement d'un opérateur Kubernetes. Source: https://blog.container-solutions.com/kubernetes-operators-explained

L'opérateur kube-prometheus pour vous sauver

Revenons à notre exemple, et voyons comment un opérateur appelé 'kube-prometheus' va sauver votre vie ! Pour simplifier encore plus le déploiement, je vais utiliser la helm chart fournie par Bitnami mais j'aurais pu le faire manuellement.

$ helm repo add bitnami https://charts.bitnami.com/bitnami
$ helm install prometheus bitnami/kube-prometheus -n monitoring 

Et là pouf, en quelques secondes vous avez déployé le contrôleur de l'opérateur, un Prometheus et même un Alertmanager ! Vous avez même en prime un déploiement de Node Exporter et de Kube-stats-metrics mais rien à voir avec l'opérateur en lui même !

Je peux refaire la même commande que dans la partie précédente pour voir si Prometheus est bien déployé et commence à récupérer les données :

$ curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | .labels.job + ": " + .health'

"prometheus-kube-prometheus-alertmanager: up"
"apiserver: up"
"prometheus-kube-prometheus-coredns: up"
"prometheus-kube-prometheus-kube-controller-manager: up"
"prometheus-kube-prometheus-kube-proxy: up"
"prometheus-kube-prometheus-kube-scheduler: up"
"kubelet: up"
"kubelet: up"
"prometheus-kube-prometheus-operator: up"
"prometheus-kube-prometheus-prometheus: up"
"prometheus-kube-state-metrics: up"
"node-exporter: up"

Cet opérateur ajoute de nombreuses nouvelles ressources comme 'Prometheus'. Vous pouvez la voir en tapant cette commande kubectl :

$ kubectl get prometheus -n monitoring 
NAME                                    VERSION   REPLICAS   AGE
prometheus-kube-prometheus-prometheus             1           2m

$ kubectl get statefulsets -n monitoring
NAME                                                   READY   AGE
alertmanager-prometheus-kube-prometheus-alertmanager   1/1     25m
prometheus-prometheus-kube-prometheus-prometheus       1/1     25m

La dernière commande vous permettent de comprendre comment l'opérateur (= le contrôleur) fonctionne : quand une ressource de type 'Prometheus' va apparaitre, il va créer un StatefulSet et pleins de ressources (ConfigMap, Secret, Service) associées. Et dans la ressource 'Prometheus', il y a des champs qui permettent de le configurer comme la période de retention, etc.. Et à chaque changement, l'opérateur va mettre à jour les ressources associées, sans redémarrage nécessaire. Egalement, si je supprime le StatefulSet, l'opérateur va immédiatement en récréer un.

Une autre nouvelle ressource intéressante est le ServiceMonitor. Il va permettre de définir les point d'entrées dynamiquement sans avoir à gérer une configuration centrale. Pour vous montrer un exemple, je vais déployer une application de test dans le namespace 'web'. Cette application expose des métriques au format Prometheus sur le port 8081 et sous l'URL /metrics. Je souhaite que ces métriques soient accessibles dans Prometheus.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-metrics
	namespace: web
  labels:
    app: golang
spec:
  replicas: 1
  selector:
    matchLabels:
      app: golang
  template:
    metadata:
      labels:
        app: golang
    spec:
      containers:
      - name: go-metrics
        image: paulbrissaud/go-metrics
        ports:
        - containerPort: 8081
---
apiVersion: v1
kind: Service
metadata:
  name: go-metrics-service
	namespace: web
	labels:
    app: golang
spec:
  selector:
    app: golang
  ports:
    - name: http
      protocol: TCP
      port: 8081
      targetPort: 8081

Pour ce faire, je vais donc déployer un serviceMonitor dans le même namespace. J'indique qu'un service avec le label 'app=golang' expose des métriques Prometheus sur le port nommé 'http' et sous le chemin '/metrics'

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: go-metrics
  namespace: web
spec:
  selector:
    matchLabels:
      app: golang
  endpoints:
  - port: http
		path: /metrics

En effectuant la même requête à l’API de Prometheus, je vois un nouveau point d'entrée : celui de l'application ! Pour la petite explication du fonctionnement, chaque serviceMonitor écrit dans la configuration "centrale" de Prometheus et un autre composant permet au Pod de recharger la configuration sans avoir à le supprimer. Grâce au ServiceMonitor, je peux laisser les développeurs s'occuper du monitoring de leur propre application sans aucune intervention de ma part.

$ curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | .labels.job + ": " + .health'

"prometheus-kube-prometheus-alertmanager: up"
"apiserver: up"
"prometheus-kube-prometheus-coredns: up"
"prometheus-kube-prometheus-kube-controller-manager: up"
"prometheus-kube-prometheus-kube-proxy: up"
"prometheus-kube-prometheus-kube-scheduler: up"
"kubelet: up"
"kubelet: up"
"prometheus-kube-prometheus-operator: up"
"prometheus-kube-prometheus-prometheus: up"
"prometheus-kube-state-metrics: up"
"node-exporter: up"
"go-metrics-service: up" #L'application 

Il existe des dizaines d'autres opérateurs permettant de faciliter le déploiement et le maintien de bases de données, de composants Kubernetes ou d'outils de monitoring. J'espère vous avoir donner l'envie de franchir le pas, vous n'allez pas le regretter ! Et puis, s'il n'existe pas encore d'opérateur pour vos besoins, leur création est relativement simple !