Le blog

L'orchestrateur de conteneurs Kubernetes : introduction

Publié le 19 juillet 2018

#
Les conteneurs et leur orchestration

Il devient de plus en plus courant pour les applications d’être séparées en plusieurs modules indépendants et communiquant entre eux généralement grâce à des API Web ou à des bus de communication tels que Apache Kafka. Cette séparation permet d’isoler de manière sécurisée les modules et de pouvoir déployer un nombre d’instances différent selon les parties de l’application les plus utilisées (scalabilité horizontale). On pense ainsi aux architectures microservices et SOA qui tirent parti de cette séparation. Ces modules indépendants sont construits dans des conteneurs, bien souvent en utilisant Docker.

Afin de simplifier le déploiement et la gestion de ces conteneurs hébergés sur des clouds publics comme Amazon Web Services (AWS) ou Google Cloud Platform (GCP), des clouds privés, ou des machines physiques dédiées (bare-metal), un orchestrateur de conteneurs est utilisé. Il permet également une montée en charge aisée de ces conteneurs, notamment en gérant automatiquement le load-balancing.

Un orchestrateur de conteneurs se démarque de ses concurrents : Kubernetes. K8s (pour les intimes) est une solution open source créée par Google. Il est maintenant disponible en version managée chez les grands noms du cloud. Citons par exemple Azure Kubernetes Service (Microsoft), Google Kubernetes Engine ou Amazon Elastic Container Service for Kubernetes. Il peut également être installé sur des parcs de machines dédiées (in-house). 

k8s

#
Introduction à Kubernetes

Issu de la R&D de Google, Kubernetes est un projet démarré en 2014. Il fait suite au projet Borg, l’ancien système interne de gestion de conteneurs utilisé depuis une dizaine d’années par le mastodonte de Mountain View. Il est écrit en Go. Google s’est basé sur son savoir-faire acquis depuis de nombreuses années dans le domaine des conteneurs, ceux-ci étant utilisés partout dans l’entreprise.

Kubernetes possède un large écosystème. Avec ses 1,5K contributeurs, ses près de 40k stars sur GitHub, ses plus de 110K de followers sur Twitter et ses nombreux meetups à travers le monde (avec Kubecon en tête de liste), Kubernetes s’impose comme l’orchestrateur de référence éclipsant AWS ECS et Docker Swarm. À noter que depuis sa version 2.2, API Platform (le framework API-driven que nous développons) propose un chart Helm (le gestionnaire de paquets de K8s) qui permet  de déployer ses applications sur n’importe quel cluster Kubernetes en un instant. Nous vous invitons à consulter la documentation dédiée pour en savoir plus sur cette nouvelle fonctionnalité.

Le système de configuration de Kubernetes est très flexible et permet notamment de créer un déploiement très facilement pour ensuite l’enrichir et l’améliorer au fur et à mesure. Dans cet article introductif, nous nous intéresserons seulement aux bases. À vous ensuite de rentrer dans les détails des différentes possibilités !

Comprendre Kubernetes, c’est donc connaître ses objets et leur utilité. En voici les principaux :

  • Nodes : les nodes (ou noeuds) correspondent aux machines (bare-metal ou virtuelles) qui exécutent vos conteneurs. Ce n’est pas à vous de vous en soucier lorsque vous utilisez K8s en version managée sur des clouds publics. Il est néanmoins bon de connaître leur existence si vous gérez les machines vous-même (c’est par exemple à vous d’installer l’agent K8s kubelet dessus).
  • Pods : les pods sont les objets centraux de K8s. Ils représentent un groupe d'un ou plusieurs conteneurs s’exécutant dans un node (un pod ne correspond donc pas à un conteneur). Un pod correspond à une seule instance de ce groupe de conteneurs. Ce sont donc eux qui vont donc être créés lors d’une montée en charge.
  • Volumes : les volumes vous permettent de gérer les fichiers de votre application, en permettant notamment de les partager entre les pods. De plus ils permettent de conserver les fichiers en cas de crash d’un conteneur. Ne les considérez pas perpétuels : les données peuvent être effacées si le pod le contenant est détruit. Si vous voulez stocker des données de manière permanente (dans le cas d’une base de données par exemple), utilisez plutôt un PersistentVolume qui est un autre objet K8s. Nous vous recommandons cependant de ne pas utiliser les volumes K8s au profit de services de stockage et de bases de données managés (type Amazon RDS ou Google Cloud SQL pour les bases de données et Amazon S3 ou Google Cloud Storage pour les fichiers).
  • Deployments : ce sont les objets représentant la manière dont vos pods sont déployés et en particulier la manière dont ils sont créés ou supprimés lors d’une montée en charge. Vous pouvez ainsi définir un nombre constant de pods, ou bien un nombre de pods variant entre deux limites selon la consommation CPU. Évoquons un petit point de détail : ce n’est pas l’objet deployment qui va gérer le cycle de vie des pods (il ne fait que décrire un état souhaité) mais un autre objet appelé ReplicaSet (créé automatiquement par un deployment).
  • Services : un objet un peu plus délicat à comprendre. Il faut déjà avoir en tête qu’un pod est l’objet qui est accédé au sein du cluster : il possède donc une adresse IP. Vous le savez maintenant : un pod peut être créé, puis détruit, puis de nouveau créé. Mais ce sont des objets différents à chaque création, il ont donc potentiellement une adresse IP différente. Pour conserver une adresse IP constante dans le temps, on utilise ainsi un service. C’est lui qui va faire le lien entre une requête entrante sur un certain port (et donc sur son adresse IP appelée souvent l’IP du cluster) et un des pods parmi les pods possédant un certain label (un label permet d’identifier un de vos modules applicatifs). C’est donc lui qui permet d’abstraire les pods d’un même module applicatif : il est seulement nécessaire de connaître l’IP du service sans se soucier du nombre de pods correspondant.
  • Namespaces : les namespaces permettent de créer une portée pour les objets. Il est ainsi possible de créer un pod avec le même nom dans le namespace A et dans le namespace B. Pour simplifier, il est possible de se représenter les namespaces comme des clusters virtuels. Nous vous conseillons fortement de toujours travailler avec un namespace différent du namespace par défaut, pour notamment effacer facilement tous les objets d’un namespace donné et repartir de zéro.
