Historiquement, Kubernetes ("capitaine" en grec ancien) a été développé en interne chez Google notamment par Joe Beda, Brendan Burns, Craig McLuckie, rapidement rejoints par Brian Grant et Tom Hockin (accessoirement un des modérateurs fondateurs du subreddit /r/kubernetes) et annoncé pour la première fois par Google en 2014. Kubernetes est dérivé d'Omega, lui même inspiré de Borg ("Toute résistance est inutile"©), le système d'orchestration interne de Google.
Google a présenté Kubernetes comme le successeur de Borg, même si Kubernetes n'est pas utilisé pour la gestion de l'infrastructure même de Google (les limites de kubernetes l'empêchent: 5000 nodes max, 150000 "pods" et 300000 containers max. Bon il y a déjà de quoi faire ).
C'est un logiciel Open-Source, désormais développé par de nombreux contributeurs sous l'égide de la Linux Foundation et de la CNCF (Cloud Native Computing Foundation). La version 1.0 est sortie en 2015, et nous sommes (Mai 2024) actuellement à la version 1.30.
Si le rythme de sortie des nouvelles versions était assez rapide par le passé, depuis 2020~ les choses se sont stabilisées, et on assiste désormais à une release majeure tous les six mois environ.
Kubernetes est un logiciel d'orchestration de containers, c'est à dire qu'il est en charge du déploiement, du dimensionnement ("scaling"), de l'accès réseau (Services, Ingresses), de l'attribution d'espace de stockage, de l'évolution, de la haute-disponibilité de charges de travail s'exécutant dans des "pods". Mais qu'est-ce qu'un "pod"?
L'unité de base de Kubernetes est le "pod", qui peut contenir un seul container, ou plusieurs (ex: un pod "nginx-php-redis" pourrait contenir un conteneur nginx, un conteneur php-fpm et un conteneur redis, communiquant entre eux via localhost).
Kubernetes est notamment utilisé par:
Docker Swarm est la solution d'orchestration de containers de Mirantis (initialement de Docker, revendu par la suite). Toujours maintenue mais de moins en moins utilisée, ses avantages sont l'absence de control-planes et une plus grande simplicité de mise en oeuvre, mais avec moins de possibilité de gestion du trafic, des services, de l'autoscaling, etc...
Plus simple à mettre en oeuvre que Kubernetes, avec une courbe d'apprentissage plus "soft", et utilisé par de nombreuses entreprises, Nomad est une solution tout à fait adaptée et à considérer par rapport à Kubernetes. Elle est axée infrastructures on-premise et IoT du fait de sa simplicité et de sa légèreté, et est aussi capable de s'interfacer avec l'API de Kubernetes. Utilisé notamment par:
Une technologie à surveiller, qui peut correspondre à des besoins ne nécessitant pas la complexité - apparente - de Kubernetes.
Un cluster Kubernetes est composé de deux types de machines (physiques ou virtuelles, voire, conteneurs type LXC ou Docker/Podman (KinD: Kubernetes in Docker):
Pour fonctionner, notre cluster Kubernetes a besoin de plusieurs choses:
cluster.local
. Un Service Kubernetes (on va l'appeler... postgres) présent dans un Namespace que nous allons appeler databases aura pour FQDN: postgres.databases.svc.cluster.local. Dès lors, un FQDN Kubernetes est toujours de la forme: nom-de-la-ressource.namespace.type-de-ressource.domain.tld
.Un petit schéma honteusement pompé de Wikipedia (merci, Wikipedia!):
Nous allons commencer par mettre en place notre propre cluster Kubernetes sur deux petites VMs Debian Stable (bookworm, donc) et une troisième VM pour le storage:
Nos VMs doivent avoir des IPs fixes et des hostnames différents (notamment si clone)!!
Sur nos VMs control-plane et worker, nous installerons aussi le paquet nfs-common
.
Sur notre VM NFS, on installera le paquet nfs-kernel-server
, on créera un répertoire /srv/k0s/nfsprov
et on éditera notre fichier /etc/exports
de la sorte:
/srv/k0s/nfsprov IP_VM1/32(rw,sync,no_subtree_check,no_root_squash,anonuid=1000,anongid=1000)
/srv/k0s/nfsprov IP_VM2/32(rw,sync,no_subtree_check,no_root_squash,anonuid=1000,anongid=1000)
... et ainsi de suite si on a plusieurs workers.
Et pour finir, un petit sudo exportfs -a
pour exporter notre répertoire en NFS à nos VMs kubernetes.
Bien sûr, nous mettrons en place un setup plus costaud sur le cluster de virtualisation de l'EPSI pendant l'atelier, mais ces deux petites machines nous permettront de découvrir tranquillement l'API de Kubernetes. Bien évidemment, si vous avez plus de 8Go de RAM, n'hésitez pas à "gonfler" un peu la conf' côté RAM ou à créer une VM supplémentaire de configuration équivalente (histoire d'avoir deux workers)
Tout d'abord, on va voter entre plusieurs solutions:
On va d'abord installer l'exécutable k0s sur nos deux machines (oui, curl pipé dans sh c'est laid.)
apt install curl iptables
curl -sSLf https://get.k0s.sh | sudo sh
Nous allons tout d'abord configurer notre controlplane. Sur notre VM controlplane:
sudo k0s config create > k0s.yaml
sudo k0s install controller -c k0s.yaml
sudo k0s start
Et... c'est tout! On peut vérifier rapidement que le nécessaire est prêt:
sudo k0s status
Version: v1.27.4+k0s.0
Process ID: 1132
Role: controller
Workloads: false
SingleNode: false
Toujours sur notre controlplane, nous allons créer le token qui va permettre à notre worker de rejoindre le cluster:
sudo k0s token create --role=worker > token
Et le copier rapidement sur notre VM worker, soit via scp, soit via le bon vieux netcat/nc
:
# sur le controlplane:
nc -l -p 1234 < token
# sur le worker:
nc IP_CONTROL_PLANE 1234 > token
Ctrl-C
Maintenant, sur notre worker, il nous suffit de lancer la commande:
sudo k0s install worker --token-file /path/to/token
sudo k0s start
On peut désormais, sur notre controlplane, vérifier le déploiement de notre worker via les commandes:
sudo k0s kubectl get nodes
sudo k0s kubectl get pods -A -o wide
Vous noterez que dans le cas de k0s, notre control-plane n'apparaît pas dans la liste des noeuds disponibles. meh
On va se simplifier un peu la vie, en effet, taper sudo k0s kubectl ...
risque de vite devenir pénible, nous allons donc simplement installer le "vrai" kubectl sur notre controlplane (ou sur votre poste de travail) et récupérer le kubeconfig, le fichier de configuration de l'administrateur du cluster:
# attention le \ est à supprimer si vous copiez ça
# sur une seule ligne ;)
curl -LO "https://dl.k8s.io/release/$(curl -L -s \
https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod 755 kubectl && sudo mv kubectl /usr/local/bin/
mkdir .kube
sudo cp /var/lib/k0s/pki/admin.conf .kube/config
sudo chown -R 1000:1000 .kube
# on teste que tout fonctionne correctement:
kubectl get nodes
Ici, nous allons déployer k0s quasi automatiquement via l'utilitaire k0sctl (https://github.com/k0sproject/k0sctl).
Attention, pour se faire, surtout si vos VMs sont des clones, quelques manipulations sont nécessaires pour préparer nos hôtes:
/etc/machine-id
est unique à chaque VM. Si ce n'est pas le cas, supprimer les fichiers /var/lib/dbus/machine-id
et /etc/machine-id
puis régénérer le machine-id via la commande systemd-machine-id-setup
.man hostnamectl
)authorized_keys
du compte root de vos VMs.Sur votre laptop, dans un répertoire de votre choix, exécutez la commande:
k0sctl init > k0scluster.yaml
Qui va nous générer un fichier yaml (how strange... ) contenant un squelette par défaut, que nous allons modifier pour "coller" à notre setup, exemple:
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
name: k0s-cluster
spec:
hosts:
- ssh:
address: 192.168.1.52
user: root
port: 22
keyPath: /home/lidstah/.ssh/id_rsa
role: controller
- ssh:
address: 192.168.1.53
user: root
port: 22
keyPath: /home/lidstah/.ssh/id_rsa
role: worker
Comme vous pouvez le constater, la syntaxe est extrêmement simple et va nous permettre de déployer k0s sur les machines renseignées avec les rôles désirés.
Attention cependant, pour le moment k0sctl n'est pas capable de supprimer une machine (en ajouter n'est pas un problème).
Afin de déployer k0s sur notre mini-cluster, il ne nous reste dès lors qu'à exécuter la commande:
k0sctl apply --disable-telemetry --config k0scluster.yaml
Une fois le cluster déployé et opérationnel, on va pouvoir récupérer notre fichier kubeconfig:
k0sctl kubeconfig > kubeconfig
# ou, si on veut se simplifier la vie:
k0sctl kubeconfig > ~/.kube/config
On peut désormais utiliser kubectl
soit directement si on a installé notre fichier de configuration de cluster dans ~/.kube/config
soit en précisant le fichier de configuration à la ligne de commande, par exemple pour afficher les noeuds de notre cluster: kubectl --kubeconfig ./kubeconfig get nodes
.
Ce qui devrait nous donner quelque chose du style:
lidstah@vega:~/src/k0sctl$ kubectl --kubeconfig ./kubeconfig get nodes
NAME STATUS ROLES AGE VERSION
k0s-worker1 Ready <none> 79m v1.27.4+k0s
k0s-worker2 Ready <none> 79m v1.27.4+k0s
Alors, c'est très simple:
sur la VM que nous voulons utiliser comme control-plane:
curl -sfL https://get.k3s.io | sh -
et sur les VMs que nous voulons utiliser comme workers:
curl -sfL https://get.k3s.io | K3S_URL=https://myserver:6443 K3S_TOKEN=mynodetoken sh -
Où K3S_URL est donc l'IP de notre control-plane et K3S_TOKEN le token récupéré sur notre control-plane dans le fichier /var/lib/rancher/k3s/server/node-token
.
Et voilà!
Pour pouvoir utiliser kubectl depuis notre control-plane:
mkdir ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown user: ~/.kube/config
chmod 600 ~/.kube/config
echo "export KUBECONFIG=~/.kube/config" >> .bashrc
export KUBECONFIG=~/.kube/config
kubectl get nodes
Talos Linux est une distribution gnu/linux dédiée à Kubernetes. Elle est immutable, pèse ~80Mo, et n'embarque que le strict nécessaire pour exécuter Kubernetes. De plus, la configuration et l'installation des différents nodes est purement déclarative, en YAML. Pas de SSH, pas de shell, mais une API gérée par talosctl
pour manipuler notre cluster, et des fichiers YAML pour décrire nos différentes machines. J'aime beaucoup (mon homelab comme ma prod' chez mes clients utilisent cette distribution). Donc je suis partial
kubectl
est l'outil de manipulation des objets de l'API du Kubernetes par défaut. Il nous permet de créer, détruire, modifier rapidement des objets de l'API mais aussi d'obtenir des informations et même des explications sur ces objets.
Il existe heureusement un module d'autocomplétion des commandes de kubectl
qui va nous permettre de créer un fichier sourçable (par exemple dans notre .bash_profile
) via la commande kubectl completion
. La commande kubectl completion -h
vous fournira de plus amples informations à ce sujet.
kubectl config
et toutes ses sous commandes permettent de manipuler les fichiers de configuration de cluster (.kube/config
,kubeconfig
, etc).
En effet, il arrive plus que souvent de travailler sur plusieurs clusters, ou dans plusieurs namespaces, il est alors primordial de pouvoir facilement changer de contexte (par exemple pour travailler sur un autre cluster ou dans un autre namespace sans avoir à spécifier d'interminables --kubeconfig
et autres -n autre-namespace
).
Il faut savoir que nous pouvons "concaténer" nos fichiers de configuration Kubernetes dans une variable d'environnement, la variable KUBECONFIG
. Dès lors il est très simple d'ajouter ou supprimer des ressources (et donc des contextes) à cette variable.
Imaginons que nous ayons un cluster K0S, dont le fichier kubeconfig se trouve dans ~/src/k0s/kubeconfig
et un cluster Talos Linux dont le kubeconfig se trouve à l'emplacement par défaut, c'est à dire ~/.kube/config
. On peut facilement "concaténer" ces deux fichiers de configuration via la commande (qu'on peut bien évidemment ajouter à nos fichiers de profils habituels):
export KUBECONFIG="${KUBECONFIG}:~/.kube/config:~/src/k0s/kubeconfig"
Dès lors, la commande kubectl config view
va nous indiquer quels contextes représentent tel ou tel cluster:
kubectl config view
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://192.168.1.52:6443
name: k0s-cluster
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://192.168.1.220:6443
name: talos-k8s-cluster
contexts:
- context:
cluster: talos-k8s-cluster
namespace: default
user: admin@talos-k8s-cluster
name: admin@talos-k8s-cluster
- context:
cluster: k0s-cluster
user: admin
name: k0s-cluster
current-context: admin@talos-k8s-cluster
kind: Config
preferences: {}
users:
- name: admin
user:
client-certificate-data: DATA+OMITTED
client-key-data: DATA+OMITTED
- name: admin@talos-k8s-cluster
user:
client-certificate-data: DATA+OMITTED
client-key-data: DATA+OMITTED
On peut voir ici que j'ai deux contextes:
Je peux basculer d'un cluster à l'autre via la commande kubectl config use-context nom-du-contexte
.
Par exemple:
lidstah@vega:~$ kubectl config use-context k0s-cluster
Switched to context "k0s-cluster".
lidstah@vega:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k0s-worker1 Ready <none> 161m v1.27.4+k0s
k0s-worker2 Ready <none> 161m v1.27.4+k0s
lidstah@vega:~$ kubectl config use-context admin@talos-k8s-cluster
Switched to context "admin@talos-k8s-cluster".
lidstah@vega:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
talos-master1 Ready control-plane 409d v1.27.4
talos-master2 Ready control-plane 89d v1.27.4
talos-master3 Ready control-plane 89d v1.27.4
talos-worker1 Ready <none> 502d v1.27.4
talos-worker2 Ready <none> 502d v1.27.4
talos-worker3 Ready <none> 502d v1.27.4
talos-worker4 Ready <none> 502d v1.27.4
talos-worker5 Ready <none> 502d v1.27.4
talos-worker6 Ready <none> 109d v1.27.4
Bien évidemment, il est important dans ce cas de bien se rappeler dans quel context on se trouve! En effet, on ne voudrait pas déployer ou détruire des charges de travail ou des éléments de configuration sur le mauvais cluster
Pour cela, on a toujours la commande:
lidstah@vega:~$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* admin@talos-k8s-cluster talos-k8s-cluster admin@talos-k8s-cluster default
k0s-cluster k0s-cluster admin
Qui nous indique dans quel contexte nous nous trouvons. Toujours vérifier dans quel contexte on est avant de modifier quoi que ce soit!
Si vous avez la flemme ou si votre mémoire flanche - c'est humain - il est possible d'afficher le contexte dans le prompt, mais je vous laisse chercher
La commande kubectl get
suivie d'un nom de ressource, d'un namespace et optionnellement d'un nom complet de ressource permet d'obtenir des informations sur cette ressource ou la liste des ressources correspondantes. Par exemple, si je veux obtenir la liste des namespaces de mon cluster (j'y reviens en détail plus loin), il me suffit de taper:
lidstah@vega:~$ kubectl get namespaces
NAME STATUS AGE
default Active 168m
ingress-nginx Active 66m
k0s-autopilot Active 168m
kube-node-lease Active 168m
kube-public Active 168m
kube-system Active 168m
metallb-system Active 71m
Si par exemple je veux obtenir la liste des pods s'exécutant dans un namespace nommé "enterprise", il me suffit de taper la commande suivante:
lidstah@vega:~$ kubectl get pods -n enterprise
NAME READY STATUS RESTARTS AGE
bookstack-db-5fdbf4695-ws5fn 1/1 Running 0 10d
bookstack-web-5bf54b9dfb-dtvbg 1/1 Running 0 10d
dokuwiki-6879cb56d9-zfp77 1/1 Running 0 10d
doli-db-6b566566cc-5hhf4 1/1 Running 0 10d
doli-web-747446586b-bkwqr 1/1 Running 0 10d
gitea-web-b7465c7d9-42cqk 1/1 Running 0 5d10h
joplin-65899c7f74-8qwmh 1/1 Running 0 3d22h
kanboard-web-699dd7d98f-dp4mb 1/1 Running 0 5d10h
snappy-65d7f49b8-ckrf9 1/1 Running 0 10d
vault-75bf6bc7f7-4tzdg 1/1 Running 0 10d
(tiens d'ailleurs il faut que je vire mon vieux dokuwiki, je l'ai remplacé par bookstack)
La commande kubectl explain
suivie du nom d'une ressource fournit de la documentation sur cette ressource et les éléments qui la définissent (super pratique en cas de trou de mémoire!). Par exemple, si je veux expliquer la ressource Namespace:
kubectl explain namespace
KIND: Namespace
VERSION: v1
DESCRIPTION:
Namespace provides a scope for Names. Use of multiple namespaces is
optional.
FIELDS:
apiVersion <string>
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
kind <string>
Kind is a string value representing the REST resource this object
represents. Servers may infer this from the endpoint the client submits
requests to. Cannot be updated. In CamelCase. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
metadata <ObjectMeta>
Standard object's metadata. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
spec <NamespaceSpec>
Spec defines the behavior of the Namespace. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
status <NamespaceStatus>
Status describes the current status of a Namespace. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
La commande kubectl describe
, optionnellement suivie d'une option définissant le Namespace, suivie du nom générique d'une ressource et du nom de la ressource que l'on veut décrire, permet justement de... hum, décrire la ressource visée.
Par exemple, si je veux décrire le service réseau 'minetest-svc' situé dans l'espace de noms "games", il me suffit de taper:
lidstah@vega:~$ kubectl describe svc -n gamez minetest-svc
Name: minetest-svc
Namespace: gamez
Labels: <none>
Annotations: metallb.universe.tf/ip-allocated-from-pool: ippool
Selector: app=minetest
Type: LoadBalancer
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.101.179.194
IPs: 10.101.179.194
IP: 192.168.1.200
LoadBalancer Ingress: 192.168.1.200
Port: minetest 30000/UDP
TargetPort: 30000/UDP
NodePort: minetest 32683/UDP
Endpoints: 10.244.12.5:30000
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal nodeAssigned 50s (x51 over 10d) metallb-speaker announcing from node "talos-worker6" with protocol "layer2"
Là aussi, une commande très pratique pour vérifier le bon comportement - ou pas! - d'une ressource kubernetes. Les évènements (Events) en fin de sortie permettent souvent de comprendre la cause d'un problème.
La commande kubectl create
suivie d'un type de ressource, d'une option d'espace de nom et du nom d'une ressource permet de créer rapidement une ressource (on utilisera en règle général plutôt du YAML pour ce genre de choses, mais ça peut dépanner).
Par exemple, si je veux créer, sur notre petit cluster k0s, un espace de noms nommé "pwet", il me suffit de taper:
lidstah@vega:~$ kubectl create ns pwet
namespace/pwet created
lidstah@vega:~$ kubectl get ns
NAME STATUS AGE
default Active 3h27m
ingress-nginx Active 105m
k0s-autopilot Active 3h27m
kube-node-lease Active 3h27m
kube-public Active 3h27m
kube-system Active 3h27m
metallb-system Active 109m
pwet Active 21s <<< here it is!
De la même façon, si je veux détruire une ressource, il me suffit d'utiliser la commande kubectl delete
suivie du type de ressource, du namespace de la ressource avec l'option -n, et enfin du nom de la ressource.
Ici, nous allons détruire notre namespace "pwet":
lidstah@vega:~$ kubectl delete ns pwet
namespace "pwet" deleted
ATTENTION: ici le namespace que nous avons détruit est vide, cela n'a donc pas d'incidence, cependant, sachez que lorsqu'on détruit un namespace, on détruit tout ce qu'il contient. Je vous laisse imaginer ce que donnerai un kubectl delete ns production-qui-rapporte-des-sous
dans lequel la prod' de la boite s'exécute
La commande kubectl apply
, suivie de -f et d'un fichier yaml, permet de créer ou modifier des ressources du cluster de façon programmatique. Un fichier yaml peut contenir la définition d'une ou plusieurs ressources (par exemple, un Déploiement wordpress, un statefulset de MariaDB, ainsi que le service et l'ingress permettant de l'exposer à l'extérieur)
La commande kubectl run
nous permet de lancer un pod de façon arbitraire, par exemple un pod nginx dans un namespace "pwet" que nous allons recréer pour l'occasion:
kubectl create ns pwet
kubectl run -n pwet --image nginx nginx-test
kubectl get pod -n pwet
NAME READY STATUS RESTARTS AGE
nginx-test 0/1 ContainerCreating 0 6s
Au bout de quelque secondes, un nouveau kubectl get pod -n pwet
devrait nous afficher:
NAME READY STATUS RESTARTS AGE
nginx-test 1/1 Running 0 78s
(sinon, on a un gros, gros problème )
Maintenant, si j'ai besoin - en général pour débugger, donc on ne fait pas ça en prod! - d'exécuter un shell dans le pod (ou plutôt dans un container du pod), je vais pouvoir le faire, quasiment de la même manière qu'avec docker ou podman, de la manière suivante:
kubectl exec -it -n pwet nginx-test -- /bin/bash
root@nginx-test:/#
# woohoo, born to be roooot!
# sortir avec un "exit"
Alors, est-ce que vous voyez la différence par rapport à Docker ou Podman?
De la même manière, je peux afficher les logs de mon pod avec la commande:
lidstah@vega:~$ kubectl logs -n pwet nginx-test
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
... et patati et patata
/docker-entrypoint.sh: Configuration complete; ready for start up
2023/08/06 21:29:32 [notice] 1#1: using the "epoll" event method
2023/08/06 21:29:32 [notice] 1#1: nginx/1.25.1
2023/08/06 21:29:32 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
2023/08/06 21:29:32 [notice] 1#1: OS: Linux 6.1.0-10-amd64
2023/08/06 21:29:32 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 999999:999999
2023/08/06 21:29:32 [notice] 1#1: start worker processes
2023/08/06 21:29:32 [notice] 1#1: start worker process 29
2023/08/06 21:29:32 [notice] 1#1: start worker process 30
Pour le moment on va laisser "tourner" ce pod, on va en avoir besoin pour la suite!
Et le meilleur pour la fin, pour le moment notre pod nginx n'est pas exposé (heureusement) à l'extérieur. Il n'y a donc pas de moyen apparent d'y accéder. Cependant, avec la commande kubectl port-forward
, je peux obtenir un résultat équivalent à un ssh -L 8080:127.0.0.1:80 user@serveurweb
et accéder à mon pod nginx, s'exécutant dans mon cluster, par exemple en local, voire, l'exposer à tout le sous-réseau auquel je suis connecté ("yolo-mode"):
kubectl port-forward -n pwet pods/nginx-test 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
# et pour l'exposer en mode yolo:
kubectl port-forward -n pwet --address=0.0.0.0 pods/nginx-test 8080:80
Forwarding from 0.0.0.0:8080 -> 80
C'est beau (bon, c'est la page par défaut d'nginx, mais c'est beau quand même).
Allez, on va nettoyer tout ça avec un bon vieux:
lidstah@vega:~$ kubectl delete ns pwet
namespace "pwet" deleted
Il y a encore énormément de possibilités avec kubectl, un bon début pour voir tout ce qu'on peut faire avec cet utilitaire se trouve ici: https://kubernetes.io/docs/reference/kubectl/cheatsheet/
On va pouvoir commencer à écrire (beaucoup) de yaml
Si on a vu comment on peut manipuler les objets de Kubernetes via la commande kubectl, en règle générale on manipule nos objets via des fichiers yaml définissant ces objets. Fichiers que, bien évidemment, nous allons pouvoir versionner, et appliquer à notre cluster via la commande kubectl apply
. Nous passons donc d'une gestion impérative de notre cluster via les différents outils fournis par kubectl à une gestion plus programmatique (et plus dev-friendly) des charges de travail s'exécutant sur notre cluster. De cette manière, en versionnant nos fichiers yaml décrivant l'infrastructure mise en place, il est possible de rapidement remonter un cluster, redéployer nos charges de travail tout aussi rapidement - tant que les datas utilisées par nos charges de travail sont bien là - du fait de la reproducibilité de notre environnement de travail.
Nous avons vu succintement précédemment deux objets de base de Kubernetes: les espaces de noms (Namespaces) ainsi que les Pods. On va les regarder un peu plus en détail, et ensuite on attaquera les morceaux "un peu plus gros". Vous vous doutez bien qu'on ne crée pas nos 192 pods "nginx-php-redis" à la main, quand même?
Tous les objets de l'API Kubernetes peuvent être décrits en YAML de la façon suivante:
apiVersion: version_de_l'api_REST
kind: type_de_ressource
metadata: #métadonnées de l'objet
name:
namespace:
labels:
label1: value
label2: value2
annotations:
annotation1: value
spec: #spécifications de l'objet
key: value
list:
- value1
- value2
spec: #spécification d'un objet imbriqué
env:
- name: "ENVVAR"
value: "value"
- name: "NUMERICVAR"
value: 1234
Absolument tous les objets kubernetes sont définis à partir de ce schéma.
Un Namespace Kubernetes diffère de la notion de namespace (réseau, PID, filesystem, etc) du kernel Linux (namespaces avec lesquels sont construits nos containers du point de vue du kernel) mais a aussi quelques points communs.
Un Namespace (ou espace de noms en français) peut être considéré comme une sorte de "tiroir" dans lequel nous allons "ranger" certaines charges de travail. Imaginons que nous ayons deux clients, la COGIP et la COFRAP, pour lesquels nous mettons à disposition des ERPs dédiés (ex: Dokos, Odoo, etc). Plutôt que d'exécuter les applicatifs des deux clients en question dans le même namespace, nous allons séparer ces applicatifs dans deux namespaces dédiés à chaque client. On pourrait donc avoir un namespace "cogip" et un namespace "cofrap" dans lesquels s'exécuteront les pods ERP, base de données, cache, etc, ainsi que les Services permettant d'exposer ces services à l'extérieur et pour finir les réclamations de volumes persistants utilisés pour la persistence des données de chaque client.
Ce mécanisme permet d'isoler des ressources de différentes façons (un peu comme un LDAP/AD), soit fonctionnelle (un namespace pour les serveurs web, un autre pour les bases de données, etc), soit "nominative" (un namespace pour le client 1, un autre pour le client 2, et ainsi de suite), soit un "mix" des deux (un namespace pour les databases, puis un namespace pour les services web de chaque client).
Par défaut, les pods d'un namespace peuvent "discuter" avec les pods d'un autre namespace, mais il existe des mécanismes d'isolation (les NetworkPolicies que nous verrons ensuite) permettant de limiter le trafic réseau entre différents namespaces, voire de l'interdire complètement.
De même, il est possible de fixer des limites d'utilisation (CPU, RAM) au niveau de l'espace de noms. Par exemple, je pourrai déclarer que les charges de travail du namespace "cogip" ne peuvent utiliser au maximum que 4000 milliCPU (4 vCores) et 12Go de RAM. Cependant ce mécanisme peut avoir certains effets de bords qu'il est bon de garder en mémoire.
En bref, un Namespace nous permet d'isoler des groupes de ressources des autres groupes de ressources s'exécutant dans un autre namespace, pour peu qu'on s'en donne la peine.
Nous avons vu qu'il est trivial de créer ou supprimer un espace de noms via la commande kubectl create ns
ou la commande kubectl delete ns
On peut aussi tout à fait créer un namespace depuis un fichier YAML (vim pwet.yaml
):
apiVersion: v1
kind: Namespace
metadata:
name: pwet
labels:
pod-security.kubernetes.io/enforce: restricted
spec: # pas nécessaire, pas de spec pour les NS
Il nous suffit ensuite d'effectuer un simple kubectl apply -f pwet.yaml
pour créer notre namespace.
Idéalement, c'est le genre de chose qu'on voudra commiter sur un dépôt git/fossil/mercurial/subversion/CVS/whatever
Un petit détail qui ne vous aura sûrement pas échappé, c'est la présence d'un label spécifique sur ce namespace: pod-security.kubernetes.io/enforce: restricted
Suivant votre cluster kubernetes, cette règle sera activée par défaut et elle implique qu'un pod s'exécutant dans cet espace de nom devra s'exécuter en mode non-privilégié (c'est à dire que tous les containers du pod doivent s'exécuter en tant qu'utilisateur non-privilégié et surtout pas en root, doit "drop" toutes les capabilities du kernel (CAP_SYSADMIN, CAP_NETWORK, etc) et ce même pour s'initialiser!). A l'inverse, la présence d'un label pod-security.kubernetes.io/enforce: privileged
indique que ce qui s'exécute à l'intérieur de ce namespace est en mode yolo-root . C'est un ajout de kubernetes 1.25, plus précisément l'ajout du PodAdmissionController en remplacement de l'ancienne PodSecurityPolicy.
On reviendra sur le PodAdmissionController plus loin dans ce module. Pour le moment, si on utilise k0s ou k3s, par défaut tout s'exécute en mode yolo . A l'inverse, si on a voté pour Talos Linux, par défaut tout est en mode restreint!
ATTENTION: la destruction d'un espace de noms provoque la destruction de TOUTES les ressources qu'il contient!!!!
si si, j'insiste!
On en a déjà parlé, mais le pod est en quelque sorte l'unité de base de Kubernetes. Il n'y a pas plus "petit". Un pod, on l'a vu, peut contenir plusieurs conteneurs (inception) qui pourront communiquer entre eux via un "localhost" partagé entre eux (un namespace réseau côté kernel linux).
En règle générale, on exécute rarement des pods "en solo", c'est à dire en dehors d'une unité plus grande (le ReplicaSet), elle même gérée par des objets supérieurs (Déploiements, StatefulSets, et DaemonSets) que nous verrons par la suite.
Cependant, il est là aussi possible de créer un pod solitaire depuis un fichier YAML:
apiVersion: v1
kind: Pod
metadata:
name: nginx-test
namespace: pwet
labels:
app: nginx-test
spec:
containers:
- name: nginx-test
image: nginx:latest
ports:
- name: web
containerPort: 80
protocol: TCP
Ici, la définition est extrêmement simple:
On décrit un pod nommé nginx-test, qui va s'exécuter dans le namespace pwet, qui a un seul label, "app: nginx-test", et dont la spécification défini un seul container ayant pour image "nginx:latest" (par défaut récupérée depuis le docker hub), exposant son port 80 à l'intérieur du cluster, port nommé "web" et acceptant le protocole TCP.
Bien évidemment, en règle générale nos définitions de pods seront plus complexes: on y ajoutera sûrement des points de montage pour stocker nos données, on aura peut-être plusieurs conteneurs à l'intérieur de notre pod, on ajoutera peut-être des règles de sécurité et des limites de ressources, on remplacera peut-être la ligne de commande exécutée au lancement du conteneur par une autre ("command" et "args" dans notre yaml), etc...
Nous verrons tout cela par la suite, mais pour le moment, si vous exécutez un kubectl apply -f nginx-test.yaml
, vous devriez avoir un pod "nginx-test" dans le namespace "pwet".
Un dernier détail, mais les containers s'exécutant à l'intérieur d'un Pod doivent être immutables. Si vous avez besoin d'un paquet spécifique à l'intérieur de vos conteneurs, définissez le lors du build du conteneur! Il est possible de trifouiller les arguments de lancement du pod pour lui faire installer tel ou tel truc (s'il se lance en mode privilégié, beurk), mais c'est ce qu'on appelle un anti-pattern par rapport à l'esprit de Kubernetes.
Un pod peut aussi avoir ce qu'on appelle un "initContainer", c'est à dire un conteneur qui va être lancé uniquement à l'initialisation du pod (par exemple, pour vérifier si le schéma en base de données est bien présent, et le créer si ce n'est pas le cas). Cependant je ne vais pas aborder tout de suite ce sujet, ça doit déjà chauffer un peu pour certain-e-s
Vous l'avez remarqué, j'ai ajouté un label à notre pod. Ce n'est pas obligatoire, cependant, pour qu'un service (voyez le comme un load-balancer à la HAProxy en vraiment plus simple pour le moment) puisse trouver les pods auxquels distribuer les requêtes qu'il reçoit, il doit pouvoir les sélectionner. Cette sélection se fait à l'aide d'un sélecteur, auquel on indique le label de nos pods. Ainsi, notre service pourra "trouver" les pods auxquels distribuer les paquets IPs, segments TCP, datagrammes UDP, ou requêtes HTTP qu'il reçoit.
Alors évidemment, on pourrait s'amuser à créer nos pods les uns après les autres, via des dizaines, voire des centaines de fichiers yaml, les labeliser proprement et créer un service pour les rassembler tous et sur le réseau les lier, mais ça pourrait vite devenir particulièrement pénible d'un point de vue administration du cluster. Sans compter que cela deviendrait bien évidemment excessivement error-prone. Qu'est ce qu'il se passe si j'oublie de mettre à jour l'image du pod n-1 dans son yaml? (bon, en vrai, à coup de sed -i 's/image: pwet:v1/image: pwet:v2/g' *.yaml
y'a moyen que ça passe, cough cough).
Plus sérieusement, pour cela on dispose sous Kubernetes de plusieurs objets. Le premier est le ReplicaSet, qu'on utilisera quasiment - tout comme le pod - jamais directement. Il sera en général créé par un Déploiement, un StatefulSet ou un DaemonSet.
On va tout de même commencer par créer un ReplicaSet pour notre pod nginx précédent (pensez à le détruire avant). Histoire de voir comment c'est fichu
Un ReplicaSet est un objet dont le seul but est de s'assurer que le bon nombre de répliques du même pod s'exécutent dans notre cluster. Si le controller-manager se rend compte que le ReplicaSet untel exécute 2 pods au lieu des 3 demandés, il en informera le serveur API qui commandera au scheduler de lancer un 3ème pod sur un worker disposant des ressources nécessaires. En règle générale, on utilisera des objets de plus haut niveau (notamment les Déploiements, Deployments) plutôt qu'un ReplicaSet directement (c'est sale :p).
Allez, soyons fous, créons notre ReplicaSet pour notre pod nginx. Et on va en coller trois ce coup ci! Créons un fichier replicaset.yaml
:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
namespace: pwet
name: nginx-replica-test
labels:
app: nginx-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx-test
image: nginx:latest
ports:
- name: web
containerPort: 80
protocol: TCP
Ici, on peut remarquer un nouveau détail: le fameux selector dont je vous parlais précédemment. C'est lui qui va indiquer au ReplicaSet quel label rechercher dans nos pods, que nous créons ensuite dans notre template.
Exécutons le ensuite via un kubectl apply -f replicaset.yaml
, et vérifions que notre replicaset ainsi que nos pods sont bien créés:
lidstah@vega:~/src/k0s$ kubectl get pods -n pwet
NAME READY STATUS RESTARTS AGE
nginx-replica-test-2hdzd 1/1 Running 0 12s
nginx-replica-test-7xc5q 1/1 Running 0 12s
nginx-replica-test-9gmr7 1/1 Running 0 12s
lidstah@vega:~/src/k0s$ kubectl get replicasets -n pwet
NAME DESIRED CURRENT READY AGE
nginx-replica-test 3 3 3 33s
Testons que cela fonctionne correctement avec un petit port-forward!
kubectl port-forward -n pwet rs/nginx-test-replicaset 8080:80
C'est toujours aussi moche, mais ça fonctionne. Notre ReplicaSet a simplement sélectionné le premier pod disponible pour lui envoyer notre requête!
Ainsi que nous l'avons déjà vu, le ReplicaSet est en général un objet créé par des objets de plus haut niveau.
Nous allons voir le premier d'entre eux, mais je vous laisse détruire votre ReplicaSet avant
question: quelle commande kubectl utiliserions nous pour détruire ce ReplicaSet?
Un Déploiement (Deployment) est un objet de plus haut niveau qu'un ReplicaSet dont le but est de déclarer un état désiré des ressources qu'il décrit.
Un Déploiement peut être manipulé de façon à effectuer des mises à jour contrôlées de nos ressources (pods ici), et à pouvoir proposer des options de rollback en cas de problème. Il va générer un premier ReplicaSet lors de sa première application, puis un nouveau si l'on met à jour un élément du déploiement, et ainsi de suite. Cela va nous permettre de pouvoir rapidement revenir à un ReplicaSet antérieur en cas de souci, en revenant à la version précédente de notre Déploiement.
Dès lors, notre déploiement va fortement ressembler à un ReplicaSet, mais avec quelques modifications. Créons un fichier deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: pwet
name: nginx-deploy
labels:
app: nginx-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx-test
image: nginx:latest
ports:
- name: web
containerPort: 80
protocol: TCP
On retrouve ici, tout comme pour notre ReplicaSet, le selector qui permettra au ReplicaSet créé par notre Deployment de retrouver ses petits pods .
Regardons un peu ce que notre cluster nous a créé lorsque nous avons effectué un kubectl apply -f deployment.yaml
:
lidstah@vega:~/src/k0s$ kubectl get deploy -n pwet
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deploy 3/3 3 3 7m7s
lidstah@vega:~/src/k0s$ kubectl get rs -n pwet
NAME DESIRED CURRENT READY AGE
nginx-deploy-846b6fdd9 3 3 3 7m12s
lidstah@vega:~/src/k0s$ kubectl get pods -n pwet
NAME READY STATUS RESTARTS AGE
nginx-deploy-846b6fdd9-787qp 1/1 Running 0 7m16s
nginx-deploy-846b6fdd9-wbm62 1/1 Running 0 7m16s
nginx-deploy-846b6fdd9-wtg5r 1/1 Running 0 7m16s
On voit ici que notre Déploiement a bien créé un ReplicaSet correspondant à la définition que nous en avons donné dans la spec de notre Déploiement. Ce ReplicaSet a ensuite créé trois pods nginx correspondants à la définition de ces pods renseignées dans le template de nos pods.
Modifions notre déploiement, pour par exemple passer le nombre de nos replicas à 4 au lieu de 3. Une fois ceci effectué, appliquons directement notre nouveau Déploiement (pas besoin de supprimer l'ancien), et regardons ce qu'il vient de se passer:
lidstah@vega:~/src/k0s$ kubectl get deploy -n pwet
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deploy 4/4 4 4 12m
lidstah@vega:~/src/k0s$ kubectl get rs -n pwet
NAME DESIRED CURRENT READY AGE
nginx-deploy-846b6fdd9 4 4 4 12m
lidstah@vega:~/src/k0s$ kubectl get po -n pwet
NAME READY STATUS RESTARTS AGE
nginx-deploy-846b6fdd9-787qp 1/1 Running 0 13m
nginx-deploy-846b6fdd9-7fvtf 1/1 Running 0 34s
nginx-deploy-846b6fdd9-wbm62 1/1 Running 0 13m
nginx-deploy-846b6fdd9-wtg5r 1/1 Running 0 13m
Ici, comme notre Déploiement ne fait que modifier un ReplicaSet existant pour augmenter le nombre de pods exécutant notre conteneur, rien de spécial.
Modifions maintenant l'image utilisée par notre Déploiement. On va utiliser nginx:stable-alpine au lieu de nginx:latest, et appliquer notre YAML après modification, et enfin regarder ce qu'il se passe du côté du ReplicaSet en spammant kubectl get rs -n pwet
:
lidstah@vega:~/src/k0s$ kubectl apply -f deployment.yaml
deployment.apps/nginx-deploy configured
lidstah@vega:~/src/k0s$ kubectl get rs -n pwet
NAME DESIRED CURRENT READY AGE
nginx-deploy-5467d68df9 1 1 0 4s
nginx-deploy-846b6fdd9 3 3 3 18m
lidstah@vega:~/src/k0s$ kubectl get rs -n pwet
NAME DESIRED CURRENT READY AGE
nginx-deploy-5467d68df9 2 2 1 9s
nginx-deploy-846b6fdd9 2 2 2 18m
lidstah@vega:~/src/k0s$ kubectl get rs -n pwet
NAME DESIRED CURRENT READY AGE
nginx-deploy-5467d68df9 2 2 1 11s
nginx-deploy-846b6fdd9 2 2 2 18m
lidstah@vega:~/src/k0s$ kubectl get rs -n pwet
NAME DESIRED CURRENT READY AGE
nginx-deploy-5467d68df9 3 3 2 13s
nginx-deploy-846b6fdd9 1 1 1 18m
lidstah@vega:~/src/k0s$ kubectl get rs -n pwet
NAME DESIRED CURRENT READY AGE
nginx-deploy-5467d68df9 3 3 3 15s
nginx-deploy-846b6fdd9 0 0 0 18m
Ici, notre Déploiement modifié a créé un nouveau ReplicaSet. Et progressivement remplacé les pods par de nouveaux pods avec la nouvelle image. Mmmmh, c'est intéressant ça, il n'a pas tué tous les pods d'un coup pour en popper de nouveau. Non, il a effectué la migration pod par pod (c'est le comportement par défaut).
Bien sûr, on peut dimensionner "à l'arrache" un déploiement via kubectl
, dans notre cas:
lidstah@vega:~/src/k0s$ kubectl scale deployment -n pwet nginx-deploy --replicas 6
deployment.apps/nginx-deploy scaled
lidstah@vega:~/src/k0s$ kubectl get deploy,pod -n pwet
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx-deploy 4/6 6 4 38s
NAME READY STATUS RESTARTS AGE
pod/nginx-deploy-846b6fdd9-d5jt4 1/1 Running 0 38s
pod/nginx-deploy-846b6fdd9-fkjzz 1/1 Running 0 2s
pod/nginx-deploy-846b6fdd9-fqf9d 0/1 ContainerCreating 0 2s
pod/nginx-deploy-846b6fdd9-l5grb 1/1 Running 0 38s
pod/nginx-deploy-846b6fdd9-rrfs7 1/1 Running 0 38s
pod/nginx-deploy-846b6fdd9-zf7q7 0/1 ContainerCreating 0 2s
On peut aussi automatiser le dimensionnement de notre Déploiement via un Horizontal Pod Autoscaler (HPA), afin que notre déploiement crée automatiquement de nouveaux pods si la charge CPU des pods existants, par exemple, dépasse un certain seuil. Bon là c'est un peu en dehors de notre périmètre, et attention aux effets de bord (il ne faudrait pas "affamer" (starve) les ressources d'autres charges de travail).
Ce qui nous amène à un élément très, très sympa des Déploiements: le RollingUpdate et ses paramètres qui vont nous permettre de gérer la mise à jour de nos pods lorsque nous modifions notre déploiement.
Cependant, est-ce que vous ne voyez pas déjà arriver un problème avec les bases de données, qui vont utiliser des volumes persistants. Qu'est-ce qu'il se passe si deux bases de données utilisent les mêmes fichiers? Il se passe ça:
Ceci dit on évite d'utiliser des Déploiements pour des bases de données, on leur préfèrera le StatefulSet, c'est en général une mauvaise idée d'utiliser un Déploiement pour une base de données.
Ce qui m'amène à la gestion des mises à jour de notre Déploiement, les RollingUpdates (ou pas!)
Une Rolling Update nous permet de mettre à jour les pods d'un déploiement sans aucune coupure du service. En effet, suivant les paramètres que nous allons utiliser, il y aura toujours au moins un pod (ou plus) disponible pour répondre aux requêtes des utilisateurs. Le Service (on va voir ça plus en détail dans la partie réseau), lui, va distribuer les requêtes utilisateurs uniquement aux pods fonctionnels. Ainsi, notre applicatif reste toujours disponible, même lorsqu'on le met à jour.
Côté YAML, on peut modifier le comportement des RollingUpdates d'un Déploiement dans la spec de ce déploiement:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: X
maxUnavailable: Y
Ici, on définit une stratégie de mise à jour de notre déploiement autorisant Y Pods indisponibles et X pods "en plus" de notre nombre de réplicas initialement demandé (pendant la transition). On peut utiliser un nombre arbitraire, ou des pourcentages pour cette valeur (par exemple, je pourrai avoir maxSurge: 50%
qui m'autorisera à avoir 50% de pods en plus par rapport à mon nombre de réplicas désirés pendant la mise à jour)
Par exemple, si j'ai une valeur de replicas: 10
, maxSurge: 5
, maxUnavailable: 1
, alors je pourrai avoir jusqu'à 15 pods pendant ma mise à jour, avec un seul pod de l'ancien ReplicaSet stoppé à la fois.
Les valeurs par défaut sont de 25% pour maxSurge
et maxUnavailable
.
A noter: Si jamais vous utilisez un Déploiement pour une base de données ("shame, shame, shame"), on voudra privilégier une stratégie de type Recreate
et non RollingUpdate
. Ainsi notre base de données sera stoppée proprement avant d'être relancée avec sa nouvelle image de conteneur, et on évite tout potentiel problème où deux pods voudraient accéder aux fichiers de notre précieuse BDD en même temps. Et faire des vilains chocapics avec vos données dans la foulée.
En théorie, avec Kubernetes, quand on a cassé un déploiement, il se passe ça:
En effet, en théorie, on peut rollback notre Déploiement facilement - tant qu'il est parfaitement stateless!!
Revenons à notre Déploiement, et essayons de le rétablir à une version antérieure. Tout d'abord nous allons vérifier l'historique de notre déploiement:
lidstah@vega:~/src/k0s$ kubectl rollout history -n pwet deployment/nginx-deploy
deployment.apps/nginx-deploy
REVISION CHANGE-CAUSE
1 <none>
2 <none>
mmh, ce <none> est embêtant... En effet, nous aurions dû ajouter une annotation à notre déploiement que nous aurions modifié à chaque itération, pour bien faire les choses.
Ceci dit, dans notre cas, il est trivial de revenir à notre ancien déploiement: c'est tout simplement la révision de plus faible identifiant numérique.
(mais j'insiste: pensez à annoter vos déploiements dans le milieu professionnel - et en règle générale votre annotation correspondra au début de votre message de commit - je dis ça comme ça hein mais vous n'imaginez pas comment ça peut être pénible de débugger l'environnement de dev' d'un collaborateur qui n'annote pas ses déploiements... "mmmh, la révision 12, elle date de quel commit dans ta branche?". que du bonheur.)
Regardons une révision de plus près:
lidstah@vega:~/src/k0s$ kubectl rollout history -n pwet deployment/nginx-deploy --revision=2
deployment.apps/nginx-deploy with revision #2
Pod Template:
Labels: app=nginx-test
pod-template-hash=86785c478f
Containers:
nginx-test:
Image: nginx:stable-alpine
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Ah oui, c'est là qu'on est passés à l'image nginx:stable-alpine
. La révision #1 doit donc utiliser l'image nginx:latest
. Allez, rollbackons notre déploiement vers sa première révision!
lidstah@vega:~/src/k0s$ kubectl rollout undo -n pwet deployment/nginx-deploy --to-revision=1
deployment.apps/nginx-deploy rolled back
et vérifions rapidement avec un kubectl describe
de notre déploiement que l'image utilisée par nos pods est bien nginx:latest
! Quelle commande utiliseriez-vous - et pensez à grep
parceque c'est verbeux hein, la preuve:
lidstah@vega:~/src/k0s$ kubectl describe deployments.apps -n pwet nginx-deploy
Name: nginx-deploy
Namespace: pwet
CreationTimestamp: Tue, 08 Aug 2023 00:59:05 +0200
Labels: app=nginx-test
Annotations: deployment.kubernetes.io/revision: 4
Selector: app=nginx-test
Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx-test
Containers:
nginx-test:
Image: nginx:latest
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: nginx-deploy-86785c478f (0/0 replicas created)
NewReplicaSet: nginx-deploy-7b54d68655 (3/3 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 22m deployment-controller Scaled up replica set nginx-deploy-86785c478f to 3
Normal ScalingReplicaSet 21m deployment-controller Scaled up replica set nginx-deploy-7b54d68655 to 1
Normal ScalingReplicaSet 21m deployment-controller Scaled down replica set nginx-deploy-86785c478f to 1 from 2
Normal ScalingReplicaSet 21m deployment-controller Scaled up replica set nginx-deploy-7b54d68655 to 3 from 2
Normal ScalingReplicaSet 21m deployment-controller Scaled down replica set nginx-deploy-86785c478f to 0 from 1
Normal ScalingReplicaSet 7m9s deployment-controller Scaled up replica set nginx-deploy-86785c478f to 1 from 0
Normal ScalingReplicaSet 7m7s deployment-controller Scaled up replica set nginx-deploy-86785c478f to 2 from 1
Normal ScalingReplicaSet 7m7s deployment-controller Scaled down replica set nginx-deploy-7b54d68655 to 2 from 3
Normal ScalingReplicaSet 7m6s deployment-controller Scaled down replica set nginx-deploy-7b54d68655 to 1 from 2
Normal ScalingReplicaSet 7m6s deployment-controller Scaled up replica set nginx-deploy-86785c478f to 3 from 2
Normal ScalingReplicaSet 7m5s deployment-controller Scaled down replica set nginx-deploy-7b54d68655 to 0 from 1
Normal ScalingReplicaSet 64s deployment-controller Scaled up replica set nginx-deploy-7b54d68655 to 1 from 0
Normal ScalingReplicaSet 62s (x2 over 21m) deployment-controller Scaled up replica set nginx-deploy-7b54d68655 to 2 from 1
Normal ScalingReplicaSet 62s (x2 over 21m) deployment-controller Scaled down replica set nginx-deploy-86785c478f to 2 from 3
Normal ScalingReplicaSet 60s (x3 over 61s) deployment-controller (combined from similar events): Scaled down replica set nginx-deploy-86785c478f to 0 from 1
Avouez que c'est quand même bien foutu, non?
Avant d'attaquer les StatefulSets, il va être nécessaire de faire un petit détour par le Storage. En effet pour le moment, tout ce que nous avons fait est purement stateless. Nous ne stockons pas de données autres que temporaires, et nous n'avons pas abordé le problème de la persistence des données. On va se servir de notre VM NFS, finalement
Maintenant qu'on a joué avec Helm et installé nfs-subdir-external-provisioner avec Helm, et vérifié que ce déploiement nous avait ajouté une classe de stockage (StorageClass) nommée nfs-client
, on va pouvoir regarder de plus près ce qu'est un Statefulset!
Un Statefulset, c'est un objet Kubernetes semblable au déploiement, qui va donc gérer la création, la réplication, la mise à jour de Pods, mais de pods avec états (stateful). C'est à dire de pods qui vont stocker des données qui ne doivent pas être partagées entre eux. Comme par exemple une base de données Postgresql avec deux pods, un master et un replica (ou plusieurs replica). Si les données que ces pods utilisent sont - sensiblement - identiques, chaque pod doit disposer de son propre espace de stockage dédié pour stocker ces données.
Un StatefulSet crée donc des pods:
On va se créer un premier StatefulSet avec, au hasard, une bonne vieille MariaDB, histoire de changer. On va se créer un Namespace dédié à notre bdd, qu'on va appeler "maria" (ou tout autre nom, si vous voulez l'appeler pwet... mais dans ce cas adaptez )
apiVersion: v1
kind: Service
metadata:
name: maria-svc
namespace: maria
labels:
app: mariadb-test
spec:
ports:
- port: 3306
name: mariadb
protocol: TCP
clusterIP: None
selector:
app: mariadb-test
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mariadb-test
namespace: maria
spec:
selector:
matchLabels:
app: mariadb-test
serviceName: "maria-svc"
replicas: 1
template:
metadata:
labels:
app: mariadb-test
spec:
terminationGracePeriodSeconds: 10
containers:
- name: mariadb
image: mariadb:10.5
env:
- name: "MARIADB_ROOT_PASSWORD"
value: "epsiepsi"
- name: "MARIADB_DATABASE"
value: "epsi"
- name: "MARIADB_PASSWORD"
value: "epsiepsi"
- name: "MARIADB_USER"
value: "epsi"
ports:
- containerPort: 3306
name: mariadb
protocol: TCP
volumeMounts:
- name: maria
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: maria
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "nfs-client"
resources:
requests:
storage: 2Gi
Vérifions que tout est bien lancé:
lidstah@vega:~/src/k0s$ kubectl get sts -n maria
NAME READY AGE
mariadb-test 1/1 2m32s
lidstah@vega:~/src/k0s$ kubectl get svc -n maria
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
maria-svc ClusterIP None <none> 3306/TCP 2m38s
lidstah@vega:~/src/k0s$ kubectl get pod -n maria
NAME READY STATUS RESTARTS AGE
mariadb-test-0 1/1 Running 0 2m44s
lidstah@vega:~/src/k0s$ kubectl get pvc -n maria
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
maria-mariadb-test-0 Bound pvc-0d1621ea-e721-411a-ad3a-bf19dc726de1 2Gi RWO nfs-client 2m49s
Seems legit
Testons si nous pouvons accéder à notre base de données via un kubectl port-forward
que je vous laisse taper
Normalement, si on a le client mysql (ou tout autre client mysql, graphique ou pas), nous devrions pouvoir nous connecter à notre base de donnée en local via un mysql -h127.0.0.1 -uroot -pepsiepsi
Ce qui devrait nous donner ça:
It works!©
Par contre, nous avons maintenant un petit problème de sécurité concernant les identifiants de notre base de données, le voyez-vous dans le YAML que nous venons de créer? (hormis les mots de passe pourris, of course).
Ce problème est aussi présent sur un kubectl describe de notre StatefulSet:
lidstah@vega:~/src/k0s$ kubectl describe sts -n maria mariadb-test
Name: mariadb-test
Namespace: maria
[snip]
Update Strategy: RollingUpdate
Partition: 0
Pods Status: 1 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=mariadb-test
Containers:
mariadb:
Image: mariadb:10.5
Port: 3306/TCP
Host Port: 0/TCP
Environment:
MARIADB_ROOT_PASSWORD: epsiepsi
MARIADB_DATABASE: epsi
MARIADB_PASSWORD: epsiepsi
MARIADB_USER: epsi
Mounts:
/var/lib/mysql from maria (rw)
Volumes: <none>
[snip]
Nous verrons comment remédier à ce problème dans la partie Sécurité, et notamment la partie concernant les Secrets Kubernetes.
Un DaemonSet est une sorte de déploiement qui va déployer un pod et un seul sur chaque worker de notre cluster. En gros, si je crée un DaemonSet nginx, il va me déployer un pod nginx sur chaque worker de mon cluster.
L'utilité peut sembler minimale, mais en fait c'est assez utilisé notamment par:
La syntaxe est identique à celle d'un Deployment à l'exception du paramètre kind
qui aura pour valeur, et bien évidemment, le paramètre replicas
ne s'applique pas vu qu'il n'a aucun sens dans ce contexte :
kind: DaemonSet
Bien évidemment, via les cgroups (Control Groups) du Kernel Linux, nous pouvons assigner des ressources CPU et mémoire minimales (requests) et maximales (limits) à nos pods (et donc, aux pods générés par nos Déploiements, StatefulSets et Daemonsets), afin d'être sûr qu'un pod disposera toujours des ressources minimales nécessaires à son fonctionnement, ou qu'il ne dépassera jamais certaines limites CPU et RAM afin de ne pas "paralyser" d'autres pods s'exécutant sur le même noeud de notre Cluster.
Les requêtes comme les limites sont des ressources réservées c'est à dire qu'elles vont déterminer si d'autres pods vont pouvoir être exécutés sur un noeud du cluster ou pas, il est donc important de bien dimensionner ces paramètres afin d'éviter des effets de bords désagréables (par exemple, avoir des requêtes ou des limites trop élevées, qui ne seront jamais utilisées par le pod mais qui empêchent d'assigner d'autres charges de travail sur le ou les noeuds du cluster concernés).
En règle générale, si on utilisera souvent les requests pour déterminer les ressources minimales à l'exécution d'un pod, on évitera d'utiliser les limites sauf cas spéciaux (notamment applicatif avec fuite de mémoire).
Par exemple, si on veut assigner à notre déploiement nginx-test précédent des ressources de ce type:
On modifiera notre déploiement de la sorte:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: pwet
name: nginx-deploy
labels:
app: nginx-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx-test
image: nginx:latest
ports:
- name: web
containerPort: 80
protocol: TCP
resources:
requests:
cpu: "10m"
memory: "32Mi"
limits:
cpu: "50m"
memory: "64Mi"
Il y a plusieurs réseaux à l'intérieur d'un cluster Kubernetes:
Pour que les pods et services puissent discuter entre eux d'un node à l'autre, les réseaux internes du cluster sont généralement encapsulés dans des VxLANs (en gros, du VLAN over UDP - ce qui explique la différence de MTU entre un lien interne et la MTU des nodes), mais on peut trouver d'autres solutions (VLANs classiques dédiés par exemple).
Tout Pod, Service dispose de son enregistrement DNS "personnel". En effet, un même pod peut changer d'adresse IP entre deux versions d'un déploiement, un service peut changer d'IP si on le modifie ou si on redémarre le cluster, alors qu'ils auront toujours le même nom DNS.
En règle générale, Kubernetes utilise CoreDNS (2 pods sont normalement créés) comme serveur DNS interne. Le nom de domaine du cluster, défini à sa création, est par défaut "cluster.local".
Un pod "nginx" dans un espace de noms "web" aura pour FQDN nginx.web.pod.cluster.local., et un service "nginx-svc" dans le même espace de nom aura pour FQDN nginx-svc.web.svc.cluster.local..
On peut modifier les paramètres DNS d'un pod. Par défaut, un pod effectuera ses recherches DNS d'abord via CoreDNS (le DNS interne de kubernetes), puis via le ou les serveurs DNS définis sur l'hôte. C'est la politique DNS ClusterFirst.
Il en existe 4:
dnsConfig
de sa spec'.Exemple avec une dnsPolicy "None" et en configurant nous mêmes les serveurs DNS utilisés par le pod:
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- 192.168.1.1
searches:
- bidule.com
- bidule.net
options:
- name: ndots
value: "5"
- name: edns0
containers:
- name: nginx
image: nginx:v1.22.0
[... suite habituelle ...]
Pour le moment, nous n'avons rien exposé à l'extérieur de notre cluster. Ni même, en réalité, à l'intérieur de notre cluster. Nous avons pu accéder à nos applicatifs de tests via la commande kubectl port-forward
, qui, si elle est très pratique pour débugger, tester, etc, ne nous permettrait pas d'exposer des services à l'extérieur de façon fiable (on pourrait imaginer un kubectl port-forward sur une machine faisant tourner haproxy ou autre load-balancer, mais il vaut mieux que cela reste du domaine de l'imaginaire ).
Pour exposer nos services, soit à l'intérieur, soit à l'extérieur du cluster, Kubernetes dispose d'un objet appelé... Service . Il fonctionne un peu comme un Load-Balancer. Si j'ai un déploiement avec 3 pods, par exemple, et un Service lié à ce Déploiement, exposé à l'intérieur du cluster avec le FQDN
nginx-svc.pwet.svc.cluster.local
et écoutant sur le port 80, alors tout pod du cluster (minus NetworkPolicy), peut accéder à ce service (et donc aux pods derrière).
Pour "trouver" les pods où envoyer les requêtes qu'il reçoit, un Service utilise un sélecteur (selector
), en général pointant un label présent dans la définition des pods que l'on veut atteindre via ce Service, et un ou plusieurs ports sur lesquels écouter, ainsi que des ports cible (targerPort
) où envoyer ce qui est reçu sur le port d'écoute du service.
Un template YAML de Service (ici de type ClusterIP) ressemble donc généralement à ça:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
namespace: pwet
spec:
type: ClusterIP # ou autre, cf plus bas
selector:
app: nginx-test
ports:
- name: web
port: 80
targetPort: 80
protocol: TCP
- name: https
port: 443
targetPort: 443
protocol: TCP
C'est le type de service par défaut. Il attribue au Service une adresse IP dans le réseau interne du cluster dédié aux services. On peut préciser cette adresse manuellement (mais bien sûr il ne faut pas utiliser une IP déjà attribuée ), mais en général il est mieux de laisser le cluster décider.
On utilise en général ce type de service pour exposer des applicatifs de façon interne au cluster, applicatifs que nous ne voulons pas exposer à l'extérieur (exemple: bases de données, cache (redis, varnish, etc)).
Le but d'un LoadBalancer est d'assigner une (ou plusieurs) IP "publique" (i.e extérieure aux réseaux internes de Kubernetes et donc joignable de l'extérieure) à un Service, afin de pouvoir y accéder sans utiliser le port-forwarding de kubectl.
En règle générale, votre fournisseur de solution cloud vous proposera un Load-Balancer "maison" bien évidemment payante (ou gratuite pour la première IP publique, auquel cas un Ingress s'imposera rapidement ).
MetalLB, lui, est un load-balancer dédié aux installations baremetal de Kubernetes. Il en existe d'autres, par exemple svc-lb utilisé par k3s (qui assigne à chaque service les IPs des nodes composant le cluster. Ce qui veut dire que l'on ne peut avoir qu'un service écoutant sur un port donné (ex: 8080)), les ports 80 et 443 étant déjà réservés pour l'Ingress de k3s (Traefik). L'avantage de MetalLB ici, est qu'on peut définir un ou plusieurs pools d'adresses IPs (liste d'adresses IP, bloc (/24, /16, etc), ou intervalle (192.168.100.10 - 192.168.100.50)) à assigner soit manuellement soit automatiquement à nos Services.
Il est capable d'annoncer les adresses assignées à nos Services soit en L2 OSI via ARP, soit en L3 OSI via BGP (classe!).
Son installation peut se faire soit via Helm (qu'on verra plus tard), soit directement via un fichier YAML téléchargeable sur le site de metalLB: https://metallb.universe.tf/ pour le site et la documentation, et https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml pour la version actuelle. Un simple wget
du YAML suivi d'un kubectl apply -f
du fichier téléchargé suffira à l'installer sur un cluster Kubernetes on-prem.
Il est composé de deux éléments installés par défaut dans le Namespace metallb-system:
On le verra dans la partie Sécurité et notamment RBAC, mais l'installation de metalLB crée aussi les comptes de service (serviceaccounts) nécessaires à la modification des configurations réseau et iptables des nodes, et l'ensemble s'exécute en mode privilégié avec les Capabilities du Kernel permettant la manipulation de la pile réseau du noyau Linux.
On va vouloir ensuite configurer deux choses:
Imaginons que je veuille pouvoir assigner (indifféremment - metalLB prendra la première adresse disponible si on ne précise pas manuellement l'IP à assigner au service) des IPs des blocs et intervalles suivants via metalLB, en ARP:
Mon fichier YAML définissant ces deux ressources (les pools et l'annonce) aura donc pour forme:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: ippoolL2
namespace: metallb-system
spec:
addresses:
- 172.16.250.0/24
- 192.168.100.50-192.168.100.60
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: ippool-l2-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- ippoolL2
Il me suffit donc d'appliquer via kubectl apply -f
ce fichier YAML pour créer mes pools d'adresses IP et leur annonce sur le réseau "public".
À partir de ce moment, je peux assigner une IP "publique" à mes Services de type LoadBalancer. Si je reprends mon fichier YAML précédent définissant un Service de type ClusterIP et que je le modifie pour assigner l'IP 192.168.100.55 à mon service nginx-svc:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
namespace: pwet
spec:
type: LoadBalancer
loadBalancerIP: 192.168.100.55
selector:
app: nginx-test
ports:
- name: web
port: 80
targetPort: 80
protocol: TCP
- name: https
port: 443
targetPort: 443
protocol: TCP
Je devrais pouvoir accéder à mon service nginx-svc (puis aux pods labellés "nginx-test" écoutant derrière) via les ports 80 et 443, à l'adresse IP 192.168.100.55 (pour autant que j'ai accès à ce sous-réseau soit directement, soit via un routeur).
Évidemment, si j'ai plusieurs services que je veux exposer à l'extérieur via cette méthode, il faut que j'utilise plusieurs adresses IPs "publiques", ce qui peut être rédhibitoire notamment si on utilise de vraies adresses IP publiques (ça coûte cher), ou si on utilise un cluster "dans le cloud" (ça coûte encore plus cher!). Cependant sur un réseau RFC1918 dont on a le contrôle, cela peut permettre une grande flexibilité. Et si on veut ensuite accéder à ces services depuis l'extérieur (Internet, autre réseau interne, etc), il suffit d'utiliser une VM load-balancer/reverse-proxy avec par exemple HAProxy ou Nginx (qui pourront dans la foulée s'occuper de la terminaison TLS pour de l'HTTPS).
Par contre, si pour diverses raisons on ne dispose que d'un nombre restreint d'adresses IPs "publiques" (voire une seule) et/ou si on veut terminer la liaison TLS à l'entrée de notre cluster, assigner cette IP (via notre LoadBalancer) à un service d'Ingress sera plus pertinent.
Si on a pas d'autre possibilité, on peut aussi utiliser le Service de type NodePort pour rendre nos Services joignables de l'extérieur. Par défaut, Kubernetes assigne la plage de ports 30000 à 32767 à ces Services, qui deviennent joignables à l'IP:Port de chaque noeud de notre cluster.
Par exemple:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
namespace: pwet
spec:
type: NodePort
selector:
app: nginx-test
ports:
- name: web
port: 80
targetPort: 80
protocol: TCP
nodePort: 30010
Ici, je pourrai accéder à mes pods nginx via mon Service de type NodePort depuis les adresses IPs de chaque membre de mon cluster, au port 30010.
Cependant, il est en général considéré comme une mauvaise pratique d'utiliser NodePort en production:
Un EndPointSlice contient les références d'un ensemble d'endpoints (en général, les ClusterIPs des pods d'un même déploiement lié à un Service). C'est un objet créé automatiquement par le cluster Kubernetes. Par exemple:
lidstah@vega:~/src/k0s$ kubectl get endpointslices -n maria
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
maria-svc-9szg4 IPv4 3306 10.244.1.12 87s
# un petit describe?
lidstah@vega:~/src/k0s$ kubectl describe endpointslices -n maria maria-svc-9szg4
Name: maria-svc-9szg4
Namespace: maria
Labels: app=mariadb-test
endpointslice.kubernetes.io/managed-by=endpointslice-controller.k8s.io
kubernetes.io/service-name=maria-svc
service.kubernetes.io/headless=
Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2023-08-11T16:16:42Z
AddressType: IPv4
Ports:
Name Port Protocol
---- ---- --------
mariadb 3306 TCP
Endpoints:
- Addresses: 10.244.1.12
Conditions:
Ready: true
Hostname: mariadb-test-0
TargetRef: Pod/mariadb-test-0
NodeName: k0s-worker2
Zone: <unset>
Events: <none>
Un Ingress en terminologie Kubernetes, se comporte comme les reverse-proxies que l'on connaît bien (Nginx, HAproxy, Traefik, etc). La majorité des Ingresses disponibles sont d'ailleurs basés sur des reverses proxies connus:
Un des Ingresses les plus utilisés étant ingress-nginx, c'est ce que nous allons utiliser.
Un Ingress, par défaut, utilisera le Header Host des requêtes de l'utilisateur pour déterminer à quels services internes (et donc quels pods) envoyer le trafic qu'il reçoit. Par exemple:
Si on ne dispose pas d'enregistrements DNS/ de serveur DNS, on peut utiliser notre fichier hosts (UNIX et Linux: /etc/hosts
, Windows: C:\Windows\System32\drivers\etc\hosts
) pour établir la corrélation IP <-> FQDN, par exemple:
192.168.100.55 nginx.mondomaine.com
L'installation est là aussi très simple, soit on utilise Helm, soit on utilise le YAML officiel d'ingress-nginx:
Le YAML va créer les différentes ressources nécessaires au fonctionnement d'ingress-nginx, parmi lesquelles:
Reprenons notre service "nginx-svc" (de type ClusterIP) dans son namespace "pwet", pointant vers les pods créés par notre Déploiement "nginx-test". Nous voulons l'exposer via notre Ingress, et y accéder depuis le FQDN nginx.mondomaine.com
(cf enregistrement hosts plus haut).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-nginx
namespace: pwet
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
# je mets la configuration TLS pour l'exemple
tls:
- hosts:
- nginx.mondomaine.com
secretName: mon-secret-tls
# règles de routage:
rules:
- host: nginx.mondomaine.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-svc
port:
number: 80
Les règles tls sont ici données pour l'exemple. On peut utiliser cert-manager
pour générer automatiquement des certificats auto-signés, ou via Let's Encrypt, ou on peut fournir manuellement à ingress-nginx un Secret de type TLS contenant notre certificat et sa clef privée afin qu'il l'utilise pour accéder à notre ressource en https. De plus, il effectue la redirection 3xx automatiquement. Si on dispose d'un certificat de type wildcard, on peut l'utiliser comme certificat par défaut en modifiant le déploiement d'ingress-nginx et en ajoutant la ligne dans les arguments du pod:
--default-ssl-certificate=namespace/mon-certificat-wildcard
Pour indiquer à Ingress-nginx d'utiliser le secret "mon-certificat-wildcard" se trouvant dans l'espace de noms "namespace" si je ne fournis pas de certificat dans la définition de mon Ingress.
Ici, on voit que mes metadonnées ont un champ annotations, qui détermine la classe de mon Ingress. Je pourrai aussi ajouter à ces annotations des paramètres propres à nginx (CORS, taille d'upload maximale, etc).
Bien évidemment, si j'ai plusieurs services dans le même namespace que je voudrais exposer avec mon Ingress, je peux simplement les ajouter à la liste "rules".
Je peux aussi jouer sur le "Path". Par exemple "/" m'enverrai vers mon service "nginx-svc" et un path "/login" m'enverrai vers mon service "login-ui-svc", et ainsi de suite.
On voit donc qu'avec un Ingress, on peut facilement exposer une multitude de services Web (mais aussi TCP et UDP, cf la documentation d'ingress-nginx) en utilisant une seule adresse IP, et donc en réduisant les coûts notamment dans le cloud.
Les stratégies réseau (NetworkPolicies, a.k.a NetPols) permettent de déterminer quels échanges réseaux sont autorisés entre pods, entre namespaces et entre services. On y reviendra plus en détail dans la partie "Sécurité" de ce module.
Tout d'abord, il faut savoir que les containers qui composent nos pods sont censés être immutables. Ce qui signifie que si nous supprimons un pod (lors d'une RollingUpdate d'un Déploiement par exemple), les données que nous aurions pu stocker "dans" le pod seront supprimées. Dès lors, il était nécessaire de trouver un moyen d'assurer la persistence des données générées par nos pods (bases de données, données de site, etc).
Pour cela, Kubernetes est capable d'utiliser directement NFS ou iSCSI, mais aussi d'utiliser des classes de stockage permettant de provisionner des volumes de stockage à la volée et d'appliquer des règles de rétention de données (ou pas ), d'utiliser du stockage distant ou local, avec ou sans réplication des données stockées.
Une StorageClass (Classe de Stockage) est une ressource définissant les paramètres de configuration d'un stockage (local ou distant) et fournissant une abstraction au cluster afin de pouvoir provisionner des Volumes Persistants (PV, PersistentVolume) automatiquement via des PersistentVolumeClaims (PVC).
On peut citer:
Il existe plusieurs modes d'accès à un volume:
Un Volume Persistant, PV, est une ressource du cluster qui peut être créée automatiquement par une StorageClass, ou manuellement par un opérateur humain (NFS ou iSCSI).
Cet objet est sans Namespace. Lorsqu'il est créé manuellement, il est de la forme:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-name
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy:
Retain
nfs:
path: /path/of/nfsmount/folder
server: 10.10.10.10 #IP or FQDN of NFS server
readOnly: false
---
Avantages de la méthode ci-dessus:
Désavantages:
Une PersistentVolumeClaim (PVC, Réclamation de Volume Persistent) permet de lier un PV à un ou plusieurs pods (suivant le mode d'accès). Contrairement au PV, une PVC est une ressource "Namespacée". Attention, dans le cas de la création de plusieurs PVs et PVCs manuellement, les PVs et PVC sont liés en mode "premier servi, premier lié", ce qui peut engendrer des problèmes (PVC s'étant liée avec un volume plus petit que le volume prévu). Dès lors, il est important dans ce cas de bien spécifier dans la PVC le nom du PV auquel on veut se lier via la directive "volumeName":
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pv-name-pvc
namespace: enterprise
spec:
volumeName: "pv-name"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
Dans le cas de l'utilisation d'une StorageClass - dans l'exemple ci-dessous la SC "longhorn" - la déclaration de la PVC entraînera automatiquement la création du PV correspondant:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myvolume-pvc
namespace: enterprise
spec:
storageClassName: "longhorn"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
La suppression d'une PVC n'entraîne pas nécessairement la suppression du PV correspondant. Cela dépend de la Reclaim Policy configurée lors de la création du PV (ou configuré par la StorageClass): Retain ou Delete.
Bien évidemment, il est important de bien s'assurer que la bonne ReclaimPolicy - ou que l'on dispose de backups vérifiés - est utilisée avant toute suppression de PVC
Pour monter notre volume dans le pod, il faut tout d'abord le déclarer dans la spec du template des pods de notre Déploiement/Statefulset/Daemonset:
kind: Deployment
metadata:
# snip
spec:
# snip
template:
metadata:
labels:
mylabel: thisisalabel
spec:
securityContext:
# snip
volumes:
- name: my-volume-name
persistentVolumeClaim:
claimName: my-pvc-name
Ensuite, on déclare notre volume et son point de montage dans la définition du container voulu (si notre pod en compte plusieurs) de notre pod:
containers:
- name: containername
image: myrepo/myimage:mytag
securityContext:
#snip
ports:
- containerPort: 80
protocol: TCP
volumeMounts:
- name: my-volume-name
mountPath: /my/path/to/mount/my/volume
Et voilà