Kubernetes, Les ressources Part 1 : Les bases

Kubernetes, Les ressources Part 1 : Les bases

150 150 Loick JONCOUR

Il existe de nombreux types de ressources dans Kubernetes (ou K8s pour les intimes), toutes peuvent être déclarées en Json ou en Yaml (privilégié car plus lisible). 

Certains paramètres sont présents dans touteles ressources :  

apiVersion: v1                # La version de l’api Kubernetes que voulez utiliser.
kind: Pod                        # Le type de ressources que vous déclarez, ici il s’agit d’un pod. 
metadata:
name: myapp                # un nom, il devra être unique dans notre namespace. 
namespace: myNs        # nom du namespace ou sera déployée la ressource 
labels:                              # des tags, pour retrouver cette ressource dans K8s 
app: myapp
type : java-back-end
annotations:                  # des tags, destinés à l’utilisation par d’autres applications que k8s 
build: Staging
builder: loick 

Chacune des ressources de Kubernetes nécessite au minimum : 

  • Une API version. Pour intéragir avec Kubernetes, tout passe par calls API, il faut donc préciser la version de l’API.
  • Ukind, le type de notre ressource. 
  • Un nom, il devra être unique par ressource et par namespace. 
  • Unamespace, ceci dit, certaines ressources ayant un scope « cluster » ne prennent pas ce parametre

Les labels nous serviront à retrouver notre ressource à l’intérieur de notre cluster via des labelSelector, les annotations seront utilisées par tous les autres outils/applications qui voudraient utiliser les méta-data de notre ressource.

Pour aller plus loin

Aujourd’hui je vais vous présenter certaines des ressources de base de Kubernetes à savoir :  

  • Namespaces (Alias : ns) 
  • Pods (Alias : po) 
  • Secret (Pas d’alias…)
  • Configmaps (Alias : cm) 
  • PersistentVolumeClaim (Alias : pvc) 
  • Persistentvolumes (Alias : pv) 
  • ReplicaSet (Alias : ns) 
  • Deployments (Alias : deploy) 
  • Ingress (Alias : ing) 
  • Services (Alias : svc) 

*Les alias peuvent être utilisés avec la cli kubectl pour raccourcir vos commandes, c’est quand même plus sympa d’écrire pdb que poddisruptionbudgets.

Namespace 

La Namespace est la ressource qui nous permettra de commencer à gérer la micro-segmentation de notre cluster. Un Namespace n’est pas un espace « physique », c’est en fait un cluster virtuel, il sera partagé entre les différents nœuds de votre cluster, vous pourrez ainsi diviser votre cluster en plusieurs Namespaces, affecter des quotas à vos Namespaces pour restreindre leurs utilisations des ressources physiques (CPU, RAM, Stockage, etc..), gérer les accès, etc… 

En d’autres termes, il nous permettra de définir un espace dans notre cluster où les différentes équipes pourront déployer leurs ressources, sans forcément avoir d‘accès sur les ressources de leurs collègues qui auraient leurs propres Namespaces. Vous pourrez régir cet espace avec des règles de flux, quotas, etc… 

Nb : Vous pourriez définir qu’un namespace x a le droit d’utiliser 2 unités de cpu.
Vous pourrez donc déployer 2 pods utilisant une unité de cpu chacun, mais si vous souhaitez en déclarer un 3eme, ce dernier restera en attente de suffisamment de cpu pour être déployé, même si votre cluster dispose de 10 cpu libre.
Si vous supprimez un des 2 premier pods, le troisième se lancera dès que les ressources aurons été libérées dans le Namespace.    

Pour aller plus loin

 

Pod  

Le Pod est la ressource de base de Kubernetes.
En effet c’est cette ressource qui va encapsuler un ou plusieurs conteneurs.
C’est dans les paramètres de cette ressource que vous définirez quelle image docker doit être utilisée, quel processus dois être lancé au démarrage du container, des variables d’environnement (qui pourront être déclarées en « dur » dans le YAML du Pod ou externalisées dans un secret ou une configmap). La scalabilité de notre Pod sera gérée par des « Controllers », soit des ressources qui « manageront » notre Pod, il en existe différents types dans Kubernetes, je vous en présente 2 ci-dessous.

Le minimum de paramètres requis pour déclarer un pod (en plus de ceux nécessaire pour toute les ressources) est un container avec un nom et une image. Mais vu que nous sommes des gens sérieux, nous voudrons également lui affecter un quota.

Exemple du Yaml d’un pod : 

