Guillaume Fenollar DevOps et SysAdmin Freelance

Guillaume FENOLLAR

Administrateur Linux | DevOps Indépendant

− Nouméa −

Introduction à l'API Python de Kubernetes (partie 1)

Si comme moi vous n'êtes pas tentés d'apprendre Go, que Python couvre tous vos besoins, et que vous utilisez Kubernetes ou autre plateforme l'utilisant (GKE, EKS...), alors ce qui suit va probablement vous intéresser. À noter que cette introduction sur la librairie python a été écrite en avril 2019, et qu'elle va forcément évoluer dans les temps à venir, auquel cas l'article qui suit sera probablement obsolète.

Cet article demande des connaissances de base en Python bien entendu, mais également en utilisation de Kubernetes. Si vous n’avez jamais touché à un kubectl, vous risquez d’être vite perdu.

Kubernetes fournit une API en constante évolution et utilise le standard OpenAPI. Cela permet donc la génération des librairies listées ici, comme Java, Javascript ou encore Python. Il existe également d’autres librairies Python maintenues par la communauté mais je ne me suis pas encore trop penché sur le sujet, ne voyant pas pour l’instant de librairie au dessus du lot.

Le gros intérêt d’une librairie générée est qu’elle restera facilement maintenue et suivra les versions d’API de Kubernetes au fur et à mesure des releases. L’avantage est que ça donne accès aux objets K8S tels qu’écrits dans la doc de référence de l’API Kubernetes. Le gros inconvénient, et vous allez le voir par la pratique bientôt, c’est que ça ne donne qu’accès qu’aux objets K8S tels qu’écrits dans la doc de référence de l’API Kubernetes. Je veux dire par là qu’il s’agit d’une librairie très bas niveau qui ne présente pas vraiment de fonction helper facilitant le travail du développeur derrière. Si vous voulez créer un Pod, vous allez devoir instancier un objet de type V1Pod, et peupler ses metadata, ses différents champs… manuellement. On est loin du côté pratique d’un kubectl run busybox --image=busybox qui démarre un déploiement, avec des labels préremplis, etc…

Également, Kubernetes présente tout un tas d’APIs décorrelées les uns des autres (le champ .apiVersion), par exemple les Pods sont gérés dans l’API Core (aussi appelé v1), alors que les deployments le sont dans l’API apps/v1. Il va donc falloir accéder aux différentes ressources grâce à des objets venants d’API différentes. Pour l’illuster, rien de tel qu’un petit exemple.

Installation

pip install kubernetes

That’s all, folks !

Charger la configuration kubeconfig

Ce package kubernetes offre un subpackage “config” chargé de gérer l’import de configuration. Il y existe des fonctions assez pratiques pour charger la configuration locale afin d’avoir accès à une API Kubernetes.

config.load_kube_config() →  Charge un fichier kubeconfig, 
                             son emplacement étant écrasé par la variable d'env KUBECONFIG.
config.load_incluster_config() →  Cherche un token de service account monté dans le pod
                                  au chemin standard /var/run/secrets/kubernetes.io/serviceaccount/

Ces deux fonctions permettent donc de faciliter la connexion car s’adaptent au contexte. Soit le script est lancé depuis une machine type d’admin k8s qui a déjà un fichier kubeconfig, soit directement dans un pod du cluster, auquel cas la fonction config.load_incluster_config sera toute indiquée. Pouvoir lancer des requêtes à l’API de potentiellement modifier des ressources depuis un pod revient à créer ce qu’on appelle un controller dans Kubernetes :) Au final, ça nous donne un snippet de chargement de configuration tel que celui-ci :

from kubernetes import config
import os

if os.path.exists('/var/run/secrets/kubernetes.io/serviceaccount/token'):
    config.load_incluster_config()
else:
    try:
        config.load_kube_config()
    except:
        print('Unable to find cluster configuration')
        exit(1)

À noter cependant que load_kube_config ne sait pas, à l’heure où ces lignes sont écrites, gérer la variable KUBECONFIG quand elle représente plusieurs path séparés par deux points (:), ce qui est une fonction supportée par kubectl depuis un long moment. Il est alors possible de définir son propre chemin de fichier de config de la façon suivante :

# Le contexte en second argument est optionnel, le current-context sera alors renvoyé
config.load_kube_config('path/to/kubeconfig', 'context')

Lister des ressources

Voici quelques exemples qui vont aider à comprendre comment utiliser l’API. Les noms des APIs côté librairie python ainsi que les méthodes affectées aux classes des ressources sont visibles à ce lien

