Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Cette année, la principale conférence européenne Kubernetes - KubeCon + CloudNativeCon Europe 2020 - était virtuelle. Cependant, un tel changement de format ne nous a pas empêché de livrer notre rapport tant attendu « Go ? Frapper! Rencontrez l'opérateur Shell » dédié à notre projet Open Source opérateur shell.

Cet article, inspiré de l'exposé, présente une approche pour simplifier le processus de création d'opérateurs pour Kubernetes et montre comment vous pouvez créer le vôtre avec un minimum d'effort à l'aide d'un opérateur shell.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Présentation vidéo du reportage (~23 minutes en anglais, sensiblement plus informatif que l'article) et l'extrait principal sous forme de texte. Aller!

Chez Flant, nous optimisons et automatisons constamment tout. Aujourd'hui, nous allons parler d'un autre concept passionnant. Rencontrer: scripts shell natifs du cloud!

Cependant, commençons par le contexte dans lequel tout cela se produit : Kubernetes.

API et contrôleurs Kubernetes

L'API de Kubernetes peut être représentée comme une sorte de serveur de fichiers avec des répertoires pour chaque type d'objet. Les objets (ressources) sur ce serveur sont représentés par des fichiers YAML. De plus, le serveur dispose d'une API de base qui permet de faire trois choses :

  • recevoir ressource par son type et son nom ;
  • changer ressource (dans ce cas, le serveur ne stocke que les objets « corrects » - tous ceux mal formés ou destinés à d'autres répertoires sont supprimés) ;
  • suivre pour la ressource (dans ce cas, l'utilisateur reçoit immédiatement sa version actuelle/mise à jour).

Ainsi, Kubernetes agit comme une sorte de serveur de fichiers (pour les manifestes YAML) avec trois méthodes de base (oui, en fait il y en a d'autres, mais nous les omettrons pour l'instant).

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Le problème est que le serveur ne peut stocker que des informations. Pour que cela fonctionne, vous avez besoin contrôleur - le deuxième concept le plus important et fondamental dans le monde de Kubernetes.

Il existe deux principaux types de contrôleurs. Le premier prend les informations de Kubernetes, les traite selon une logique imbriquée et les renvoie aux K8. Le second prend les informations de Kubernetes, mais, contrairement au premier type, modifie l'état de certaines ressources externes.

Examinons de plus près le processus de création d'un déploiement dans Kubernetes :

  • Contrôleur de déploiement (inclus dans kube-controller-manager) reçoit des informations sur le déploiement et crée un ReplicaSet.
  • ReplicaSet crée deux réplicas (deux pods) sur la base de ces informations, mais ces pods ne sont pas encore planifiés.
  • Le planificateur planifie les pods et ajoute des informations sur les nœuds à leurs YAML.
  • Les Kubelets apportent des modifications à une ressource externe (par exemple Docker).

Ensuite, toute cette séquence se répète dans l'ordre inverse : le kubelet vérifie les conteneurs, calcule l'état du pod et le renvoie. Le contrôleur ReplicaSet reçoit l'état et met à jour l'état du jeu de réplicas. La même chose se produit avec le contrôleur de déploiement et l'utilisateur obtient enfin le statut mis à jour (actuel).

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Opérateur de shell

Il s'avère que Kubernetes repose sur le travail conjoint de différents contrôleurs (les opérateurs Kubernetes sont également des contrôleurs). La question se pose, comment créer son propre opérateur avec un minimum d'effort ? Et voilà celui que nous avons développé vient à la rescousse opérateur shell. Il permet aux administrateurs système de créer leurs propres instructions en utilisant des méthodes familières.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Exemple simple : copier des secrets

Regardons un exemple simple.

Disons que nous avons un cluster Kubernetes. Il a un espace de noms default avec un secret mysecret. De plus, il existe d'autres espaces de noms dans le cluster. Certains d’entre eux portent une étiquette spécifique. Notre objectif est de copier Secret dans des espaces de noms avec une étiquette.

La tâche est compliquée par le fait que de nouveaux espaces de noms peuvent apparaître dans le cluster, et certains d'entre eux peuvent avoir cette étiquette. En revanche, lorsque le label est supprimé, Secret doit également être supprimé. En plus de cela, le Secret lui-même peut également changer : dans ce cas, le nouveau Secret doit être copié dans tous les espaces de noms avec des étiquettes. Si Secret est accidentellement supprimé dans un espace de noms, notre opérateur doit le restaurer immédiatement.

Maintenant que la tâche a été formulée, il est temps de commencer à l'implémenter à l'aide de l'opérateur shell. Mais d’abord, il convient de dire quelques mots sur l’opérateur shell lui-même.

Comment fonctionne l'opérateur shell

Comme les autres charges de travail dans Kubernetes, l'opérateur shell s'exécute dans son propre pod. Dans ce module du répertoire /hooks les fichiers exécutables sont stockés. Il peut s'agir de scripts en Bash, Python, Ruby, etc. Nous appelons ces fichiers exécutables des hooks (crochets).

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

L'opérateur Shell s'abonne aux événements Kubernetes et exécute ces hooks en réponse aux événements dont nous avons besoin.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Comment l’opérateur shell sait-il quel hook exécuter et quand ? Le fait est que chaque crochet comporte deux étapes. Au démarrage, l'opérateur shell exécute tous les hooks avec un argument --config C'est l'étape de configuration. Et après cela, les hooks sont lancés de manière normale - en réponse aux événements auxquels ils sont attachés. Dans ce dernier cas, le hook reçoit le contexte de liaison (contexte contraignant) - des données au format JSON, dont nous parlerons plus en détail ci-dessous.

Créer un opérateur dans Bash

Nous sommes maintenant prêts pour la mise en œuvre. Pour ce faire, nous devons écrire deux fonctions (d'ailleurs, nous recommandons bibliothèque shell_lib, ce qui simplifie grandement l'écriture des hooks dans Bash) :

  • le premier est nécessaire pour l'étape de configuration - il affiche le contexte de liaison ;
  • le second contient la logique principale du hook.

#!/bin/bash

source /shell_lib.sh

function __config__() {
  cat << EOF
    configVersion: v1
    # BINDING CONFIGURATION
EOF
}

function __main__() {
  # THE LOGIC
}

hook::run "$@"

La prochaine étape consiste à décider de quels objets nous avons besoin. Dans notre cas, nous devons suivre :

  • source secrète pour les modifications ;
  • tous les espaces de noms du cluster, afin que vous sachiez lesquels sont associés à une étiquette ;
  • ciblez les secrets pour vous assurer qu’ils sont tous synchronisés avec le secret source.

Abonnez-vous à la source secrète

La configuration de la liaison est assez simple. Nous indiquons que nous sommes intéressés par Secret avec le nom mysecret dans l'espace de noms default:

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

function __config__() {
  cat << EOF
    configVersion: v1
    kubernetes:
    - name: src_secret
      apiVersion: v1
      kind: Secret
      nameSelector:
        matchNames:
        - mysecret
      namespace:
        nameSelector:
          matchNames: ["default"]
      group: main
EOF

Par conséquent, le hook sera déclenché lorsque le secret source changera (src_secret) et recevez le contexte de liaison suivant :

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Comme vous pouvez le voir, il contient le nom et l'intégralité de l'objet.

Garder une trace des espaces de noms

Vous devez maintenant vous abonner aux espaces de noms. Pour ce faire, nous spécifions la configuration de liaison suivante :

- name: namespaces
  group: main
  apiVersion: v1
  kind: Namespace
  jqFilter: |
    {
      namespace: .metadata.name,
      hasLabel: (
       .metadata.labels // {} |  
         contains({"secret": "yes"})
      )
    }
  group: main
  keepFullObjectsInMemory: false

Comme vous pouvez le constater, un nouveau champ est apparu dans la configuration avec le nom jqFilter. Comme son nom l'indique, jqFilter filtre toutes les informations inutiles et crée un nouvel objet JSON avec les champs qui nous intéressent. Un hook avec une configuration similaire recevra le contexte de liaison suivant :

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Il contient un tableau filterResults pour chaque espace de noms du cluster. Variable booléenne hasLabel indique si une étiquette est attachée à un espace de noms donné. Sélecteur keepFullObjectsInMemory: false indique qu'il n'est pas nécessaire de conserver des objets complets en mémoire.

Suivi des secrets des cibles

Nous souscrivons à tous les secrets pour lesquels une annotation est spécifiée managed-secret: "yes" (ce sont notre objectif dst_secrets):

- name: dst_secrets
  apiVersion: v1
  kind: Secret
  labelSelector:
    matchLabels:
      managed-secret: "yes"
  jqFilter: |
    {
      "namespace":
        .metadata.namespace,
      "resourceVersion":
        .metadata.annotations.resourceVersion
    }
  group: main
  keepFullObjectsInMemory: false

Dans ce cas, jqFilter filtre toutes les informations à l'exception de l'espace de noms et du paramètre resourceVersion. Le dernier paramètre a été passé à l'annotation lors de la création du secret : il permet de comparer les versions des secrets et de les maintenir à jour.

Un hook configuré de cette manière recevra, une fois exécuté, les trois contextes de liaison décrits ci-dessus. Ils peuvent être considérés comme une sorte d’instantané (instantané) grappe.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Sur la base de toutes ces informations, un algorithme de base peut être développé. Il parcourt tous les espaces de noms et :

  • si hasLabel questions true pour l'espace de noms actuel :
    • compare le secret global avec le secret local :
      • s'ils sont identiques, cela ne fait rien ;
      • s'ils diffèrent - exécute kubectl replace ou create;
  • si hasLabel questions false pour l'espace de noms actuel :
    • s'assure que Secret n'est pas dans l'espace de noms donné :
      • si le Secret local est présent, supprimez-le en utilisant kubectl delete;
      • si le secret local n'est pas détecté, il ne fait rien.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Implémentation de l'algorithme dans Bash vous pouvez télécharger dans notre référentiels avec exemples.

C'est ainsi que nous avons pu créer un simple contrôleur Kubernetes en utilisant 35 lignes de configuration YAML et environ la même quantité de code Bash ! Le travail de l'opérateur shell est de les relier entre eux.

Cependant, la copie de secrets n'est pas le seul domaine d'application de l'utilitaire. Voici quelques exemples supplémentaires pour montrer de quoi il est capable.

Exemple 1 : Apporter des modifications à ConfigMap

Examinons un déploiement composé de trois pods. Les pods utilisent ConfigMap pour stocker certaines configurations. Lorsque les pods ont été lancés, ConfigMap était dans un certain état (appelons-le v.1). Par conséquent, tous les pods utilisent cette version particulière de ConfigMap.

Supposons maintenant que le ConfigMap ait changé (v.2). Cependant, les pods utiliseront la version précédente de ConfigMap (v.1) :

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Comment puis-je les amener à passer au nouveau ConfigMap (v.2) ? La réponse est simple : utilisez un modèle. Ajoutons une annotation de somme de contrôle à la section template Configurations de déploiement :

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

En conséquence, cette somme de contrôle sera enregistrée dans tous les pods, et elle sera la même que celle du Déploiement. Il ne vous reste plus qu'à mettre à jour l'annotation lorsque le ConfigMap change. Et l'opérateur shell s'avère utile dans ce cas. Il ne vous reste plus qu'à programmer un hook qui s'abonnera au ConfigMap et mettra à jour la somme de contrôle.

Si l'utilisateur apporte des modifications au ConfigMap, l'opérateur shell les remarquera et recalculera la somme de contrôle. Après quoi la magie de Kubernetes entrera en jeu : l'orchestrateur va tuer le pod, en créer un nouveau, attendre qu'il devienne Ready, et passe au suivant. En conséquence, le déploiement se synchronisera et passera à la nouvelle version de ConfigMap.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Exemple 2 : Utilisation de définitions de ressources personnalisées

Comme vous le savez, Kubernetes vous permet de créer des types d'objets personnalisés. Par exemple, vous pouvez créer un type MysqlDatabase. Disons que ce type a deux paramètres de métadonnées : name и namespace.

apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
  name: foo
  namespace: bar

Nous disposons d'un cluster Kubernetes avec différents espaces de noms dans lequel nous pouvons créer des bases de données MySQL. Dans ce cas, l'opérateur shell peut être utilisé pour suivre les ressources MysqlDatabase, en les connectant au serveur MySQL et en synchronisant les états souhaités et observés du cluster.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Exemple 3 : Surveillance du réseau de cluster

Comme vous le savez, utiliser le ping est le moyen le plus simple de surveiller un réseau. Dans cet exemple, nous montrerons comment implémenter une telle surveillance à l'aide d'un opérateur shell.

Tout d’abord, vous devrez vous abonner aux nœuds. L'opérateur shell a besoin du nom et de l'adresse IP de chaque nœud. Avec leur aide, il pingera ces nœuds.

configVersion: v1
kubernetes:
- name: nodes
  apiVersion: v1
  kind: Node
  jqFilter: |
    {
      name: .metadata.name,
      ip: (
       .status.addresses[] |  
        select(.type == "InternalIP") |
        .address
      )
    }
  group: main
  keepFullObjectsInMemory: false
  executeHookOnEvent: []
schedule:
- name: every_minute
  group: main
  crontab: "* * * * *"

Paramètre executeHookOnEvent: [] empêche le hook de s'exécuter en réponse à un événement (c'est-à-dire en réponse à une modification, un ajout ou une suppression de nœuds). Cependant, il va courir (et mettre à jour la liste des nœuds) programmé - toutes les minutes, comme prescrit par le terrain schedule.

Maintenant, la question se pose : comment pouvons-nous exactement connaître des problèmes tels que la perte de paquets ? Jetons un coup d'œil au code :

function __main__() {
  for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
    node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
    node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
    packets_lost=0
    if ! ping -c 1 "$node_ip" -t 1 ; then
      packets_lost=1
    fi
    cat >> "$METRICS_PATH" <<END
      {
        "name": "node_packets_lost",
        "add": $packets_lost,
        "labels": {
          "node": "$node_name"
        }
      }
END
  done
}

Nous parcourons la liste des nœuds, obtenons leurs noms et adresses IP, leur pingons et envoyons les résultats à Prometheus. L'opérateur Shell peut exporter des métriques vers Prometheus, en les enregistrant dans un fichier localisé selon le chemin spécifié dans la variable d'environnement $METRICS_PATH.

comme ça vous pouvez créer un opérateur pour une surveillance simple du réseau dans un cluster.

Mécanisme de file d'attente

Cet article serait incomplet sans décrire un autre mécanisme important intégré à l'opérateur shell. Imaginez qu'il exécute une sorte de hook en réponse à un événement dans le cluster.

  • Que se passe-t-il si, au même moment, quelque chose se produit dans le cluster ? encore une chose événement?
  • L’opérateur shell exécutera-t-il une autre instance du hook ?
  • Que se passe-t-il si, disons, cinq événements se produisent simultanément dans le cluster ?
  • L'opérateur shell les traitera-t-il en parallèle ?
  • Qu’en est-il des ressources consommées telles que la mémoire et le processeur ?

Heureusement, Shell-operator dispose d'un mécanisme de file d'attente intégré. Tous les événements sont mis en file d'attente et traités séquentiellement.

Illustrons cela avec des exemples. Disons que nous avons deux crochets. Le premier événement va au premier crochet. Une fois son traitement terminé, la file d'attente avance. Les trois événements suivants sont redirigés vers le deuxième hook - ils sont supprimés de la file d'attente et y sont entrés dans un « bundle ». C'est hook reçoit un tableau d'événements – ou, plus précisément, un ensemble de contextes contraignants.

Aussi ceux-ci les événements peuvent être combinés en un seul grand. Le paramètre en est responsable group dans la configuration de liaison.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Vous pouvez créer n'importe quel nombre de files d'attente/hooks et leurs différentes combinaisons. Par exemple, une file d'attente peut fonctionner avec deux hooks, ou vice versa.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Tout ce que vous avez à faire est de configurer le champ en conséquence queue dans la configuration de liaison. Si aucun nom de file d'attente n'est spécifié, le hook s'exécute sur la file d'attente par défaut (default). Ce mécanisme de file d'attente vous permet de résoudre complètement tous les problèmes de gestion des ressources lorsque vous travaillez avec des hooks.

Conclusion

Nous avons expliqué ce qu'est un opérateur shell, montré comment il peut être utilisé pour créer rapidement et sans effort des opérateurs Kubernetes et donné plusieurs exemples de son utilisation.

Des informations détaillées sur l'opérateur shell, ainsi qu'un rapide tutoriel sur la façon de l'utiliser, sont disponibles dans le fichier correspondant. dépôts sur GitHub. N'hésitez pas à nous contacter pour toute question : vous pourrez en discuter dans un espace spécial Groupe de télégramme (en russe) ou en ce forum (en anglais).

Et si vous l'avez aimé, nous sommes toujours heureux de voir de nouveaux numéros/RP/stars sur GitHub, où, d'ailleurs, vous pouvez en trouver d'autres projets intéressants. Parmi eux, il convient de souligner opérateur complémentaire, qui est le grand frère de l'opérateur shell. Cet utilitaire utilise les graphiques Helm pour installer des modules complémentaires, peut fournir des mises à jour et surveiller divers paramètres/valeurs des graphiques, contrôle le processus d'installation des graphiques et peut également les modifier en réponse aux événements du cluster.

Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)

Vidéos et diapositives

Vidéo de la performance (~23 minutes) :


Présentation du rapport :

PS

A lire aussi sur notre blog :

Source: habr.com

Ajouter un commentaire