apiVersion: v1 
kind: Pod 
metadata: 
  name: memory-demo-3 
  namespace: myNs-example 
spec: 
  containers: 
  – name: memory-demo-3                  # Nom de notre container 
    image: polinux/stress                       # nom de référence de notre image docker 
    env:                                                       # Définition des variable d’environnement 
      – name: SECRET_USERNAME       # Nom qu’aura la variable dans le container 
        Value: loick                                      # Valeur de la variable 
    resources:                                             # Définition du quota affecter au pod
      limits: 
        memory: « 1000M »                        # Maximum de Ram que ce pod pourra utiliser   
      requests: 
        memory: « 500M »                          # Minimum de Ram réservé sur le cluster 
    command: [« stress »]                        # Commande lancée au démarrage de notre container 
    args: [« –vm », « 1 », « –vm-bytes », « 150M », « –vm-hang », « 1 »] 

 

Quota Requests / Limits

Les quotas requests/limits sont optionnels mais primordiaux, si nous avons bien construit notre cluster, nous avons créé différents Namespace en leur attribuant différents quotas pour s’assurer que les workloads des différentes équipes n’impactent pas l’équipe voisine. Mais si nous ne précisons pas de quota à notre Pod, rien ne lui interdira d’aller consommer toutes les ressources de notre Namespace et donc d’avoir un impact significatif sur les autres applications qui tourneraient dans ce Namespace. 

Nous voudrons donc limiter notre pod, pour cela il existe donc 2 paramètres : requests et limits. 

Les Requests, qui seront le minimum requis pour faire tourner notre application, si cette valeur n’est pas disponible dans votre namespace, le pod ne sera pas schedulé et restera en statut pending. 

Les limits, quant à elles, définissent ce que votre pod pourra consommer dans votre namespace s’il reste de la ressource disponible. 

Dans notre exemple nous voyons donc que notre pod nécessite 500M, mais si notre namespace dispose de plus de RAM, non utilisées par d’autre ressources, notre pod ira les consommer. 

Cela permet de définir un Burst maîtrisé de notre podKubernetes se chargera de définir quand il est raisonnable de l’utiliser sans que cela n’affecte les autres ressources du Namespace. 

 

Command  

Comme vous le savez un container docker a un cycle de vie lié à un processus (Voir article « faire sa première image »), dans votre YAML vous pouvez écraser la commande par défaut de votre image docker pour la remplacer par celle de votre choix. 

C’est donc ce process qui correspondra à notre « entrypoint » Docker, il faut noter aussi que nous pouvons passer uniquement les arguments, ces derniers seront utilisés par l’entrypoint par défaut de l’image que nous avons choisie, ou utiliser l’entrypoint et les arguments par défaut. 

Dans notre exemple nous pouvons voir que nous voulons lancer la commande « stress » accompagnée de différents arguments/options.  

Nb : Je ne saurais que trop vous conseiller de privilégier un container par pod, particulièrement tant que vous ne maîtrisez pas bien Kubernetes. En effet si vous déclarez plusieurs containers dans votre pod ces derniers partageront la même adresse IP, les mêmes port réseaux, le même storage, ce qui, sans une bonne maîtrise, ruinera vos efforts d’isolation et de sécurité. 

Pour aller plus loin 

 

Secrect / Configmaps

La Configmap est un dictionnaire prenant tous types de String. Nous pourrons les référencer dans un pod pour les envoyer en variable d’environnement dans nos containers, ce qui nous permettra de ne pas mettre en « dur » nos variables d’environnement dans le YAML de nos pods.

Le Secret est quant à lui très similaire a la Configmap, excepté qu’il ne prend que des String encoder en base 64, il est à privilégier pour les informations sensibles tel que les mots de passe. 

 

Exemple d’un secret :

apiVersion: v1 
kind: Secret 
metadata: 
  name: monSecret 
  namespace: myNs 
type: Opaque 
data:                                                                 # Définition des variable 
  username: YWRtaW4=                              # key: valeurs 
  password: MWYyZDFlMmU2N2Rm  

Exemple de l’injection de variable d’environnement via un secret : 

apiVersion: v1 
kind: Pod 
metadata: 
  name: monPod 
  namespace: myNs 
spec: 
  containers: 
  – name: mypod 
    image: redis 
    env:                                                         # Définition des variable d’environnement 
      – name: SECRET_USERNAME          # Nom qu’aura la variable dans le container 
        valueFrom:                                         # fait référence à un objet externe 
          secretKeyRef:                                  # qui est un secret 
            name: monSecret                         # du nom de  
            key: username                               # nom de la variable dans le secret. 
      – name: SECRET_PASSWORD 
        valueFrom: 
          secretKeyRef: 
            name: monSecret 
            key: password 