#
Utilisation

Vous connaissez maintenant les principaux objets utilisés par Kubernetes. Nous allons maintenant voir comment les utiliser avec kubectl pour déployer votre application.

Deux possibilités existent pour gérer votre cluster :

  • Utiliser les arguments de kubectl pour gérer les détails des actions sur les objets.
  • Utiliser des fichiers de configuration.

Si vous utilisez kubectl, nous vous recommandons d’exporter les configurations des objets créés et de les maintenir dans votre dépôt. Pour cela, utilisez la commande get avec l’argument output. Par exemple pour obtenir les fichiers de configuration en YAML de vos pods, utilisez la commande suivante :

kubectl get pods --namespace=app --output=yaml

Par la suite, nous indiquerons à la fois la commande et la configuration permettant d’obtenir le résultat souhaité. Si vous optez pour l’utilisation de fichiers de configuration, la commande permettant de créer les objets correspondant est la suivante :

kubectl create -f config-file.yaml --namespace=app

Considérons à partir de maintenant que vous possédez un cluster Kubernetes fonctionnel et que kubectl est utilisable et correctement configuré. Vous avez par exemple créé un cluster GKE et avez configuré kubectl grâce à Google Cloud SDK (gcloud) en suivant la documentation. Nous allons prendre pour exemple une application composée de deux services API Platform : une API principale et une API dédiée à la gestion des utilisateurs. Nous utiliserons Nginx comme serveur HTTP. Nous aurons donc 3 pods : nginx, api et users. Commençons par créer le namespace que nous utiliserons pour notre application : app.

kubectl create namespace app

En utilisant un fichier de configuration :

# namespace.yaml :
apiVersion: v1
kind: Namespace
metadata:
  name: app

Le champ apiVersion est nécessaire pour les fichiers de configuration (il permet à Kubernetes de savoir quelle version de son API il doit utiliser pour créer l’objet correspondant). Notre namespace est maintenant créé : vous pouvez le vérifier en exécutant la commande :

kubectl get namespace

Les commandes suivantes devront donc maintenant toutes être exécutées avec l’argument --namespace=app (ou -n app pour la version raccourcie). Créons maintenant nos différents pods. Nous n’allons pas les créer directement : rappelez-vous, ce sont les objets Deployments qui en sont responsables. Nous considérons que vous avez déjà créé vos images Docker, que celles-ci sont disponibles dans le cloud (par exemple dans le Google Container Registry). Pour créer un déploiement en ligne de commande, il est bien sûr possible de faire :

kubectl create deployment api-deployment --image=docker-api -n app

Cette commande ne permet pas de gérer le nombre de pods répliqués. C'est pourquoi nous vous recommandons d'utiliser l’action run plutôt que create, à la manière de Docker :

kubectl run api-deployment --image=docker-api -n app

Ajoutons quelques options (ouverture des ports utilisés par les pods, nombre de répliques, variables d’environnement et labels permettant d’ajouter des attributs utiles à nos pods) et créons les déploiements restants.

kubectl run api-deployment --image=docker-api --port=9000 --replicas=1 --env="APP_ENV=dev" --labels=”app=api” -n app
kubectl run users-deployment --image=docker-users --port=9000 --replicas=1 --env="APP_ENV=dev" --labels=”app=users” -n app 
kubectl run nginx-deployment --image=docker-nginx --port=80 --labels=”app=nginx” -n app

Avec des fichiers de configuration plutôt que des commandes :

# nginx-deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: docker-nginx
        ports:
        - containerPort: 80
# api-deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
  labels:
    app: api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: api
        image: docker-api
        ports:
        - containerPort: 9000
        env:
        - name: APP_ENV
          value: dev
