Kubernetes

Introduction

Qu’est-ce que Kubernetes ?

Kubernetes, souvent abrégé en K8s est une technologie d’orchestration de containers. Initialement développée par Google en 2014, elle a été offerte à la Cloud Native Computing Foundation à sa sortie en 2015. Kubernetes est souvent utilisé avec Docker en tant que “container runtime” (technologie qui permet de créer et de faire fonctionner le container) mais ce n’est pas une obligation. Il est par exemple tout à fait possible d’utiliser K8s avec Rocket ou CRIO qui sont des alternatives à Docker.

Que permet-il ?

Il permet principalement de gérer de manière automatique et organisé de grandes quantités de containers. Kubernetes peut absorber la monté en charge en créant automatiquement plus de containers et également en supprimer quand la charge diminue. L’exécution des containers peut être répartie sur différentes machines (virtuelles ou physique) appelées “Nodes”. Si une machine (Node) venait à ne plus fonctionner, K8s répartirait alors la charge sur les Nodes restants.

💡
Il est important de souligner qu’il ne faut pas utiliser Kubernetes pour tout et n’importe quoi. Le fait que cette solution soit “à la mode” rend son utilisation très courante et parfois dans des cas où ce n’est pas du tout nécessaire. En informatique, le mieux est souvent de faire au plus simple.

Les objets Kubernetes :

Les Pods

Les Pods sont les plus petites entités dans K8s, ce sont eux qui contiennent les containers. Il y a en général un container par Pod. Dans certain cas il peut y en avoir plusieurs mais uniquement dans le cas où l’on aurait besoin de rajouter des containers auxiliaires, des “helpers” au container principal. Il n’y aura donc jamais deux fois le même container dans un Pod. Si l’on a besoin des plusieurs containers identiques pour absorber de la charge alors on créera plusieurs Pods.

Les Nodes

Les Nodes, ou nœuds en français, sont des machines virtuelles ou physiques sur lesquelles Kubernetes est installé. Un Pod s’exécute alors sur un Node.

Les Replica Sets

Les Replica Set sont des objets qui permettent de maintenir un nombre défini de Pods actifs. Un Replica Set est un contrôleur qui surveille constamment les Pods et réagis en fonction du contexte. En général, un Replica Set ne surveille pas tous les Pods mais uniquement un type de Pods (ex: frontend) et donc offre une garantie de disponibilité sur un type de service. Si un ou plusieurs Pods du type de service surveillé par le Replica Set venait à planter, alors il(s) serai(ent) automatiquement recréés. Les Pods surveillés par le Replica Set sont ceux dont les labels correspondent avec ceux définis dans la partie selector du Replica Set.

Les Deployments

Les Deployments ne sont rien d’autre que des Replica Set auxquels on a rajouté une couche pour permettre le déploiement d’une nouvelle version d’application. Les Deployment offrent la possibilité de mettre à jour notre application à chaud, de façon progressive et de manière transparente pour l’utilisateur (sans interruption de service). Pour cela il va progressivement supprimer les anciens Pods et en créer des nouveaux avec la nouvelle image du container qu’on lui a spécifiée. Il est également possible de faire machine arrière dans le cas où la nouvelle image ne fonctionnerait pas comme souhaité.

Les Services

Cluster IP

Les services de type Cluster IP permettent de créer une adresse IP virtuelle à l’intérieur d’un cluster pour pouvoir établir des communications entre différents services comme un ensemble de serveur web et un ensemble de base de données. Un ensemble de Pods est donc masqué derrière une même adresse IP.

NodePort

Le NodePort est un service qui écoute sur un port d’un Node (entre 30 000 à 32 767) et qui redirige le trafic vers un Pod ou un ensemble de Pods effectuant le même service (un Replica Set)

Cela permet à un utilisateur extérieur du Node d’accéder à une application à l’intérieur du Node. Par exemple si un Pod exécute un serveur web, le NodePort va orienter le trafic entrant sur le Node sur le port 30 080 (par exemple) vers le serveur web.

S’il y a plusieurs Nodes dans un même Cluster, le service sera orienter le trafic, peu importe le Node visé.