De cette façon nous pourrons facilement éditer notre secret pour changer les variables d’environnement de nos pods (ils devront être redémarrés pour prendre en compte les modifications faites sur le secret).  

Pour aller plus loin :configMap, secret

 

Persistentvolumes / PersistentVolumeClaims

A la base, les containers ont été créés pour héberger des applications stateless, sans besoin de persistance de données. Dans le cas de docker, lorsque nous lancions un container, Docker Engine nous créait un system de fichier temporaire dans lesquels était stocké toutes la data générée par notre container. Une fois notre container décédé, tous ces fichiers étaient supprimés. Vous vous doutez bien que dans les cas d’une base de données par exemple, cela peut être problématique. Docker a donc créé un système : les volumes docker.  

Un docker volume est un system qui montera un répertoire externe (soit sur l’hôte, soit sur un autre système de stockage) vers notre container, de cette façon les data survivront au container et pourront être partagées sur ces différents réplicas. 

Pour manager la persistance de donnée dans Kubernetes il existe 2 ressources, les PV et PVC. 

Le PersistentVolume est une ressource qui définit un espace de stockage en faisant abstraction de son type, il peut aussi bien s’agir de NFS que d’ISCSI ou même d’une solution propre à notre cloud provider (comme un EBS pour AWS). Tout comme le node (voir article Kubernetes et son architecture), c’est une ressource qui fait directement référence à une ressource physique définie par l’administrateur. 

 

Le PersistentVolumeClaim quant à lui est une demande d’un espace de stockage, une ressource qui va nous permettre de faire le lien entre notre pod et notre PersistentVolumes. Il pourra viser un PV spécifique ou simplement se mettre en attente d’un PV approchant au mieux de sa définition. 
 

On référencera notre PVC dans notre POD, le PVC de son côté demandera X taille de stockage et (si on ne lui a pas spécifié de PV spécifique) ira chercher les PV disponible sur notre cluster et s’associera avec celui qui a des caractéristiques les plus proches de celle dont il a besoin.
Un PVC ne peut se connecter qu’avec un seul PV et inversement. Un PVC ne s’associera jamais à un PV ne remplissant pas au minimum la demande, mais pourra s’associer à un PV beaucoup plus important que ces besoins.

Ainsi nous pouvons créer 2 PV de 50Go, 
Créer un PVC de 15Go qui s’associera à un des PV,
Un autre PVC de 40 Go qui s’associera avec un deuxième PV,
Mais pas un 3ème de 30Go, en effet une fois un PVC associé à un PV, ce dernier ne peut pas être utilisé par un autre PVC, même si le PVC déjà associé n’utilisait pas l’intégralité des ressources du PV.

 

C’est pourquoi il est recommandé, dans un souci d’optimisation, soit de créer plusieurs jeux de PV de différentes tailles, pour répondre au mieux aux demandes faites par les PVC (et donc par les utilisateurs), soit de créer un système de création de PV sizé a la perfection pour chaque PVC et de les référencer l’un l’autre (Attendre qu’un PVC soit en status pending et lui créé un PV sizé sur mesure a l’image des service que l’ont peut trouver sur les clouds publics). De cette façon nous nous assurerons qu’aucun espace ne restera inutilisé. 

Pour aller plus loin 

  

Les ReplicaSet

Les Controllers sont des ressources Kubernetes permettant de gérer des pods, de leur définir un cycle de vie et s’assurer qu’ils soient conformes à la dernière config déclarée sur le cluster. Un pod est mortel, s’il meurt il ne ressuscitera pas, le Controller est là pour faire ce qu’il peut pour garder notre pod en vie, quitte à le tuer pour le ressusciter (aucun pod n’a souffert durant la rédaction de cet article). Il existe différents types de Controllers, qui correspondent à différents besoins, certains Controllers peuvent utiliser d’autres types de Controllers pour arriver à leurs fins. 

Les ReplicaSet sont les Controllers de base, ils permettent d’introduire une notion de scalabilité autour de notre pod. 

En effet nous souhaitons des micro services robustes sans avoir à réserver, en permanence, des ressources sur notre cluster que nous n’utiliserions pas et qui pourraient donc être utilisées par d’autre applications.   

C’est le ReplicaSet qui va nous aider à gérer le nombre d’instances de notre application. Cette ressource nous permettra de définir un nombre de « réplicas » de notre pod et pourra les répartir sur différents nœuds.

