Kubernetes - Ecrire ses premiers manifests Kubernetes
Publié le : 10 janvier 2022 | Mis à jour le : 22 janvier 2023Maintenant que nous avons vu les concepts et le fonctionnement de Kubernetes, il va falloir commencer à écrire ses premiers manifests. Mais comment le faire de manière efficace ?
Je ne suis pas l’adepte du copier/coller à gogo et j’aime maîtriser ce que je fais. Je crée mes propres conteneurs Docker, mes propres playbooks et rôles Ansible. Mais comme vous j'utilise beaucoup d’outils devops et je switche sans cesse de projet, je suis parfois un peu perdu dans les syntaxes. Alors je vous propose une méthode qui vous sera utile lors du passage des certifications Kubernetes (CKA, CKS,CKD) ou lors de l’écriture de vos manifests.
Kubectl
Avant de démarrer dans le vif du sujet, je pense qu’il est important de connaitre quelques fonctionnalités du client officiel Kubernetes : Kubectl.
Pour utiliser ces commandes vous pouvez soit utiliser kind ou minkube. Pour ceux qui passent les certifications ma sandbox peut être un bon point de départ.
L’autocomplétion
Kubectl est fourni avec de l’autocomplétion, c’est pratique et ça permet d’aller très vite dans la création des commandes kubectl :
sudo apt install bash-completion
kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl > /dev/null
Redémarrer votre shell et tapez kubectl
suivi de tabulation. Vous ferez par
exemple pour changer de namespace, on verra la création plus tard, il suffit de
taper kubectl -n tab et la liste de tous les namespaces existant sur votre
cluster s’affichera.
kubectl -n
default kube-node-lease kube-public kube-system
kubectl config set-context --current --namespace=kube-system
Context "kubernetes-admin@kubernetes" modified.
Pour aller encore plus on peut utiliser un alias :
alias k=kubectl
complete -F __start_kubectl k
k tab
annotate apply autoscale completion
Pour le rendre permanent ajouter ces deux lignes à la fin de votre fichier .bashrc
Obtenir de la documentation
Pour obtenir de l’aide sur la syntaxe de kubectl il suffit de taper classiqement
l’option --help
:
kubectl --help
# ou pour une commande
kubectl run pod --help
De plus, pour créer des ressources vous pourriez chercher la documentation sur le site Kubernetes, mais il est utile de savoir que Kubectl intègre une documentation complète des ressources qu’il est possible de créer dans un cluster Kubernetes : nodes, pod, service, deployment, nodes
Exemple sur les pods :
kubectl explain pods
KIND: Pod
VERSION: v1
DESCRIPTION:
Pod is a collection of containers that can run on a host. This resource is
created by clients and scheduled onto hosts.
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 <Object>
Standard object's metadata. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
spec <Object>
Specification of the desired behavior of the pod. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
status <Object>
Most recently observed status of the pod. This data may not be up to date.
Populated by the system. Read-only. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
Pour obtenir la documentation d’un sous-objet il suffit d’ajouter un point plus le nom de l’objet :
Exemple pods.spec.affinity :
kubectl explain pods.spec.affinity
kubectl explain pods.spec.affinity
KIND: Pod
VERSION: v1
RESOURCE: affinity <Object>
DESCRIPTION:
If specified, the pod's scheduling constraints
Affinity is a group of affinity scheduling rules.
FIELDS:
nodeAffinity <Object>
Describes node affinity scheduling rules for the pod.
podAffinity <Object>
Describes pod affinity scheduling rules (e.g. co-locate this pod in the
same node, zone, etc. as some other pod(s)).
podAntiAffinity <Object>
Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod
in the same node, zone, etc. as some other pod(s)).
Pour afficher toutes les informations d’objet et de ces sous-objets on utilise
l’option --recursive
. Attention c’est violent !
Pour trouver la liste des ressources pouvant être déclarées, avec leur kind et versions d’api respectives, il suffit d’utiliser la commande api-ressources :
kubectl api-resources
NAME SHORTNAMES APIVERSION NAMESPACED KIND
bindings v1 true Binding
componentstatuses cs v1 false ComponentStatus
configmaps cm v1 true ConfigMap
endpoints ep v1 true Endpoints
events ev v1 true Event
limitranges limits v1 true LimitRange
namespaces ns v1 false Namespace
nodes no v1 false Node
persistentvolumeclaims pvc v1 true PersistentVolumeClaim
persistentvolumes pv v1 false PersistentVolume
pods po v1 true Pod
....
Ce sont ces informations qui seront déclarées au début d’un manifest.
Gros avantage la documentation est à jour et correspond à celle de votre cluster. Bien sûr, la version de Kubectl doit correspondre à celle du cluster. Pour le vérifier rien de plus simple :
kubectl version
Client Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.1", GitCommit:"86ec240af8cbd1b60bcc4c03c20da9b98005b92e", GitTreeState:"clean", BuildDate:"2021-12-16T11:41:01Z", GoVersion:"go1.17.5", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.5", GitCommit:"5c99e2ac2ff9a3c549d9ca665e7bc05a3e18f07e", GitTreeState:"clean", BuildDate:"2021-12-16T08:32:32Z", GoVersion:"go1.16.12", Compiler:"gc", Platform:"linux/amd64"}
Mince ce n’est pas mon cas ! Allez on fait la mise à jour de kubectl (on pourrait faire l’upgrade du cluster plutôt) :
apt-cache madison kubectl |grep 1.22.5
apt install kubectl=1.22.5-00
kubectl version
Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.5", GitCommit:"5c99e2ac2ff9a3c549d9ca665e7bc05a3e18f07e", GitTreeState:"clean", BuildDate:"2021-12-16T08:38:33Z", GoVersion:"go1.16.12", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.5", GitCommit:"5c99e2ac2ff9a3c549d9ca665e7bc05a3e18f07e", GitTreeState:"clean", BuildDate:"2021-12-16T08:32:32Z", GoVersion:"go1.16.12", Compiler:"gc", Platform:"linux/amd64"}
kubectl get
La commande get
permet de rechercher et d’afficher la configuration d’un objet.
Regardons un exemple avec un pod du namespace kube-system.
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/etcd.advertise-client-urls: https://192.168.121.82:2379
kubernetes.io/config.hash: 413446f10a47cd29048961fc9e411de6
kubernetes.io/config.mirror: 413446f10a47cd29048961fc9e411de6
kubernetes.io/config.seen: "2022-01-10T09:31:44.361934226Z"
kubernetes.io/config.source: file
seccomp.security.alpha.kubernetes.io/pod: runtime/default
creationTimestamp: "2022-01-10T09:31:44Z"
labels:
component: etcd
tier: control-plane
name: etcd-master1
kubectl get pod -A
kubectl -n kube-system get pod etcd-master1 -o yaml
La commande nous retourne toutes les informations du pod ETCD.
Utilisation du mode dry-run de Kubectl
Pour rappel il existe deux modes de création des ressources Kubernetes :
- le mode déclaratif utilisant ce qu’on appelle des manifests écrit en YAML
- le mode impératif créant les ressources à la volée.
Il est possible d’utiliser le mode impératif pour créer vos manifests en ajoutant simplement le mode dry-run couplé à la sortie de type yaml.
Création des ressources simples
Création d’un namespace
Pour ceux qui ne connaissent pas les NameSpace je vous renvoie à mon introdution à Kubernetes.
kubectl create namespace test --dry-run=client --output=yaml > namespace.yaml
cat namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
creationTimestamp: null
name: test
spec: {}
status: {}
Pour l’appliquer :
kubectl apply -f namespace.yaml
namespace/test created
Une commande kubectl que nous n’avions pas vu auparavant est la commande describe qui permet d’obtenir une description détaillée d’une ressource. Vous allez me demander c’est quoi la différence avec get? Describe affiche plus d’informations et de manière plus simple. Il affiche par exemple les ressources en relation de l’objet et les événements. Elle sera très utile pour debuger !
kubectl describe namespace test
Name: test
Labels: kubernetes.io/metadata.name=test
Annotations: <none>
Status: Active
No resource quota.
No LimitRange resource.
Dans le cadre d’un Namespace c’est peu utile, mais on peut facilement voir que nous avons défini de limite de ressources dans ce Namespace.
Création d’un pod
Pour créer un pod, ce qui sera très rare, puisque nous n’utiliserons que des
objets plus complexes, comme les deployments, nous utiliserons non pas la
commande create
mais run
. En effet, create
ne créé que les objets suivants :
- clusterrole
- configmap
- deployment
- job
- poddisruptionbudget
- quota
- rolebinding
- service
- clusterrolebinding
- cronjob
- ingress
- namespace
- priorityclass
- role
- secret
- serviceaccount
Lancement d’un premier pod qui ne fait rien :)
kubectl run busybox --image=busybox --dry-run=client -o yaml > pod.yaml
cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: busybox
name: busybox
spec:
containers:
- image: busybox
name: busybox
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
On vérifie :
kubectl -n kube-system get pod
NAME READY STATUS RESTARTS AGE
busybox 0/1 Completed 0 9s
Il est possible d’ajouter d’autres options lors de la création d’un pod.
Il suffit de taper la commande kubectl run pod --help
. Un exemple empêchant
que le pod ne redémarre tout le temps:
kubectl run busybox --image=busybox --restart=Never --dry-run=client -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: busybox
name: busybox
spec:
containers:
- image: busybox
name: busybox
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Never
status: {}
Pour ajouter des limites de ressources il faudra ici utiliser la commande set
:
kubectl run nginx --image=nginx --dry-run=client -o yaml | kubectl set resources --limits=cpu=200m,memory=512Mi --local -f - -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
resources:
limits:
cpu: 200m
memory: 512Mi
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
Pour ajouter d’autres paramètres il faudra passer par l’édition du fichier yaml.
En ajoutant les paramètres récupérés par la commande kubectl explain
:
kubectl explain pods.spec.containers.resources
KIND: Pod
VERSION: v1
RESOURCE: resources <Object>
DESCRIPTION:
Compute Resources required by this container. Cannot be updated. More info:
https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
ResourceRequirements describes the compute resource requirements.
FIELDS:
limits <map[string]string>
Limits describes the maximum amount of compute resources allowed. More
info:
https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
requests <map[string]string>
Requests describes the minimum amount of compute resources required. If
Requests is omitted for a container, it defaults to Limits if that is
explicitly specified, otherwise to an implementation-defined value. More
info:
https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
Les liens seront bien utiles lors du passage des certifications, car vous n’aurez droit qu’à ce site.
Pour les ressources on retrouve la syntaxe :
---
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
Détruire une ressource Kubernetes
Pour détruire une ressource on utilise la commande delete
kubectl delete pod busybox
Pour détruire un objet qui a été déclaré avec un manifest :
kubectl -n kube-system delete -f pod.yaml
Attention si on détruit un namespace tous les objets de ce NameSpace le seront également et cela prend du temps !
Création d’un déploiement
Un deployement Kubernetes est un objet de niveau supérieur à ceux que nous avons vu jusqu’à maintenant. En effet il est de type Workload (Ressource) qui gère le clycle de vie des pods. Parmi les autres objets de ce type on retrouve le StatefulSet, le DaemonSet et les Jobs. Je vous renvoie à mon introduction sur Kubernetes pour leur définition.
Un déployement permet donc de gérer le cycle de vie d’une application, en définissant les images à utiliser, le nombre de replica (Replicaset) et bien d’autres paramètres.
Nous allons continuer à utiliser les commandes create
et run
pour générer
les manifests.
kubectl create deployment --image=nginx nginx --dry-run=client -o yaml > deployment.yaml
cat deployement.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
resources: {}
status: {}
kubectl create -f deployment.yaml
deployment.apps/nginx created
Et voilà nous avons créé notre premier deployment
. Etudions le avec la commande
describe :
kubectl describe deployment nginx
Name: nginx
Namespace: test
CreationTimestamp: Fri, 14 Jan 2022 06:17:10 +0000
Labels: app=nginx
Annotations: deployment.kubernetes.io/revision: 1
Selector: app=nginx
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: nginx-85b98978db (1/1 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 80s deployment-controller Scaled up replica set nginx-85b98978db to 1
On peut voir que lorsqu’on n’indique pas de propriété, par exemple la stratégie,
Kubernetes en applique une par défaut. Ici, on retrouve la stratégie
RollingUpdate
qui permet de déployer automatiquement une nouvelle version d’une
image tout en conservant les existantes pour qu’il n’y ait pas de coupure de
service.
Vous avez certainement remarqué qu’un deployement
s’appuie sur un Replicaset
.
C’est cet objet qui gère l’état des pods. Le nombre de replicas étant géré par
le paramètre replicas du manifest. Nous pourrions le modifier dans le manifest
mais je vais vous montrer comment le gérer.
Pour gérer le nombre de replicas, à la volée il faut utiliser la commande scale
de kubectl
kubectl scale deployment -n test nginx --replicas 2
NAMESPACE NAME READY STATUS RESTARTS AGE
test nginx-85b98978db-z9jnx 0/1 ContainerCreating 0 6s
test nginx-85b98978db-znb4f 1/1 Running 0 10m
Une autre méthode via l’utilisation de la commande kubectl edit
qui permet de
modifier un manifest stocké en base.
kubectl edit deployments.apps nginx
Rechercher replicas, elle est normalement à 2. Passez la à 3 et sauvegarder (commande vi : r et :wq )
deployment.apps/nginx edited
NAME READY STATUS RESTARTS AGE
nginx-85b98978db-qf25p 1/1 Running 0 36s
nginx-85b98978db-z9jnx 1/1 Running 0 3m27s
nginx-85b98978db-znb4f 1/1 Running 0 13m
Création d’un service
Un service est un objet permettant d’exposer une application composé de plusieurs pods pouvant tourner sur plusieurs nœuds du cluster Kubernetes.
En effet, les pods possèdent leurs propres adresse IP, qui peuvent changer à tout moment lors du changement de version de l’image ou quand elle meurt suite à une erreur.
Un service est donc la couche d’abstraction qui permet d’accéder facilement aux pods de votre application.
Écrivons notre premier manifest de services exposant notre déploiement créé précédemment :
kubectl expose deployment nginx --port 80 --dry-run=client -o yaml > service.yaml
cat service.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
status:
loadBalancer: {}
kubectl apply -f service.yaml
service/nginx created
Cette fois, nous avons utilisé la commande expose pour créer l’objet service. La section spec.ports permet de définir quel port du pod est à exposer et sur quel port de l’IP du service (targetPort).
Affichons l’état du service et les pods :
kubectl describe service nginx
Name: nginx
Namespace: test
Labels: app=nginx
Annotations: <none>
Selector: app=nginx
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.108.196.175
IPs: 10.108.196.175
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 192.168.189.76:80,192.168.235.148:80,192.168.235.149:80
Session Affinity: None
Events: <none>
webapp-545557d95c-xc2tf 1/1 Running 0 16h
❯ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-85b98978db-qf25p 1/1 Running 0 27m 192.168.235.149 worker1 <none> <none>
nginx-85b98978db-z9jnx 1/1 Running 0 30m 192.168.189.76 worker2 <none> <none>
nginx-85b98978db-znb4f 1/1 Running 0 40m 192.168.235.148 worker1 <none> <none>
On voit que le service possède :
- des entrées référençant les IP des pods: Endpoints
- un type. Ici ClusterIP
Les «Service Type» de Kubernetes peuvent être de type :
- ClusterIP: Expose le service sur une IP interne au cluster. Le choix de cette valeur rend le service uniquement accessible à partir du cluster. Il s’agit du ServiceType par défaut.
- NodePort: Expose le service sur l’IP de chaque nœud sur un port statique (le NodePort).
- LoadBalancer: Expose le service en externe à l’aide LoadBalancer.
- ExternalName: Mappe le service au contenu du champ externalName (par exemple foo.bar.example.com), en renvoyant un enregistrement CNAME avec sa valeur.
Je ne vais pas rentrer dans le détail sur ces types, car cela fera le sujet du prochain billet avec les ingress. Un vaste sujet que je ne maitrise qu’en partie pour le moment.
Utilisation des labels et selector
Pour le moment nous n’avions pas parlé des labels. Les labels sont des données
de type clé/valeur qui sont attachés aux objets permettant de les identifier.
Regardez le contenu du fichier deployement et vous remarquerez qu’il possède
dans la partie metadata un tableau labels possédant une étiquette app: nginx
.
Pour afficher les labels d’un objet il suffit d’ajouter l’option --show-labels
à la commande kubectl get
:
kubectl get pod --show-labels
NAME READY STATUS RESTARTS AGE LABELS
nginx-85b98978db-qf25p 1/1 Running 0 15m app=nginx,pod-template-hash=85b98978db
nginx-85b98978db-z9jnx 1/1 Running 0 18m app=nginx,pod-template-hash=85b98978db
nginx-85b98978db-znb4f 1/1 Running 0 28m app=nginx,pod-template-hash=85b98978db
On vient bien notre label app=nginx
. Un exemple de commande on va utiliser les
labels pour afficher les logs de tous les pods possédant le label app=nginx.
Bien pratique :
kubectl logs -f -l app=nginx
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/01/14 06:27:51 [notice] 1#1: using the "epoll" event method
2022/01/14 06:27:51 [notice] 1#1: nginx/1.21.5
2022/01/14 06:27:51 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
2022/01/14 06:27:51 [notice] 1#1: OS: Linux 5.4.0-91-generic
2022/01/14 06:27:51 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2022/01/14 06:27:51 [notice] 1#1: start worker processes
2022/01/14 06:27:51 [notice] 1#1: start worker process 30
Perso je préfère utiliser une petite application répondant au nom de stern. Revenons à nos labels.
Comment le service sait qu’elle pod exposé? Et bien il utilise un selector
. Ce
selector, qui se trouve dans la partie spec, spécifie quel label doit être
utilisé pour identifier le(s) pod(s) à exposer.
Pour vérifier que le service est bien exposé, il suffit de se connecter sur un des noeuds du cluster et d’utiliser un bon vieux curl :
ssh master1
curl http://10.108.196.175
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
Pour aller plus loin
Nous avons vu comment générer rapidement des manifests, en attendant le prochain
billet sur les objets un peu plus évolué vous pouvez regarder ce
billet et testez par exemple popeye
qui vous indiquera les faiblesses de vos premiers manifests. Attention ne prenez
pas l’habitude de les utiliser si vous passez les certifs !