En résumé, que l’on ait un Pod sur un Node, plusieurs Pods sur un Node ou plusieurs Pods sur plusieurs Nodes, le service saura s’adapter.

LoadBalancer

Les Config Maps

Les Config Maps sont des objets permettant de stocker des variables d’environnement sous la forme clé-valeur pour pouvoir les injecter lors de la création d’un Pod par exemple. Il est possible d’injecter toutes les variables stockées d’un objet Config Map dans un Pod ou bien d’en sélectionner seulement certaines à l’aide de leur clé. Les Config Maps ne sont pas faites pour stocker des données sensibles comme des mots de passe. Pour les données sensibles il faut utiliser des secrets.

Secrets

À l’instar des Config Maps les Secrets sont des objets permettant des stocker des variables d’environnement sous la forme clé-valeur sauf que les Secrets sont prévus pour stocker des données sensibles comme des mots de passe. Par défaut, les Secrets ne sont pas chiffrés, seules les valeurs sont encodées en base 64. Les format base 64 permet seulement de masquer une donnée sensible mais ne permet pas de la protéger. Il est aussi simple d’encoder en base 64 que de décoder du base 64. L’intérêt d’utiliser des Secrets sans chiffrement est donc limité mais permet tout de même d’être plus discret par rapport à l’utilisation de Config Maps.

Les Namespaces

Les Namespaces permettent d’isoler des ensembles d’objets (Pods, Services, Deployments etc) à l’intérieur d’un même Cluster. Les noms d’objets doivent être uniques à l’intérieur d’un Namespace mais pas nécessairement entre tous les Namespaces. Deux Pods peuvent avoir le même nom pourvu qu’ils soient dans deux Namespaces différents.

Les Taints et Tolerations

Taint peut être traduit par contaminer ou infecter. Dans K8s quand on “Taint” un Node, on lui applique une caractéristique spéciale qui va empêcher, par défaut, les Pods de se déployer dessus. C’est un peu comme si on irradiait ☢️ notre Node, personne n’aurait envie de s’y exécuter. C’est là qu’interviennent les Tolerations, des résistances aux contamination (“Taint”).

Pour résumer si on applique une Taint : “irradié” à un Node, aucun Pod ne pourra s’exécuter sur ce Node sauf les Pods ayant une résistance (Toleration) à “irradié”.

En pratique ce sont des pairs de clé-valeur qui sont comparées entre les Pods et les Nodes, empêchant soit l’attribution d’un Pod sur un Node (schedulling) soit l’exécution d’un Pod sur un Node (executing).

Les Jobs

Le Job agit comme un Replica Set mais pour des containers effectuant une tâche finie. Là ou un serveur web doit rester actif le plus longtemps possible, une tâche de calcul doit s’arrêter une fois que le calcul est fini. Un Job va donc lancer un container dans un Pod, attendre qu’il se termine en indiquant qu’il a réussi et le supprimer. Si le container échoue, le Job va le relancer jusqu’à réussite ou jusqu’à atteindre un seuil définie.

Il est possible de dire au Job de lancer des containers jusqu’à un certain nombre de réussites, auquel cas il va lancer les containers un par un jusqu’à obtenir le nombre souhaité de réussites. Il est également possible de paralléliser l’exécution des Pods et donc de ne pas les lancer un par un.

Les CronJobs

Un CronJob n’est rien d’autre qu’un Job qui peut être planifier à la manière des CrontTabs sous Liunx. Cela permet de choisir la date et/ou l’heure à laquelle on veut qu’une tâche s’exécute ainsi que d’introduire une périodicité si l’on souhaite.

Les Ingress

Un objet Ingress permet de gérer les accès aux Services internes via une entrée externe. Typiquement, un Ingress est utilisé lorsque l’on a plusieurs applications web à l’intérieur de notre cluster, chaque application étant gérée par un Service de type NodePort. Alors, l’Ingress va permettre, à partir d’une même nom de domaine (FQDN).

Ex : www.example.com pointera vers l’adresse IP de l’Ingress