# users-deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-deployment
  labels:
    app: users
spec:
  replicas: 1
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      containers:
      - name: users
        image: docker-users
        ports:
        - containerPort: 9000
        env:
        - name: APP_ENV
          value: dev

Si vous recherchez plus d’informations pour des champs des fichiers de configuration, référez-vous à la documentation de référence de l’API que vous trouverez ici : https://kubernetes.io/docs/reference/. Nous avons donc ajouté nos déploiements à notre cluster. Nous pouvons en être certain grâce à cette commande :

kubectl get deployments -n app

En toute logique, nous devrions avoir nos pods automatiquement créés. Vérifions cela :

kubectl get pods -n app

Ils sont bien là, n’est-ce pas ? Profitons-en pour vérifier l’efficacité de Kubernetes dans sa gestion des ressources. Nous allons d’abord supprimer un pod. Une partie du nom des pods créés par Kubernetes varie aléatoirement lors de leur création. C’est pour cette raison que nous leur avons donné des labels lors de la création des déploiements : ils nous permettent de référencer les pods dans les commandes kubectl. Voici par exemple la commande pour supprimer le pod users :

kubectl delete pods -l app=users -n app

Aussitôt supprimé, aussitôt recréé ! Vous pouvez lister les pods pour le vérifier : le nom du pod users vient de changer. Patientez encore quelques secondes et votre pod est fonctionnel. Kubernetes a bien suivi les instructions du déploiement et vous garantit qu’un pod users sera toujours présent. Simulons également un cas de montée en charge : un seul pod users n’est pas suffisant et nous en voulons trois. Rien de plus simple :

kubectl scale deployment users-deployment --replicas=3 -n app

Attendez un petit peu, listez vos pods : 2 nouveaux pods users sont apparus. Bien évidemment, il est possible d’automatiser ce comportement selon les ressources CPU disponibles grâce à la commande autoscale. Posons-nous un instant. Nous avons un cluster contenant des pods fonctionnels : notre application est donc capable d’être exécutée. Pourtant il nous manque encore quelque chose. Oui, vous avez bien lu la description des objets : nos pods ne sont pas accessibles avec une adresse IP constante car nous n’avons pas encore créé de services. Nous allons régler cela.

kubectl expose deployment api-deployment --port=9000  
kubectl expose deployment users-deployment --port=9001 --target-port=9000
kubectl expose deployment nginx-deployment --port=80 --type=NodePort

Si vous préférez utiliser directement des fichiers de configuration :

# api-service.yaml :
kind: Service
apiVersion: v1
metadata:
  name: api
spec:
  selector:
    app: api
  ports:
  - port: 9000
# users-service.yaml :
kind: Service
apiVersion: v1
metadata:
  name: users
spec:
  selector:
    app: users
  ports:
  - port: 9001
    targetPort: 9000
# nginx-service.yaml :
kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
  - port: 80

Quelques remarques :

  • Par défaut, le service créé a comme type ClusterIP : son IP est interne au cluster.
  • Le trafic du pod users est redirigé du port 9001 vers le port 9000 afin d’éviter un conflit avec l’api, également sur le port 9000.
  • Le service du pod nginx est de type NodePort. Le port du service est alors exposé au niveau du Node. Cela va nous permettre de mettre en place le load-balancing

Comme vous commencez à en avoir l’habitude, vous pouvez vérifier que vos services ont bien été créés :

kubectl get services -n app

Nos pods sont donc accessibles avec une adresse IP constante grâce à nos services. Mais ils le sont seulement à l’intérieur du cluster. Pour pouvoir y accéder depuis l’extérieur nous allons mettre en place du load-balancing grâce à un objet particulier de K8s : Ingress. Il n’est pour l’instant pas possible d’utiliser une commande pour créer une Ingress (on espère que ça le sera bientôt).

# ingress.yaml :
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
spec:
  rules:
  - http:
      paths:
      - path: /app
        backend:
          serviceName: nginx
          servicePort: 80

Vérifiez que votre Ingress a bien été créée :

kubectl get ingress -n app

Grâce à Ingress, un load-balancer layer 7 (généralement basé sur Nginx) a été automatiquement créé. Votre application est maintenant accessible depuis l’extérieur avec le chemin /app. Nous en avons terminé pour cette première utilisation de Kubernetes, bravo ! Pour finir, il est possible de réaliser ces opérations avec une interface Web graphique. Oui nous ne vous le disons que maintenant, c’est voulu ! Lancez la commande suivante :

kubectl proxy

Vous pouvez maintenant accéder à l’interface Web à partir de votre navigateur préféré :) 

Vous souhaitez monter en compétences sur Kubernetes ? Consultez notre formation disponible sur Masterclass ! Notre équipe de consultants peut vous accompagner dans la gestion de vos conteneurs en production. N’hésitez pas à nous contacter !

Alan Poulain

Alan Poulain

Lead developer & consultant

Mots-clésGoogle, K8s, Kubernetes, Orchestration de conteneurs

Le blog

Pour aller plus loin