Temps de lecture estimé: 15 minutes
toujours dans la continuité de la migration de mon infra d’un simple serveur virtualisé, je continue à me former sur Kubernetes. après avoir installé un cluster sur 2 machines virtuelles Ubuntu 22.04, fait quelques opérations simples et installé Kubernetes Dashboard, je suis finalement revenu sur les bases avec un cours FreecodeCamp de 3h que j’avais mis de côté sur YouTube.
le cours vidéo en anglais est ci dessous ou ici
Je vais reprendre ici les grandes étapes et faire une sorte de mémo pour moi ou toute autre personne à qui ça pourrait servir. Ça me conforte dans l’idée que j’ai de Kubernetes : c’est complet, mais complexe. il y a plein de notions à connaitre pour réussir à l’utiliser convenablement dans un environnement production. Pour information, il y a aussi une Cheatsheet disponible sur la documentation Kubernetes.
Dans ce cours, c’est Minikube qui est utilisé. Donc si c’est pour des raisons de test en local, tu peux installer une instance simplifiée comme Minikube. Comme moi j’avance sur mon infra serveur, j’ai déjà un cluster Kubernetes avec un nœud Master et un nœud worker.
- Info sur le cluster
- API Kubernetes
- Lancer un pod unique
- Afficher les détails d’un pod
- Exécuter une commande sur un pod
- Supprimer une ressource
- Créer un déploiement Kubernetes
- Mettre à l’échelle un déploiement
- Afficher les détails d’un déploiement
- Redémarrer les pods d’un déploiement
- Suivre le déroulement d’un rollout
- Créer un service ClusterIp
- Afficher les détails d’un service
- Gérer les accès depuis l’extérieur avec des services NodePort
Info sur le cluster
pour savoir si dans les grandes lignes le cluster est fonctionnel
kubectl cluster-info
Kubernetes control plane is running at https://IP_MASTER:6443
CoreDNS is running at https://IP_MASTER:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
pour lister les nœuds
kubectl get nodes
globalement pour la plupart des commandes de listing on peut définir le format de sortie avec l’option output
pour avoir un affichage plus complet, j’utilise souvent –output=wide ou -o=wide ou simplement -o wide
kubectl get nodes -o=wide
API Kubernetes
kubectl utilise finalement qu’une API donc à chaque fois ça va se décomposer avec
kubectl action ressource
kubectl get pods
kubectl get namespaces
kubectl delete pods mon_pod
kubectl describe service mon_service
Si tu ne spécifies rien sur les commandes, le namespace utilisé va être celui par défaut, default.
du coup si tu crées tes éléments dans un autre namespace il faudra le spécifier ensuite avec l’option –namespace=mon_namespace ou -n=mon_namespace ou -n mon_namespace
kubectl get pods -n kube-system -o wide
Lancer un pod unique
pour lancer un pod à partir d’une image docker
kubectl run nom_de_mon_pod --image=image_docker
donc par exemple pour un pod Nginx de base
kubectl run nginx-single-pod --image=nginx
Afficher les détails d’un pod
pour avoir des détails sur un pod, celui créer juste au-dessus par exemple, on va demander à le décrire
kubectl describe pod nginx-single-pod
ça permet d’avoir accès à des infos détaillées sur le pod, comme l’image Docker utilisée ou l’Id du conteneur Docker
Container ID: docker://477223dc4216144046145a382bb515adeb6479b50ab4f98ba695aed6b66f07ce
Exécuter une commande sur un pod
ce qui permet ensuite d’utiliser Docker pour d’autres actions isolées si tu le souhaite, comme lancer une console bash dans ce conteneur
docker exec -it 477223dc4216144046145a382bb515adeb6479b50ab4f98ba695aed6b66f07ce bash
cet exemple me permet aussi d’introduire le fait que l’équivalent est possible directement avec kubectl
kubectl exec -it -n default nginx-single-pod -- bash
Supprimer une ressource
pour supprimer une ressource Kubernetes, il suffit d’utiliser l’action delete
kubectl delete pod nginx-single-pod
Créer un déploiement Kubernetes
maintenant que l’image nginx est supprimée, on va plutôt créer un « déploiement ». c’est ce déploiement qui va s’occuper, suivant les paramètres fournis, de lancer les différents pods.
Pour créer un déploiement qui va se charger de déployer un seul pod
kubectl create deployment nginx-deployment –image=nginx
si par contre tu souhaites déployer directement 5 pods tu peux aussi spécifier
kubectl create deployment nginx-deployment --image=nginx --replicas=5
Mettre à l’échelle un déploiement
C’est quelque chose qui peut ensuite être modifié si jamais tu veux mettre à l’échelle tes déploiements. la mise à l’échelle se fait avec scale
kubectl scale deployment nginx-deployment --replicas=10
Afficher les détails d’un déploiement
de la même manière que pour avoir des détails pour un pod, on peut avoir des détails du déploiement avec
kubectl describe deployment nginx-deployment
Redémarrer les pods d’un déploiement
pour redémarrer les pods d’un déploiement, il faut faire un rollout
kubectl rollout restart deployment nginx-deployment
Suivre le déroulement d’un rollout
pour suivre le déroulement d’un rollout (ou d’une mise à l’échelle), il faut consulter le statut du rollout
kubectl rollout status deployment nginx-deployment
Créer un service ClusterIp
pour l’instant, si je fais
kubectl get pods -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
default nginx-deployment-85c6d5f6dd-f58sv 1/1 Running 0 12m 172.17.0.10 k8s-workernode.anthony-jacob.com <none> <none>
default nginx-deployment-85c6d5f6dd-gjt9w 1/1 Running 0 12m 172.17.0.6 k8s-workernode.anthony-jacob.com <none> <none>
default nginx-deployment-85c6d5f6dd-mxsss 1/1 Running 0 12m 172.17.0.9 k8s-workernode.anthony-jacob.com <none> <none>
default nginx-deployment-85c6d5f6dd-q8xlt 1/1 Running 0 12m 172.17.0.5 k8s-workernode.anthony-jacob.com <none> <none>
default nginx-deployment-85c6d5f6dd-wjglf 1/1 Running 0 12m 172.17.0.4 k8s-workernode.anthony-jacob.com <none> <none>
on peut voir l’adresse IP des pods Nginx. depuis le nœud, dans mon cas le workernode, je peux tester Nginx sur chacun des pods avec curl
curl 172.17.0.10
ou
curl 172.17.0.6
ou ou les autres adresses IP de mes pods Nginx, ça me renvoie bien la page HTML d’accueil du serveur Nginx Welcome to nginx!
Si maintenant on veut non plus accéder à chaque pod individuellement, mais qu’on souhaite laisser le déploiement faire ça sauce, on peut exposer un port et créer un service Kubernetes.
Si par exemple on souhaite exposer le port 8080 et le rediriger sur le port 80
kubectl expose deployment nginx-deployment --port 8080 --target-port=80
ce qui a pour effet de créer un service
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 35d
nginx-deployment ClusterIP 10.100.178.93 <none> 8080/TCP 2s
J’ai bien mon service nginx-deployment accessible depuis le port 8080 avec l’adresse IP 10.100.178.93
Si maintenant je teste depuis mon nœud workernode avec un curl, j’ai toujours bien la réponse de nginx avec la page HTML Welcome to nginx!
curl 10.100.178.93:8080
par contre, cette adresse IP n’est toujours pas accessible depuis l’extérieur
Afficher les détails d’un service
la logique commence à rentrer, pour avoir une vue plus détaillée du service
kubectl describe service nginx-deployment
Gérer les accès depuis l’extérieur avec des services NodePort
Application Node
Maintenant pour gérer les accès depuis l’extérieur avec des services NodePort, on va passer au déploiement d’une petite application Node. Tous les fichiers nécessaires sont disponible sur mon dépôt git (un fork du dépôt original de l’auteur de ce cours avec quelques petites différences)
Dans mon environnement de développement sous Windows, j’ai tous les outils déjà installés et accessibles dans mon PATH (npm, docker, vscode etc etc), il faut aussi dans le cadre de ce cours un compte sur Docker Hub pour pousser des images Docker donc je te laisse voir ce qu’il te manque pour aller plus loin
Je créer le dossier k8s-freecodecamp qui me servira de répertoire pour tout ce training. je créer aussi le sous répertoire k8s-web-hello dans lequel il y aura l’application Node de ce tuto. C’est un serveur Express qui aura pour but de renvoyer le hostname à chaque requête.
Dans ce répertoire en ligne de commande, j’initialise un projet Node avec npm
npm init -y
ce qui crée le fichier package.json, et ensuite on installe Express
npm install express
je créer le fichier index.mjs dans lequel je copie le contenu du fichier sur mon dépôt git
import express from 'express'
import os from 'os'
const app = express()
const PORT = 3000
app.get("/", (req, res) => {
const helloMessage = `Hello from the ${os.hostname()} \n`
console.log(helloMessage)
res.send(helloMessage)
})
app.listen(PORT, () => {
console.log(`Web server is listening at port ${PORT}`)
})
dans le fichier package.json, je rajoute une instruction start
"start": "node index.mjs"
Image Docker
l’application Node est prête, passons maintenant a la construction de l’image Docker qui servira au déploiement. Donc dans le dossier de l’application Node, k8s-web-hello, je crée le fichier Dockerfile dans lequel je mets le contenu du fichier présent sur mon dépôt git
#depuis l'image Node:alpine
FROM node:alpine
#on defini le repertoire de travail /app
WORKDIR /app
#on expose le port 3000,
EXPOSE 3000
#on copie les 2 fichiers package.json package-lock.json dans ce workdir /app
COPY package.json package-lock.json ./
#ce qui permet de passer ensuite à l'etape d'installation de tos les package node necessaire
RUN npm install
#on copie le reste du dossier (index.mjs)
COPY . ./
#et on fini par lancer l'application avec la commande npm et la directive start qu'on a creé un peu plus tot dans le fichier package.json
CMD ["npm", "start"]
maintenant que le Dockerfile est prêt, on peut construire l’image Docker avec
docker build . -t anthonygj/k8s-web-hello
(anthonygj est mon espace sur le hub Docker)
Je vais maintenant pousser l’image sur le hub docker pour pouvoir l’utiliser en déploiement avec Kubernetes. D’abord, je me log sur le hub Docker avec
docker login
et ensuite pour pousser l’image
docker push anthonygj/k8s-web-hello
elle est bien poussée et présente sur https://hub.docker.com/repositories
Déployer l’application sur le cluster Kubernetes
l’image est maintenant disponible, on peut maintenant la déployer sur Kubernetes avec la commande utilisée dans la première partie sur les basiques de Kubernetes
kubectl create deployment k8s-web-hello --image=anthonygj/k8s-web-hello --replicas=3
encore une fois, de la même manière que dans la première partie, on peut créer un service pour ce déploiement et exposé le port 80 pour le redirigé sur le port 3000 interne des pods
kubectl expose deployment k8s-web-hello --port 80 --target-port=3000
mon service est bien créé
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
k8s-web-hello ClusterIP 10.108.73.221 <none> 80/TCP 8s
et cette fois si, je peux voir qu’avec un curl sur l’ip du cluster, j’ai une réponse de mon application depuis plusieurs pods suivant le load balancing
curl 10.108.73.221
VERSION 2: Hello from the k8s-web-hello-5fb9f678bf-qkpfk
curl 10.108.73.221
VERSION 2: Hello from the k8s-web-hello-5fb9f678bf-6j6j7
curl 10.108.73.221
VERSION 2: Hello from the k8s-web-hello-5fb9f678bf-cfrkl
curl 10.108.73.221
VERSION 2: Hello from the k8s-web-hello-5fb9f678bf-6j6j7
Accès au service Kubernetes depuis l’extérieur
la problématique ici étant que cette adresse IP est accessible uniquement depuis le cluster Kubernetes. Si je souhaite accéder depuis l’extérieur ou depuis mon réseau local à ce service, je ne peux pas.
Jusqu’à présent, nous avons créé les services avec le type ClusterIP. nous allons maintenant créer un service avec le type NodePort, ce qui va permettre d’accéder au service depuis l’IP et un port du nœud qui eux (sauf règle firewall contraire) sont accessible depuis mon réseaux local.
donc on supprime le service qu’on vient juste de créer
kubectl delete service k8s-web-hello
pour en recréer un de type NodePort
kubectl expose deployment k8s-web-hello --type=NodePort --port=3000
le service est maintenant créé
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
k8s-web-hello NodePort 10.98.63.173 <none> 3000:30297/TCP 15s
et on peut voir qu’un port aléatoire 30297 a été attribué pour faire la redirection en externe du nœud vers le cluster de déploiement. si maintenant j’essaie d’accéder depuis mon navigateur à http://IP_DU_NOEUD:30297/ , j’ai bien une réponse
VERSION 2: Hello from the k8s-web-hello-5fb9f678bf-6j6j7
Avec des fournisseurs de service de Cloud comme AWS, google Cloud, Azure etc qui proposent directement un load balancer intégré, on aurait aussi pu utiliser un service de type LoadBalancer.
kubectl expose deployment k8s-web-hello --type=LoadBalancer --port=3000
sur mon instance qui n’est pas préparée pour ça, ça revient dans les grandes lignes à faire un service NodePort
Mettre à jour une image et un déploiement
Pour commencer, on va mettre à jour le projet en changeant quelque chose comme le message de réponse. dans mon cas, je le remplace par
const helloMessage = `Hello from the ${os.hostname()} pods \n`
j’enregistre et je reconstruis l’image docker avec un tag v2
docker build . -t anthonygj/k8s-web-hello:2.0
et je la pousse sur Docker Hub
docker push anthonygj/k8s-web-hello:2.0
Maintenant qu’elle est bien présente sur le repo Docker Hub, je peux spécifier aux pods de mon déploiement d’utiliser cette nouvelle image
kubectl set image deployment k8s-web-hello k8s-web-hello=anthonygj/k8s-web-hello:2.0
pour suivre le déploiement et le rollout des pods
kubectl rollout status deployment k8s-web-hello
Créer des spécifications de déploiement avec des fichier Yaml
maintenant qu’on a vu comment déployer une application en utilisant la ligne de commande pour le déploiement et les services, nous allons voir comment le faire à travers des fichiers YAML de spécification des déploiements. Pour ça, on va une nouvelle fois revenir à zéro sur les déploiements et services avec lesquels on s’amusait ici.
soit on supprime les déploiements et les services un à un
kubectl delete service nom_service -n namespace
kubectl delete deployment nom_deploiement -n namespace
Soit un raccourci pour supprimer les objets d’un namespace
kubectl delete all --all -n namespace
dans mon cas, vu que j’utilisais le namespace par défaut, je n’ai pas besoins de le spécifier
kubectl delete all --all
on va pouvoir créer plusieurs fichiers pour définir le déploiement de l’application et le service. Ces fichiers sont un peu pompeux, c’est là que Visual Studio Code et ses extensions Kubernetes vont bien aider avec l’autocomplétion
ce qui peut être pratique aussi quand on connait l’équivalent en ligne de commande, c’est de lancer la commande en spécifiant que c’est un essai –dry-run=client et en spécifiant le format de sortie yaml -o=yaml
kubectl create deployment k8s-web-hello --image=anthonygj/k8s-web-hello --replicas=3 --dry-run=client -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: k8s-web-hello
name: k8s-web-hello
spec:
replicas: 3
selector:
matchLabels:
app: k8s-web-hello
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: k8s-web-hello
spec:
containers:
- image: anthonygj/k8s-web-hello
name: k8s-web-hello
resources: {}
status: {}
ça permet d’avoir une bonne base de départ.
Les fichiers sont disponibles sur le dépôt git. Moi j’ai également créé un namespace supplémentaire.
kind: Namespace
apiVersion: v1
metadata:
name: k8s-web-hello
labels:
name: k8s-web-hello
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-web-hello
namespace: k8s-web-hello
spec:
replicas: 3
selector:
matchLabels:
app: k8s-web-hello
template:
metadata:
labels:
app: k8s-web-hello
spec:
containers:
- name: k8s-web-hello
image: anthonygj/k8s-web-hello
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 3000
apiVersion: v1
kind: Service
metadata:
name: k8s-web-hello-svc
namespace: k8s-web-hello
spec:
type: NodePort
selector:
app: k8s-web-hello
ports:
- port: 80
targetPort: 3000
pour en savoir plus sur les options disponibles dans les fichiers de définitions yaml de Kubernetes, ça se trouve sur les méandres de la documentation
une fois les fichiers créés avec les options voulues, on passe toujours par la commande kubectl et cette fois on va appliquer les fichiers
kubectl apply -f namespace.yaml -f deployment.yaml -f service.yaml
si tout va bien, le nouveau namespace, le nouveau déploiement et le nouveau service sont créés. on peut vérifier le port du service avec
kubectl get service -A
et retester avec le nouveau port, http://IP_DU_NOEUD:30562/
Maintenant que tout fonctionne… on peut tout supprimer pour passer à la suite. Pour ça, on peut juste donner les fichiers de config à manger à Kubernetes pour supprimer les objets correspondants
kubectl delete -f service.yaml -f deployment.yaml -f namespace.yaml
Faire communiquer 2 pods ou 2 services
On peut passer à la dernière partie de ce cours, à savoir le déploiement d’une application Node qui communique avec Nginx. On va réutiliser ce qu’on a déjà fait avec l’application Node et la modifier un peu, donc on peut faire un copier coller du répertoire k8s-web-hello vers k8s-web-to-nginx
l’index.mjs est un peu plus complet, le contenu est disponible sur le dépôt git
import express from 'express'
import fetch from 'node-fetch'
import os from 'os'
const app = express()
const PORT = 3000
app.get("/", (req, res) => {
const helloMessage = `Hello from the ${os.hostname()}`
console.log(helloMessage)
res.send(helloMessage)
})
app.get("/nginx", async (req, res) => {
const url = 'http://nginx.k8s-web-to-nginx.svc.cluster.local'
const response = await fetch(url);
const body = await response.text();
res.send(body)
})
app.listen(PORT, () => {
console.log(`Web server is listening at port ${PORT}`)
})
vu qu’on utilise un paquet en plus, node-fetch, il ne faut pas oublier de l’inclure dans le package.json
npm install node-fetch
on peut maintenant build une nouvelle image Docker et la pousser sur notre repo Docker hub
docker build . -t anthonygj/k8s-web-to-nginx
et
docker push anthonygj/k8s-web-to-nginx
passons maintenant à la spécification yaml du déploiement et du service. Le fichier est disponible sur le dépôt git.
kind: Namespace
apiVersion: v1
metadata:
name: k8s-web-to-nginx
labels:
name: k8s-web-to-nginx
---
apiVersion: v1
kind: Service
metadata:
name: k8s-web-to-nginx-svc
namespace: k8s-web-to-nginx
spec:
type: LoadBalancer
selector:
app: k8s-web-to-nginx
ports:
- port: 3333
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-web-to-nginx
namespace: k8s-web-to-nginx
spec:
replicas: 3
selector:
matchLabels:
app: k8s-web-to-nginx
template:
metadata:
labels:
app: k8s-web-to-nginx
spec:
containers:
- name: k8s-web-to-nginx
image: anthonygj/k8s-web-to-nginx:2.0
resources:
limits:
memory: "128Mi"
cpu: "1"
requests:
memory: "10Mi"
cpu: "50m"
ports:
- containerPort: 3000
kind: Namespace
apiVersion: v1
metadata:
name: k8s-web-to-nginx
labels:
name: k8s-web-to-nginx
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: k8s-web-to-nginx
spec:
selector:
app: nginx
ports:
- port: 80
name: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: k8s-web-to-nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
memory: "128Mi"
cpu: "1"
requests:
memory: "10Mi"
cpu: "50m"
ports:
- containerPort: 3000
j’ai un peu tweaké les ressources, premièrement parce qu’au premier test je me suis retrouvé bloqué. Apparemment, en ne spécifiant que les limites en ressources, celles-ci sont aussi utilisées en request. ce qui fait que mon nœud Kubernetes atteignait la limite de request et refusais de lancé les pods nginx
Ces limites et ces requêtes sont définies pour chaque pods et non pour le déploiement. un article intéressant qui parle des ressources est disponible ici
J’ai voulu aller un peu plus loin en créant et en spécifiant un namespace pour tout ce petit monde. De ce fait, j’ai découvert la gestion DNS de kubernetes.
dans l’exemple de base, il y a
const url = 'http://nginx'
c’est valable parce que dans l’exemple, les pods et le service sont créés dans le même namespace, celui par defaut, default . dans mon cas, j’ai choisi de créer un namespace k8s-web-to-nginx et de changer mon url avec
const url = 'http://nginx.k8s-web-to-nginx.svc.cluster.local'
à noter que dans mon cas, les 2 déploiements (et donc les pods qui seront créés) sont dans le même namespace, donc ça fonctionnerait aussi avec juste http://nginx
Je me suis aussi rendu compte que mon cluster Kubernetes n’était pas complètement fonctionnel. Les pod ne pouvaient ni résoudre les domaines internes comme nginx.k8s-web-to-nginx.svc.cluster.local , ni ceux externes comme google.com ou debian.org, quelque chose ne tournait pas rond au niveau du pod réseau ou du service Kube-dns/coredns j’en parle plus en détail dans l’article Kubernetes problèmes DNS CoreDns et problèmes d’accès aux IP ClusterIP.
je déploie mes nouveaux pods et mes nouveaux services avec les fichiers de spécifications que je viens de créer
kubectl apply -f k8s-web-to-nginx.yaml -f nginx.yaml
je vérifie une nouvelle fois le port du service
kubectl get service -A
cette fois ci, j’ai 2 services
k8s-web-to-nginx k8s-web-to-nginx-svc LoadBalancer 10.105.227.24 <pending> 3333:32261/TCP 48s
k8s-web-to-nginx nginx ClusterIP 10.101.213.147 <none> 80/TCP 48s
k8s-web-to-nginx-svc qui est le service de mon appli Node qui va nous servir ici de front (service de type LoadBalancer sur le port 32261) et le service nginx qui va être appelé par l’application Node grâce à son IP de cluster et son enregistrement DNS nginx.k8s-web-to-nginx.svc.cluster.local
donc si j’accède à http://IP_DU_NOEUD:32261/ , j’ai toujours la réponse de mon application Node
Hello from the k8s-web-to-nginx-65ff6545dc-5skxk
et la nouveauté maintenant c’est que si j’accède à http://IP_DU_NOEUD:32261/nginx, j’ai maintenant la page par défaut de nginx Welcome to nginx!
et voilà, finito pour cette introduction à Kubernetes. moi ça m’a permis d’avoir un peu plus de compréhension au fonctionnement de ce mastodonte. Dis-moi ce que tu en penses et si toi aussi ça a pu te servir !
Je ne sais pas encore dans quel ordre, mais dans les prochaines étapes, j’ai toujours dans les cartons de tester portainer.io, j’ai aussi prévu de mettre en place un reverse proxy Ingress nginx et la mise en place de la gestion de mes certificats ssl Letsencrypt au sein de Kubernetes…
Voilà, en attendant, je te dis à bientôt pour de nouvelles aventures!!