Si l’on souhaite accéder au service VOD du site il faudra taper www.example.com/VOD et alors l’Ingress transfèrera la requête vers le Service de type NodePort s’occupant de l’application web VOD.

De même, si l’on souhaite accéder au service live du site il faudra taper www.example.com/live et alors l’Ingress transfèrera la requête vers le Service de type NodePort s’occupant de l’application web VOD.

Les containers derrière l’application VOD et live sont complètement différents, ils dépendent de Services et de Deployments différents, mais grâce à l’Ingress, on peut y avoir accès en passant par le même nom de domaine complètement qualifié (FQDN).

Network Policy

Les Network Policies définissent un ensemble de règles d’accès réseaux s’appliquant sur un Pod ou un groupe de Pods. Ces règles peuvent aussi bien s’appliquer sur le trafic entrant (Ingress) ou le trafic sortant (Egress). Elles permettent par exemple de définir quel Pod ou groupe de Pod peuvent communiquer avec le Pod ciblé par la Network Policy. Il est également possible de cibler des ports TCP ou UDP ainsi qu’une plage d’adresses IP.

💡
Par défaut, tous les Pods d’un même Namespace peuvent communiquer entre eux sans restriction.

Un exemple concret d’utilisation de Network Policies serait pour le cas d’un serveur web et d’une base de données. Pour le serveur web, on autoriserait les requêtes venant de l’extérieur sur le port 80 ainsi que paquet sortant vers le Pod de base de données. Pour le Pod base de données, on autoriserait uniquement les paquets venant du Pod serveur web sur un port précis.

Persistent Volume

Les Persistent Volumes sont un type de ressource comme les Pods par exemple. Ils définissent un espace de stockage qui pourra être utilisé par des Pods. Les Pods et les PV (Persistent Volumes) sont indépendant même s’ils sont liées. Si l’un des deux est supprimé, l’autre ne le sera pas.

Ex : Si je supprime le Pod qui utilise un PV, le PV ne sera pas supprimé et les données écrites dessus ne seront pas perdues.

Il existe plusieurs types de Volume qui peuvent être définis dans le PV selon le besoin (emptyDir, nfs, hostPath etc).

Persistent Volume Claim

Là où le PV permet de stocker des données, le PVC (Persistent Volume Claim) est une ressource qui permet de formuler une demande de stockage. Cette demande sera alors attribuée à un PV qui correspond aux exigences de la demande.

Dans les exigences de la demande, on retrouve bien sur la taille souhaitée (50Mo, 10Go, etc.), le mode d’accès (lecture seulement, lecture et écriture une fois, lecture et écriture plusieurs fois) ainsi que d’autres demandes plus spécifiques.

Role

Les Roles font partie du RBAC (Role-Based Access Control). Ils permettent de définir quel utilisateur peut faire quoi. Un même Role, peut être attribué à différents utilisateurs, cela évite de définir les mêmes règles pour plusieurs utilisateurs ainsi que d’ajouter de la sémantique grâce au nom du Role (Ex : “Dev”).

La définition d’objet en YAML :

Pour chaque objet de Kubernetes voici un exemple le plus complet possible de fichier de définition.

Pod

NB : Le fichier de définition d’un Pod est rarement aussi long que ça, celui est une compilation de tous les éléments possibles qu’un fichier de définition peut contenir.

apiVersion: v1

kind: Pod

metadata:
  name: mon_pod
  labels:
    type: front-end
    app: mon_app

spec:
# Si un container crash, doit-il être redémarré ?
  restartPolicy: Never 

# Liste des containers étant lancés un par un avant l'execution des containers classique
# Ils s'executent, font leur tâche et meurent.
# Si un de ces containers échoue, tout le pod restart jusqu'à ce que tous les inits réussicent
  initContainers:  # non obligatoire
    - name: before-start
      image: busybox
      command: 
        - "sh"
        - "-c"
# 'sh -c' veut dit exécute la commande suivant avec 'sh'
        - "sleep 3600"

# Définition des containers : (fils de spec)
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 8080

# Pour monter des volumes dans le container
      volumeMounts:
        - mountPath: /opt
          name: volume1