Exemple du Yaml d’un ReplicaSet :

apiVersion: apps/v1 
kind: ReplicaSet 
metadata: 
  name: frontend 
  namespace: myEmptyNs 
  labels:                                              # Label/tag placer sur notre replicaSet
    app-gekko: mon-app 
    tier-app-gekko: frontend 
spec: 
  replicas: 3                                     # Nombre d’instances de notre pod 
  selector:                                       # System permettant a notre rs de retrouver nos pods 
    matchLabels:                             # Nos pods devront posséder les labels suivant  
      tier-app-gekko: frontend     # notre tag tier = frontend
      app-gekko: mon-app             # notre tag app = mon-app
 template:                                       # Ici démarre la déclaration de nos pods 

  selector:                                        # System permettant de retrouver nos pods 
    matchLabels:                              # Nos pods devront posséder le label suivant  
      tier-app-gekko: frontend      # notre tag tier = frontend
     app-gekko: mon-app               # notre tag tier = frontend

  template:                                        # Ici démarre la déclaration de nos pods 
    metadata: 
      labels: 
        tier-app-gekko: frontend 
        app-gekko: mon-app
    spec:                                              # Les spec du pod que supervise ce Controller 
      containers: 
      – name: php-redis 
        image: myregistry.io/mon-frontend:v3 

Je vous ai déjà parlé du labelSelector, en voici un exemple. 

Dans ce ReplicaSet, nous définissons que ce Controller devra superviser les pods qui contiendront le label « tier » avec la valeur « frontend ».
S’il existe des pods déjà déployés dans le namespace possédant ces tags, Kubernetes les liera à ce ReplicaSet, dans le cas contraire il créera 3 pods avec les spec définis dans la section template du ReplicaSet et les liera à ce dernier. 

Nb : Attention donc à vos labelSelector, ne soyez pas trop génériques, de plus les labels peuvent nous servir à sécuriser nos flux et peuvent être utilisés par de applications tierces, pensez donc bien à préfixer la clé de vos labels, pour éviter à l’avenir de tomber sur un outil qui utilise les mêmes labels que vous. 

 

Les Deployment

Je vous ai parlé tout à l’heure de Controllers pouvant utiliser d’autres Controllers, c’est le cas du DeploymentEn effet dans notre Deployment nous pourrons, entre autres, préciser un nombre de replicas. Plutôt que de ré-introduire un système déjà existant, notre Deployment nous créera un ReplicaSet qui gérera les replicas, et s’occupera du reste du cycle de vie de notre pod. C’est une sorte de surcouche au ReplicaSet

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myApp-deployment
  labels:
    app: myApp
spec:
  replicas: 3                          # Nombre d’instances de notre pod 
  selector:                            LabelSelector pour retrouver nos pods 
    matchLabels:
      app: myApp
  template:                          Ici démarre la déclaration de nos pods 
    metadata:
      labels:
        app: myApp
    spec:
      containers:
      – name: myApp
        image: myApp:3.2.5
        ports:
        – containerPort: 80  


Si nous appliquons ce 
Yaml dans un Namespace vide, nous obtiendrons donc, un Deployment, un ReplicaSet et 3 pods qui serons exposer sur le port 80. 

Le déploiement facilitera aussi les upgrade de vos pods, intègrera un system de rollback et quelques autres fonctions qui le rendent très intéressant. 
Il existe d’autres types de Controllers qui seront à utiliser en fonction de votre besoin, je pourrais vous donner l’exemple du Job, qui peut être utilisé pour lancer un container qui effectuera une tâche précise et se détruira une fois la tâche accomplie. Il est très important de tous les connaitre pour savoir quand les utiliser.  

Pour aller plus loin : ReplicaSet, Deployment

 

Services 

Les pods sont volatiles, grâce a notre scheduler ( voir article sur l’architecture k8S ), si pour une raison x ou y, kubernetes a besoin de déplacer notre pod sur un autre noeud, il le fera. Il nous faut donc un système nous permettant de contacter facilement nos containers, où qu’ils soient. Et bien sûr, il existe une ressource pour ça, les Services.

Un Service est une sorte de loadbalancerun point d’entrée pour accéder à notre pod, quel que soit son nombre de replicas. Pour que notre Service retrouve nos pods, nous pouvons utiliser un moyen simple que vous commencez à connaître, le labelSelector. 

Nb : Il existe d’autres types de selectors, et d’autres moyens d’associations plus spécifiques. 

Exemple d’un Yaml de Service : 