# Listing de tous les namespaces
from kubernetes import client

v1 = client.CoreV1Api()
all_ns = v1.list_namespace()
for ns in all_ns.items:
    print(ns.metadata.name)

Ici, la méthode list_namespace renvoie un objet de type NamespaceList, comme l’API standard finalement. Dans cet objet se trouve une clé ‘items’ sur laquel on peut itérer pour avoir les objets namespaces au complet, avec les mêmes champs que si on avait récupéré les objets avec kubectl, encore une fois normal grâce à OpenAPI.

Maintenant, listing de tous les pods dans kube-system avec affichage de leur IP

from kubernetes import client

v1 = client.CoreV1Api()
all_pods = v1.list_namespaced_pod(namespace="kube-system")
listformat="{:.<64}{:16}"
print(listformat.format('POD NAME', 'IP'))
for pod in all_pods.items:
    pod_name = pod.metadata.name
    pod_ip = pod.status.pod_ip
    print(listformat.format(pod_name, pod_ip))

Listing de tous les deploiements, dans tous les namespaces, qui n’ont pas de replica actif. On voit ici qu’on doit faire appel à une autre API, apps/v1 :

from kubernetes import client
appsv1 = client.AppsV1Api()

all_deploy = appsv1.list_deployment_for_all_namespaces()
print('UNAVAILABLE DEPLOYMENTS')
for deploy in all_deploy.items:
    if not deploy.status.ready_replicas:
        print(deploy.metadata.name)
Il serait alors facile de déclencher une action, comme une remontée d’alerte, si cet état subsiste plus de 5 minutes, par exemple.

Enfin, un exemple un peu plus complexe mais plus utile, afficher les pods avec conteneurs ayant redémarré au moins une fois et le nombre de redémarrage

from kubernetes import client

v1 = client.CoreV1Api()
all_pods = v1.list_pod_for_all_namespaces()
res = {}
for pod in all_pods.items:
    pod_ns = pod.metadata.namespace
    pod_name = pod.metadata.name
    for container in pod.status.container_statuses:
        cont_name = container.name
        if container.restart_count:
            if not pod_ns in res:
              res[pod_ns] = {}
            if not pod_name in res[pod_ns]:
              res[pod_ns][pod_name] = {}
            res[pod_ns][pod_name][container.name] = container.restart_count

if not res: 
    print('No container has restarted')
    exit(0)
listformat="{0:<16}{1:<64}{2:>8} {3:<32}"
print(listformat.format('NAMESPACE','POD','RESTARTS','CONTAINER'))
for ns in res:
    print(listformat.format(ns,'','',''))
    for pod in res[ns]:
        print(listformat.format('',pod,'',''))
        for container in res[ns][pod]:
            print(listformat.format('','',container,res[ns][pod][container]))

Watch d’évenements

J’ai un peu plus tôt parlé de Controller Kubernetes. Les Controllers sont omniprésents dans Kubernetes et s’occupent de toutes les boucles permettant de faire en sorte que l’état désiré (le contenu de la base ETCd) soit cohérent avec l’état des objets. Entre le controller manager qui implémente tous les controllers natifs, le scheduler (qui est une sorte de controller finalement), les ingress controller et j’en passe, ces processus forme la colonne vertébrale d’un cluster. Le point commun entre tous ces controller est qu’ils “Watch” des ressources, ils restent en attente d’évènements et les traite à la volée pour répondre à un besoin précis. La librairie nous permet de watch l’état d’une ressource d’une manière assez simple:

from kubernetes import watch
w = watch.Watch()

La méthode “stream” de l’objet retourné va alors nous créer un générateur sur la méthode de l’API demandé. Exemple:

from kubernetes import client, watch

w = watch.Watch()
v1 = client.CoreV1Api()
for ns in w.stream(v1.list_namespace):
    #Le code suivant va être exécuté à chaque namespace détecté, puis à chaque évènement touchant un namespace
    obj = ns['object']
    print("{}: {}".format(ns['type'], obj.metadata.name))

Laissez tourner ce code et créez puis supprimer un nouveau namespace sur votre cluster pour voir les nouveaux évènements passer !

On pourrait alors faire un watcher qui enverrait une notification à l’administrateur pour toute création de namespace, pourquoi pas.

C’est déjà la fin de cette petite introduction à la librairie officielle de kubernetes en Python. Dans une deuxième partie, j’aborderai la création de ressources et des exemples un peu plus poussés.