# Pour passer des varaibles d'environnement codées en dur :
      env:
        - name: REMOTE_DB
          value: mySql.fr
        - name: PASSWORD_DB
          value: 123SqlMy
# Pour passer une varaible d'environnement depuis un config map :
        - name: IP_ADDR
          valueFrom:
            configMapKeyRef:
              name: config_map_name  # Nom du config map
              key: IP_ADDR   # Clé dans le config map

# Pour passer toutes les varaibles d'environnement définies dans une ConfigMap :
      envFrom:
        - configMapRef:
            name: config-map-db

# Pour permettre de lancer le container en tant que user et non root
      securityContext:  
        runAsUser: 1000  # User ID. Sans cette ligne l'utilisateur est root
        capabilities: # Pour ajouter ou retirer des droits
          add: ["MAC_ADMIN"]
          drop: ["SYS_LOG"]

# Pour définir quand le container est prêt (prêt a répondre aux requêtes par exemple)
      redinessProve:
# Test HTTP :
        httpGet:
          path: /api/ready
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 8  # seuil
# Test de socket TCP :
        tcpSocket:
          port: 3306
# Test d'exécution de commandes :
        exec:
          command:
            - cat
            - /app/is_ready

# Pour définir quand le container est vivant (capable de répondre aux requêtes par exemple)
      livenessProve:
# Test HTTP :
        httpGet:
          path: /api/ready
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 8  # seuil
# Test de socket TCP :
        tcpSocket:
          port: 3306
# Test d'exécution de commandes :
        exec:
          command:
            - cat
            - /app/is_ready

        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"

# Taint et tolerations :
# NB : "tolerations:" est au même niveau que container, c'est un fils de spec
  tolerations:
    - key: "spray"
      operator: "Equal"
      value: "mauve"
      effect: "NoSchedule"

# Node Selectors :
  nodeSelector:
    size: medium # NB: size: medium n'est qu'une paire clé valeur. Elle doit correspondre aux labels d'un pods

# Node Affinity, a bit more complicated but more precise.
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      # Ou preferredDuringSchedulingIgnoredDuringExecution:
      # Ou requiredDuringSchedulingRequiredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
            - key: size
	            operator: In # Ou InNot Ou Exists ( si Exists, ne pas mettre values )
	            values:
                - Medium
	
# Volumes, fils de spec:
  volumes:
# Directement sur l'hote :
    - name: test-volume
      hostPath:
      # Chemin du répertoire sur l'hote
        path: /data
      # Optionnel :
        type: Directory
# En utilisant un persistentVolumeClaim (PVC)
    - name: volume1
      persistentVolumeClaim:
        claimName: myclaim
    - name: cache-volume
        emptyDir:
          sizeLimit: 500Mi
Replica Set
apiVersion: apps/v1
kind: ReplicaSet
metadata: 
  name: monApp-replicaSet
	labels:
		app: monApp
		type: front-end
spec:
	replicas: 5
  selector:
    matchLabels:  # Doivent être les mêmes que dans la définition du pod
			app: monApp
			type: part1
  template:
		## Début de la définition du Pod ##
    metadata:
			name: monApp-pod
			labels:
				app: monApp
				type: part1
			spec:
				containers:
					- name: nginx-container
						image: nginx
		## Fin de la définition du Pod ##
Deployment

Très similaire à la définition du Replica Set

apiVersion: apps/v1

kind: Deployment

metadata: 
  name: monApp-deployment
	labels:
		app: monApp
		type: front-end

spec:
	replicas: 5

  selector:
    matchLabels:  # Doivent être les mêmes que dans la définition du pod
			app: monApp
			type: part1

  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate

  template:
		## Début de la définition du Pod ##
    metadata:
			name: monApp-pod
			labels:
				app: monApp
				type: part1
			spec:
				containers:
					- name: nginx-container
						image: nginx
		## Fin de la définition du Pod ##
Config Map
apiVersion: v1

kind: ConfigMap

metadata:
  name: my-app-config-map

data:
	DB_HOST: sql.fr
	DB_PASSWORD: 1234
  IP_ADDR: 192.168.1.1