kind: Service 
apiVersion: v1 
metadata: 
  name: service-myapp 
spec: 
  selector:                          # Notre labelSelector, pour retrouver nos pods 
    app: MyApp                 # Tag présent sur les pods targets 
  ports:                               # Les ports à mettre en relation 
  – protocol: TCP             # Le protocole du flux 
    port: 80                         # Port sur lequel est exposé le service 
    targetPort: 9376          # Le port vers lequel le flux sera redirigé sur le pod

Le Service possèdera une adresse IP (appelée « cluster IP »), elle sera interne au cluster, et nous permettra de contacter nos pods a l’intérieur de ce dernier.
Un service peut associer n’importe quel port en entrée ou en sortie, dans cet exemple nous pourrons contacter notre service sur le port 80. Il redirigera les flux vers un replica du pod procédant le label « app » défini sur « MyApp » sur le port 9376


Pour aller plus loin

 

Ingress  

Les ressources Ingress gèreront les accès externes à nos Services. En effet nos Services nous offrent un point d’entrée vers nos pods, à l’intérieur du cluster, mais ils ne sont (généralement) pas accessibles depuis l’extérieur du cluster, c’est là qu’interviennent les Ingress.
Les Ingress exposerons nos Services sur l’extérieur du cluster, ils exposeront des routes HTTP/HTTPS, nous fourniront un system de load balancing, une terminaison SSL, etc.

Mais l’ingress en lui-même n’est qu’une règle, il se repose en fait sur un Ingress controller qui fera office de reverse proxy, il en existe plusieurs (Traefik, Nginx, etc..) et seront définit au niveau du cluster.

Nb : vous pourrez installer plusieurs Ingress controllersur votre cluster et les utiliser différemment à travers les annotations de votre Ingress. 

Exemple d’un Yaml d’ingress : 

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: myIngress
spec:
  rules:
  – host: myapp.gekko.fr                        # l’URL que nous allons mapper 
    http:
      paths:                                                   # Déclaration de nos paths
      – backend:                                          # Définition de notre target 

          serviceName: service-myapp    # Nom du Service target 
          servicePort: 80                              # Port du Service 
  – host: foo.gekko.fr                              # Autre URL a mapper 
    http:
      paths:                            
      – path: /doc                                        # Ici nous déclarons un path /doc 
        backend:
          serviceName: service-doc          # Qui pointera sur ce Service
          servicePort: 4200                          # Sur le port 4200 
      – path: /status                                   # Ici le second path /status 
        backend:                                           # Pointera sur un autre Service 
          serviceName: service-monitoring 
          servicePort: 8080                         # Sur le port 8080 

 

Dans cet exemple nous pouvons trouver une première règle qui fera pointer l’URL myapp.gekko.fr sur notre Service service-myapp 

Comme vous pouvez le voir dans la seconde règle, il est possible de faire pointer différents paths sur différents services. 

 

TLS 

Pour sécuriser notre Ingress nous pourrons créer un Secret qui contiendra notre clé privée et notre certificat et référencer ce Secret dans notre Ingress afin d’implémenter le TLS. 

Exemple d’un Secret pour stocker notre clé et certificat : 

apiVersion: v1 
kind: Secret 
metadata: 
  name: notre-secret-tls 
  namespace: myNs 
type: kubernetes.io/tls                  # Ici nous remarquons un type spécial TLS 
data: 
  tls.crt: <base64 encoded cert>  # Base64 car on est dans un Secret 
  tls.key: <base64 encoded key> 

 

Exemple d’un ingress TLS :

apiVersion: extensions/v1beta1 
kind: Ingress 
metadata: 
  name: tls-example-ingress 
spec: 
  tls:                                                         # Ici notre config TLS  
  – hosts:                           
    – myapp.gekko.fr 
    secretName: notre-secret-tls      # La référence de notre Secret 
  rules:                                                    # Nos règles Ingress 
    – host: myapp.gekko.fr 
      http: 
        paths: 
        – path: / 
          backend: 
            serviceName: service-myapp 
            servicePort: 80 

Il existe bien d’autres façons d’utiliser l’Ingress et d’autres méthodes que l’Ingress pour exposer nos applications, répondant à certains besoins spécifiques, tels que les nodePort qui peuvent être utilisés avec un loadbalancer externe, ou le Service de type loadBalancer compatible avec les grands cloud providers, nous souscrivant un loadbalancer à l’instanciation et le mappant avec notre service mais nous en parlerons dans un prochain article.   

Pour aller plus loin 

Laisser une réponse