ProHoster > Blog > administration > Aller? Frapper! Rencontrez l'opérateur shell (revue et reportage vidéo de KubeCon EU'2020)
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.
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).
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).
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.
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).
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.
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:
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 :
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):
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.
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.
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) :
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 :
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.
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.
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.
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.
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.
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.