Job
apiVersion: batch/v1

kind: Job

metadata:
  name: calcul

spec:
	completions: 5  # Nombre de fois que l'on souhaite que le container réussisse
	parallelism: 4 # Nombre de container pouvant être exécutés en même temps
  template:
    spec:
      containers:
      - name: ubuntu
        image: ubuntu
        command: ["expr",  "55", "+", "25"]
      restartPolicy: Never  # Si le container se termine en réussissant alors il ne sera JAMAIS redémarrer

  backoffLimit: 4  # Par défaut c'est 6
CronJob
apiVersion: batch/v1

kind: CronJob

metadata:
  name: hello

spec:
  schedule: "* * * * *"
  jobTemplate:
# Début de la défintion du Job
    spec:
      template:
		# Début de la défintion du pod
        spec:
          containers:
          - name: hello
            image: busybox
            command:
            - /bin/sh
          restartPolicy: Never
		# Fin de la défintion du pod
# Fin de la défintion du Job
Service
apiVersion: v1

kind: Service

metadata: 
  name: webapp-service

spec:
  type: NodePort  # ou ClusterIP ou LoadBalancer
  ports:
    - targetPort: 80
      port: 80
      nodePort: 30080  # uniquement si type = NodePort et non obligatoire 
  selector:  # Pods visés par le service :
    app: webapp
    type: frontend
Ingress
apiVersion: networking.k8s.io/v1

kind: Ingress

metadata:
  name: minimal-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /

spec:
  ingressClassName: nginx-example

  rules:
  - http:
      paths:
      - path: /testpath
        pathType: Prefix

        backend:
          service:
            name: test
            port:
              number: 80
Network Policy
apiVersion: networking.k8s.io/v1

kind: NetworkPolicy

metadata:
  name: test-network-policy
  namespace: default

spec:
  podSelector:
    matchLabels:
      role: db

  policyTypes:
    - Ingress
    - Egress

  ingress:
    - from:
        - ipBlock:
            cidr: 172.17.0.0/16
            except:
              - 172.17.1.0/24

        - namespaceSelector:
            matchLabels:
              project: myproject

        - podSelector:
            matchLabels:
              role: frontend

      ports:
        - protocol: TCP
          port: 6379

  egress:
    - to:
        - ipBlock:
            cidr: 10.0.0.0/24

      ports:
        - protocol: TCP
          port: 5978
Persistent Volume
Persistent Volume Claim
Storage Class
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: Immediate
Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""] # "" indique le group API coeur 
# A accès à :
  resources: ["pods"] 
# Peut executer :
  verbs: ["get", "watch", "list"]  
Role Binding
apiVersion: rbac.authorization.k8s.io/v1
# This role binding allows "jane" to read pods in the "default" namespace.
# You need to already have a Role named "pod-reader" in that namespace.
kind: RoleBinding
metadata:
  name: read-pods
  namespace: default
subjects:
# You can specify more than one "subject"
- kind: User
  name: jane # "name" is case sensitive
  apiGroup: rbac.authorization.k8s.io
roleRef:
  # "roleRef" specifies the binding to a Role / ClusterRole
  kind: Role #this must be Role or ClusterRole
  name: pod-reader # this must match the name of the Role or ClusterRole you wish to bind to
  apiGroup: rbac.authorization.k8s.io

Les commandes kubectl :

Les commandes générales

Lister les objets :

NB : Par défaut, la commande affiche les objets dans le Namespace par défaut, si besoin spécifier le Namespace à l’aide de l’argument -n <namespace>.

Supprimer des objets :

Modifier des ressources

Pour modifier à chaud une ressource en ouvrant le yaml dans un éditeur :

kubectl edit pod my-webapp

⚠️ Il n’est possible de modifier que certains champs comme l’image ou la tolerations

Pour faire la même chose sans ouvrir d’éditeur :

kubectl set image pod <nom-du-pod> <non-du-container>=<nom_de_la_nouvelle_image>

kubectl set image pod webapp container-name=new-image

Cela fonctionne aussi avec les Deployment, Replicaset etc..

Taint et Tolerations

