Dans ce nouveau volet, nous allons explorer des apsects encore plus avancés de Kubernetes, toujours au travers du mini-cluster packagé Minikube :
Il est recommandé de faire un fresh start du cluster en le supprimant :
minikube delete
puis en relançant le cluster minikube :
minikube start
kubectl create configmap app-config --from-literal=APP_ENV=production --from-literal=APP_DEBUG=false
Créez un fichier config.properties
:
nano config.properties
avec :
app.env=production
app.debug=false
app.log.level=info
Puis créez depuis ce fichier le ConfigMap appelé... configmap
tout simplement
kubectl create configmap app-config-file --from-file=config.properties
Changeons de format, passons de clé-valeur à des fichiers.
Créez un fichier nginx-config.yaml
:
nano nginx-config.yaml
avec :
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
nginx.conf: |
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>Kubernetes ConfigMap Example</title>
</head>
<body>
<h1>Hello from Kubernetes ConfigMap!</h1>
</body>
</html>
Nous voyons que ce manifeste définit le contenu de deux fichiers : nginx.conf
et index.html
.
Appliquez le ConfigMap :
kubectl apply -f nginx-config.yaml
kubectl get configmaps
ou encore en forme abrégée :
kubectl get cm
kubectl describe configmap <nom-du-configmap>
Par exemple
kubectl describe configmap app-config
Créez un fichier nginx-pod-configmap.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: nginx-configmap
spec:
containers:
- name: nginx
image: nginx:1.20
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config-volume
mountPath: /etc/nginx/conf.d
- name: nginx-index-volume
mountPath: /usr/share/nginx/html
volumes:
- name: nginx-config-volume
configMap:
name: nginx-config
items:
- key: nginx.conf
path: default.conf
- name: nginx-index-volume
configMap:
name: nginx-config
items:
- key: index.html
path: index.html
Prenez le temps de bien faire le lien entre les éléments déclarés et le fichier précédent :
nginx-configmap
contient un container nginx
basé sur l'image nginx:1.20
nginx-config-volume
(relié au répertoire /etc/nginx/conf.d
dans le conteneur)nginx-index-volume
(relié à /usr/share/nginx/html
dans le conteneur)nginx-config
Appliquez :
kubectl apply -f nginx-pod-configmap.yaml
et testez en exposant temporairement ce pod :
kubectl port-forward nginx-configmap 8080:80
Depuis le navigateur, accédez à la page http://localhost:8080
:
Créez un fichier env-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: env-pod
spec:
containers:
- name: env-container
image: alpine
command: ["sh", "-c", "echo APP_ENV=$APP_ENV && sleep 3600"]
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
restartPolicy: Never
Appliquez :
kubectl apply -f env-pod.yaml
et vérifiez :
kubectl logs env-pod
kubectl create secret generic db-credentials --from-literal=username=admin --from-literal=password=secretpass
Créez des fichiers pour vos informations sensibles :
echo -n "admin" > username.txt
echo -n "secretpass" > password.txt
kubectl create secret generic db-credentials-file --from-file=username=username.txt --from-file=password=password.txt
Créez un fichier secret.yaml
:
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
data:
api-key: VGhpc0lzTXlBUElLZXk= # 'ThisIsMyAPIKey' encodé en base64
api-token: VGhpc0lzTXlBUElUb2tlbg== # 'ThisIsMyAPIToken' encodé en base64
Rappel - pour encoder en base64 :
echo -n "ThisIsMyAPIKey" | base64
echo -n "ThisIsMyAPIToken" | base64
Appliquez le Secret :
kubectl apply -f secret.yaml
Créez un fichier secret-env-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: secret-env-pod
spec:
containers:
- name: secret-container
image: alpine
command: ["sh", "-c", "echo The API Key is $API_KEY && sleep 3600"]
env:
- name: API_KEY
valueFrom:
secretKeyRef:
name: api-secrets
key: api-key
restartPolicy: Never
Appliquez :
kubectl apply -f secret-env-pod.yaml
et vérifiez :
kubectl logs secret-env-pod
Créez maintenant un fichier secret-volume-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: secret-volume-pod
spec:
containers:
- name: secret-container
image: alpine
command: ["sh", "-c", "echo -n 'Contenu de api-key : '; cat /etc/secrets/api-key; echo && sleep 3600"]
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: api-secrets
restartPolicy: Never
Appliquez :
kubectl apply -f secret-volume-pod.yaml
et vérifiez :
kubectl logs secret-volume-pod
Dans un cluster Kubernetes, plusieurs applications partagent les ressources physiques des nœuds. Sans limites appropriées, une application gourmande en ressources pourrait :
La gestion des ressources permet d'optimiser l'utilisation du cluster et d'augmenter sa fiabilité.
Kubernetes utilise deux concepts distincts pour gérer les ressources :
Requests (Demandes)
Limits (Limites)
Exemples :
cpu: "500m" # 500 millicores (0.5 CPU)
cpu: "1" # 1 CPU (équivalent à 1000m)
cpu: "1.5" # 1.5 CPU (équivalent à 1500m)
Exemples :
memory: "256Mi" # 256 mebibytes
memory: "1Gi" # 1 gibibyte
memory: "512M" # 512 megabytes (à éviter, préférer Mi)
Kubernetes attribue automatiquement une classe QoS à chaque pod, qui détermine sa priorité d'éviction en cas de pression sur les ressources :
En environnement cloud, il est important de noter que :
Créez un fichier resource-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: resource-container
image: nginx
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
Appliquez :
kubectl apply -f resource-pod.yaml
et vérifiez :
kubectl describe pod resource-demo
Nous allons utiliser une image spéciale pour le stress mémoire.
Créez un fichier memory-test-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: memory-test
spec:
containers:
- name: memory-container
image: polinux/stress
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "150M", "--vm-hang", "1"]
resources:
limits:
memory: "128Mi"
restartPolicy: Always
Appliquez :
kubectl apply -f memory-test-pod.yaml
Observez les redémarrages :
kubectl get pod memory-test -w
Vérifiez les événements :
kubectl describe pod memory-test
Enfin, supprimons ce pod au comportement désastreux :
kubectl delete pod memory-test
Allons au-delà du pod, avec maintenant un déploiement sur trois replicas.
Créez un fichier resource-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: resource-deployment
spec:
replicas: 3
selector:
matchLabels:
app: resource-app
template:
metadata:
labels:
app: resource-app
spec:
containers:
- name: resource-container
image: nginx
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
Appliquez :
kubectl apply -f resource-deployment.yaml
et vérifiez :
kubectl describe deployment resource-deployment
Kubernetes propose plusieurs types de volumes pour répondre à différents besoins de stockage :
volumes:
- name: data-volume
emptyDir: {}
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-example
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-example
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
# Dans la définition du pod
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: pvc-example
Ce modèle en trois couches (Volume, PVC, PV) permet de séparer les préoccupations entre développeurs et administrateurs tout en offrant une flexibilité dans la gestion du stockage.
Créons un pod avec deux conteneurs qui vont partager un volume (shared-data). L'un (writer) va écrire des données, l'autre (reader) va les lire.
Créez un fichier emptydir-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: emptydir-pod
spec:
containers:
- name: writer
image: alpine
command: ["/bin/sh", "-c", "while true; do echo $(date) >> /data/output.txt; sleep 10; done"]
volumeMounts:
- name: shared-data
mountPath: /data
- name: reader
image: alpine
command: ["/bin/sh", "-c", "while true; do cat /data/output.txt; sleep 15; done"]
volumeMounts:
- name: shared-data
mountPath: /data
volumes:
- name: shared-data
emptyDir: {}
La gymnastique habituelle :
Appliquez :
kubectl apply -f emptydir-pod.yaml
Vérifiez :
watch kubectl logs emptydir-pod -c reader
Nous allons créer ces deux ressources au sein d'un même manifeste.
pv-pvc.yaml
:apiVersion: v1
kind: PersistentVolume
metadata:
name: task-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: task-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
Appliquez et vérifiez :
kubectl apply -f pv-pvc.yaml
kubectl get pv
kubectl get pvc
Nous allons maintenant utiliser ce volume persistant au sein d'un pod. Créez un fichier pvc-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: pvc-pod
spec:
containers:
- name: pvc-container
image: nginx
volumeMounts:
- name: task-volume
mountPath: "/usr/share/nginx/html"
volumes:
- name: task-volume
persistentVolumeClaim:
claimName: task-pvc
Prenez le temps de bien comprendre tout le processus logique qui va du volume "physique" hôte (
/mnt/data
) jusqu'au volume persistant utilisé par le conteneur/usr/share/nginx/html
.
Appliquez et testez :
kubectl apply -f pvc-pod.yaml
kubectl exec -it pvc-pod -- sh -c "echo 'Hello from PV' > /usr/share/nginx/html/index.html"
kubectl port-forward pvc-pod 8081:80
Dans un autre terminal ou navigateur, accédez à http://localhost:8081
Vérifions la persistance, en supprimant le pod :
kubectl delete pod pvc-pod
Puis en le recréant :
kubectl apply -f pvc-pod.yaml
Ajoutons quelque chose :
kubectl exec -it pvc-pod -- sh -c "echo '<br>Hello again from PV !' >> /usr/share/nginx/html/index.html"
et réexposons le pod pour vérifier :
kubectl port-forward pvc-pod 8081:80
Pour finir, illustrons avec un manifeste de déploiement avec le service associé. Créez un fichier stateful-app.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: stateful-app
spec:
replicas: 1
selector:
matchLabels:
app: stateful-app
template:
metadata:
labels:
app: stateful-app
spec:
containers:
- name: app-container
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: data-volume
mountPath: /usr/share/nginx/html
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: task-pvc
---
apiVersion: v1
kind: Service
metadata:
name: stateful-app-service
spec:
type: NodePort
selector:
app: stateful-app
ports:
- port: 80
targetPort: 80
nodePort: 30085
Appliquez le manifeste :
kubectl apply -f stateful-app.yaml
Accédez au service via Minikube :
minikube service stateful-app-service
Écrire des données dans le volume persistant :
kubectl exec -it deployment/stateful-app -- sh -c "echo '<h1>Contenu persistant cree le $(date)</h1>' > /usr/share/nginx/html/index.html"
Rafraîchir le navigateur pour voir le contenu mis à jour.
Supprimer le pod pour forcer sa recréation :
kubectl delete pod -l app=stateful-app
Attendre que le nouveau pod soit prêt :
kubectl wait --for=condition=Ready pods -l app=stateful-app
Rafraîchir à nouveau le navigateur pour vérifier que le contenu persiste
Ajouter maintenant plus de contenu :
kubectl exec -it deployment/stateful-app -- sh -c "echo '<p>Modification apres recreation du pod, $(date)</p>' >> /usr/share/nginx/html/index.html"
Rafraîchir le navigateur une dernière fois pour voir les modifications.
Les sondes de disponibilité (probes) sont des mécanismes essentiels pour la fiabilité des applications dans Kubernetes. Kubernetes propose deux types principaux de sondes :
Créez un fichier probes-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: probes-pod
spec:
containers:
- name: probes-container
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe
httpGet
(vérifie l'URL HTTP)/
(racine du serveur web)80
(port HTTP standard)15
(attendre 15 secondes après le démarrage du conteneur avant de commencer les vérifications)10
(vérifier toutes les 10 secondes)readinessProbe
httpGet
(même méthode que la liveness)5
(démarrage plus rapide des vérifications)5
(vérification plus fréquente)Autres paramètres possibles pour les sondes (non utilisés ici mais importants)
timeoutSeconds
: Temps maximum pour considérer la sonde comme échouée si pas de réponsesuccessThreshold
: Nombre de succès consécutifs nécessaires après un échecfailureThreshold
: Nombre d'échecs consécutifs tolérés avant actionTypes de sondes disponibles
httpGet
: Vérifie une URL HTTP (utilisé dans cet exemple)tcpSocket
: Vérifie si un port TCP est ouvertexec
: Exécute une commande dans le conteneur et vérifie le code de sortieAppliquez et vérifiez :
kubectl apply -f probes-pod.yaml
kubectl describe pod probes-pod
Comment provoquer un redémarrage automatique du pod ?
On peut par exemple lancer un shell interactif sur le pod ...
kubectl exec -it probes-pod -- /bin/bash
.. puis modifier le port d'écoute de Nginx :
sed -i 's/80;/8080;/' /etc/nginx/conf.d/default.conf
On relance Nginx :
nginx -s reload
et on attend...
Au bout de quelques secondes, le pod va être stoppé, et le terminal ouvert sur le pod fermé avec ce genre d'erreur :
root@probes-pod:/# command terminated with exit code 137
et la commande
kubectl describe pod probes-pod
doit indiquer des traces du problème dans la section events (lignes 6 à 8) :
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 6m37s default-scheduler Successfully assigned default/probes-pod to minikube
Normal Pulled 6m35s kubelet Successfully pulled image "nginx" in 1.124s (1.124s including waiting). Image size: 191998640 bytes.
Warning Unhealthy 4m57s (x8 over 5m22s) kubelet Readiness probe failed: Get "http://10.244.0.45:80/": dial tcp 10.244.0.45:80: connect: connection refused
Warning Unhealthy 4m57s (x3 over 5m17s) kubelet Liveness probe failed: Get "http://10.244.0.45:80/": dial tcp 10.244.0.45:80: connect: connection refused
Normal Killing 4m57s kubelet Container probes-container failed liveness probe, will be restarted
Normal Pulling 4m56s (x2 over 6m36s) kubelet Pulling image "nginx"
Normal Created 4m55s (x2 over 6m35s) kubelet Created container: probes-container
Normal Started 4m55s (x2 over 6m35s) kubelet Started container probes-container
Normal Pulled 4m55s kubelet Successfully pulled image "nginx" in 1.127s (1.127s including waiting). Image size: 191998640 bytes.
Créez un fichier lifecycle-pod.yaml
(attention ce pod va utiliser le PVC task-pvc
précédemment mis en place ; au besoin, recrééz-le) :
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-pod
spec:
containers:
- name: lifecycle-container
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo \"postStart hook executed at $(date)\" >> /mnt/data/lifecycle-events.log"]
preStop:
exec:
command: ["/bin/sh", "-c", "echo \"Container stopping at $(date)\" >> /mnt/data/lifecycle-events.log"]
volumeMounts:
- name: lifecycle-data
mountPath: /mnt/data
volumes:
- name: lifecycle-data
persistentVolumeClaim:
claimName: task-pvc
Explications :
Section lifecycle : Cette section définit les comportements spéciaux qui doivent se produire à des moments précis du cycle de vie du conteneur.
Hook postStart :
Hook preStop :
Utilité et cas d'usage :
postStart est utile pour :
preStop est utile pour :
Comportements importants à noter :
Appliquez :
kubectl apply -f lifecycle-pod.yaml
et consultez le fichier /mnt/data/lifecycle-events.log
depuis le pod :
kubectl exec lifecycle-pod -- cat /mnt/data/lifecycle-events.log
Pour observer le hook preStop, on supprime le pod :
kubectl delete pod lifecycle-pod
puis on le relance :
kubectl apply -f lifecycle-pod.yaml
Une nouvelle consultation du fichier devait indiquer les étapes :
Découvrons ici les initContainers qui permettent d'ordonnancer le lancement des containers d'un pod (ou d'un déploiement).
Les initContainers sont particulièrement utiles dans les Deployments pour :
Créez un fichier init-container-pod.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: init-container-pod
spec:
initContainers:
- name: init-container
image: alpine
command: ["/bin/sh", "-c", "echo 'Init container executed' > /data/init.txt"]
volumeMounts:
- name: data-volume
mountPath: /data
containers:
- name: main-container
image: alpine
command: ["/bin/sh", "-c", "cat /data/init.txt && sleep 3600"]
volumeMounts:
- name: data-volume
mountPath: /data
volumes:
- name: data-volume
emptyDir: {}
Appliquez et vérifiez :
kubectl apply -f init-container-pod.yaml
kubectl logs init-container-pod
La sortie contient deux parties importantes :
Defaulted container "main-container" out of: main-container, init-container (init)
indique que Kubernetes a choisi par défaut d'afficher les logs du conteneur principal, car le pod contient plusieurs conteneurs.Init container executed
: C'est le contenu réel du fichier /data/init.txt
qui a été créé par l'init container et qui est maintenant lu par le conteneur principal.Cela démontre que l'init container a bien fonctionné :
Et nous voici au bout d'une nouvelle étape dans cette découverte de Kubernetes, où nous avons pu :
À la fin du TP, les étudiants doivent être capables de :
Prêts pour un nouvel épisode ?