Node Selectors

Logs

Deployment

Role

Api Resources

Explain

Proxy

Label

Namespace

Par défaut les commandes s’exécutent dans le Namespace default

Les commandes impératives

Ces commandes dites impératives permettre de créer des objets sans passer par des fichiers de définition en YAML. C’est utile notamment lors de passage de certification K8s où il faut être rapide.

💡
NB : l’option --dry-run=client permet de faire un coup à blanc, c’est à dire d’évaluer la commande mais de ne pas l’exécuter. Aucune ressource de sera créée avec cette option mais cela permet de voir si la commande tapée est correcte. Son usage peut être utile avec l’option -o yaml qui va afficher le fichier de définition YAML correspondant à la commande tapée.
Pod

kubectl run <nom_pod> --image <nom_image> --port <num_port_a_exposer> --labels "<key1>=<value1>,<key2>=<value2>" --expose=true|false

Ex :

kubectl run pod-nginx --image nginx --port 8080 --labels "tier=frontend,app=monapp"

Si --expose=true alors la commande crée un service de type ClusterIP lié au pod. Cela nécessite l’argument --port.

Deployment

kubectl create deployment <nom_deploy> --image <nom_image> --replicas <nb_replicas>

Ex :

kubectl create deployment nginx --image nginx --replicas 5

Pour changer (mettre à jour par exemple) l’image d’un Deployment :

kubectl set image deployment/<mon_deploy> <nom_container>=<nouvelle_image>

Ex :

kubectl set image deployment/my_app_deploy nginx=nginx:1.9.3
Service

kubectl expose pod <nom_pod_a_exposer> --port <numero_port_a_exposer> --name <nom_du_service> --type <type_de_service>

NodePort :

Ex :

kubectl expose pod nginx --port 80 --name nginx-service --type NodePort

Les sélecteurs utilisés par le service créé seront les labels du pod nginx. Le numéro du NodePort sera choisi au hasard dans la plage allant de 30 000 à 32 767 (sans créer de conflit).

ClusterIP :

Ex :

kubectl expose pod redis --port 6379 --name redis-service --type ClusterIP

Config Map

kubectl create configmap <nom_config_map> [--from-file=<source>] [--from-literal=key1=value1] [--dry-run=server|client|none] [options]

Ex :

kubectl create configmap db-configMap --from-literal=HOST=mysql.fr --from-literal=PASSWORD=1234mySql
Ingress

kubectl create ingress <nom_ingress> --rule="<host>/<path>=<service>:<port>"

Ex :

kubectl create ingress test-ingress --rule="www.example.com/store*=wear-service:80"
Role

kubectl create role <nom_role> --verb=<verb1>,<verb2>,<verb3> --resource=<resources1>,<resource2>

Ex :

kubectl create role app-dev --verb=get,create,delete --resource=pods
Role Binding

kubectl create rolebinding <nom_du_binding> --role=<nom_role> --user=<nom_user>

Ex :

kubectl create rolebinding app-dev-binding --role=app-dev --user=user-dev

Autres

Apiserver :

Pour obtenir des information sur le kube-apiserver :

cat /etc/kubernetes/manifests/kube-apiserver.yaml

Instances de Kubernetes

Pour obtenir des informations sur une instance de Kubernetes :

ps -aux | grep <une_instance>

Ex :

ps -aux | grep my-webapp

Obtenir la release de l’OS

cat /etc/*release*

Tests sur containers

Tester une connexion avec ports

Une fois attaché à au shell d’un container :

nc -vzw 2 <IP_ou_nom_service/pod> <port>

Ex :

nc -vzw 2 my-service 80

Je teste si, depuis le pod sur lequel je suis, je peux contacter le service my-service sur le port 80 (le service va sans doute me rediriger vers un pod)

nc est la commande netcat.

L’argument -v indique que l’on veut une réponse verbeuse.

L’argument -z indique que l’on veut uniquement tester l’ouverture d’un port sans envoyer de donnée.

L’argument -w 2 indique qu’on considère que le port n’est pas accessible au bout de deux secondes sans réponse. (timout = 2s)