Kubernetes

De Justine's wiki
Aller à la navigation Aller à la recherche

Présentation vite fait

Kubernetes (communément appelé « K8s2 ») est un système open source qui vise à fournir une « plate-forme permettant d'automatiser le déploiement, la montée en charge et la mise en œuvre de conteneurs d'application sur des clusters de serveurs »3. Il fonctionne avec toute une série de technologies de conteneurisation, et est souvent utilisé avec Docker. Il a été conçu à l'origine par Google, puis offert à la Cloud Native Computing Foundation.

Un cluster k8s contient deux types de machines (ou nodes):

  • Un Master qui coordonne le cluster: scheduling, maintien de leur état recherché, scaling, application des mises à jour...
  • Des workers, où tournent les applications: Cela peut être une VM ou une machine physique, utilisé comme worker et managée par le Master. Typiquement, un worker contient des outils pour gérer les opérations sur le conteneur (Docker, rkt) et Kubelet, un agent servant à manager le noeud. Un cluster de prod devrait avoir au moins trois noeuds.

Terminologie

Kubernetes contient un certain nombre de termes particuliers. Voir les sources.

Mise en oeuvre

Je pars d'un cluster à 2 machines sous Debian 11.

Je vais utiliser k3s pour la mise en place, une distribution légère de k8s très rapide à installer.

MEP

Prérequis

  • ici
  • 2 nodes ne peuvent pas avoir le même hostname
  • 512Mo de RAm et 1 CPU
  • SSD recommandé

Ports:

  • 6443 pour tous les nodes (protocole ?)
  • 8472/udp si utilisation de VXLAN Flannel
  • Port 10250 pour le serveur de métriques
  • Ports 2379 et 2380 si utilisation de etcd

Install

Mon premier node sera master, le second un simple node. Tout se fait en root.

Sur le master:

#curl -sfL https://get.k3s.io | sh -
#Mieux, sans son traefik intégré
curl -sfL https://get.k3s.io | sh -s - server --no-deploy=traefik
#On peut spécifier le token
curl -sfL https://get.k3s.io | sh -s - server --token=<ayo le token> --no-deploy=traefik
kubectl get node

Le script installe kubernetes et je vois que mon serveur est désormais noeud du cluster.

Mon cluster est identifié par un token présent sur le master. Je vais le récupérer:

/var/lib/rancher/k3s/server/node-token

Sur le worker, c'est un peu la même chose, mais on ajoute l'adresse du master et le token:

curl -sfL https://get.k3s.io | K3S_URL=https://myserver:6443 K3S_TOKEN=mynodetoken sh -

Je peux voir mes 2 machines sur le worker:

kubectl get node

Si je veux pouvoir lancer kubectl sur mon pod depuis une autre machine, je vais sur mon manager et je récupère sa conf dans

/etc/rancher/k3s/k3s.yaml

Et je vais la coller sur mon autre machine dans

~/.kube/config

En prenant soin de modifier la section "adress" en remplacement 127.0.0.1 par l'adresse de mon manager

Désinstallation si besoin

Pour un master:

/usr/local/bin/k3s-uninstall.sh

Pour un worker:

/usr/local/bin/k3s-agent-uninstall.sh

Explications théoriques, déploiement manuel

Je vais tenter de déployer mon premier service, ma propre application (sqnotes). Celle-ci requiert idéalement un accès en http et un volume.

Tout va se faire en utilisant la commande kubectl depuis le master.

Distinction pod / deployment / service

Un pod est un groupe de conteneurs, que l'on met ensemble pour des raisons d'administration.

Un deployment est l'expression d'un état désiré pour un pod. Schématiquement, "Je veux que mon pod untel soit actif".

Un service est une façon abstraite d'exposer une application tournant sur un ensemble de pods en tant que service réseau.

Déploiement

kubectl

C'est la commande principale servant à gérer un cluster kub.

Sa doc est dispo ici

Un deployment

Déployer mon conteneur en tant que simple déploiement est... plutôt simple:

kubectl create deployment sqnotes --image=squi/sqnotes:1.0

Informations sur mon deployment

Je peux ensuite le voir:

$ kubectl get pods                                      
NAME                       READY   STATUS              RESTARTS   AGE
sqnotes-85dd78574f-p27wb   0/1     ContainerCreating   0          5s
$ kubectl get deployments
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
sqnotes   1/1     1            1           41s

...ainsi que ses évènements

kubectl get events

...avoir une description détaillée de mes deploiements

kubectl describe deployments

Supprimer mon déployment

kubectl delete deployment sqnotes

Un service

Parlons un peu de réseau...

Par défaut, mon pod n'est accessible que depuis son IP interne, depuis l'intérieur du cluster. Pour que mon pod soit accessible de l'extérieur, il faut en faire un service. Un service (le nom est mal choisi), c'est une ouverture réseau entrant vers l'appli.

Types de services

Il existe 4 types de service:

  • ClusterIP (par défaut): le service n'est pas accessible de l'extérieur, il n'a qu'une IP interne au cluster.
  • NodePort: Expose le service sur l'IP de chaque node avec un port statique : le node port. Un service ClusterIP est automatiquement créé; NodePort redirige sur celui-ci.
  • LoadBalancer: Expose le service en utilisant le lb d'un fournisseur. Les services NodePort et ClusterIP sont créés, et le lb redirige sur ceux-ci.
  • ExternalName: Mappe le service au contenu du champ externalName (par exemple un fqdn foo.bar.example.com) en renvoyant un CNAME. Aucun proxy n'est mis en place.


Faire un service

La commande suivante va donc exposer le port 8080 de mon deployment:

kubectl expose deployment sqnotes --type=LoadBalancer --port=8080
  • --type=LoadBalancer signifie que le service doit être accessible de l'extérieur.

J'expose le port 8080 car celui utilisé par mon image docker.

Voir un service

kubectl get services

Me donne:

NAME         TYPE           CLUSTER-IP      EXTERNAL-IP                   PORT(S)          AGE
kubernetes   ClusterIP      10.43.0.1       <none>                        443/TCP          54m
sqnotes      LoadBalancer   10.43.138.228   192.168.1.208,192.168.1.209   8080:31349/TCP   104s

Delete un service

Supprimer le deployment ne suffit pas, il faut aussi supprimer le service.

kubectl delete service sqnotes

Stockage

Volumes

K8s se base sur des volumes. Ceux-ci sont provisionnés sans se soucier de l'infrastructure sous-jacente; ainsi, un partage réseau par exemple peut servir à stocker les données des conteneurs et à les partager entre eux, comme par exemple avec un cluster Ceph !

Un volume est donc "une unité de stockage abstraite". K8s propose en réalité un tas de plugins de stockage qui peuvent fonctionne sur AWX, Azure, VMWare, d'autres choses...

Volumes et Volumes persistants

Les volumes normaux sont éphémères. Les volumes persistants, déployés de la même façon que les pods, fournissent du stockage à long terme (même si les pods sont arrêtés).

Les nodes peuvent provisionner des volumes en faisant des claim, et en précisant le type de stockage demandé. On peut définir des objets StorageClass qui spécifient quels stockages sont sont dispo. Le cluster va, lors d'un claim, chercher un stockage adapté en fonction de sa StorageClass et faire le lien entre un claim et un volume.

Provisionnement Statique / dynamique

Les volumes peuvent être créés de façon statique ou dynamique. Si c'est statique, le cluster dispose d'un ensemble fixe de volumes; les claims sont rattachés à l'un deux si cela correspond aux critères demandés (débit, prix, etc).

Le provisionnement dynamique fait que les volumes sont créés automatiquement en réponse à un claim. K8s ne fait qu'allouer le stockage, pas les backups ou la HA sur celui-ci.

Types de volumes

Au fond, un volume est un dossier contenant ou pas des données, accessible aux conteneurs d'un pod. Comment ce dossier est créé et accessible dépend du type de volume; contrairement à Docker, k8s propose divers drivers de volumes.

Je ne vais pas faire la liste de tous les types de volumes (rien que pour les volumes AWS, ce serait long...). Les volumes sont déclarés dans .spec.volumes et les points de montage dans .spec.containers[*].volumeMounts. Comme pour Docker, le conteneur verra son FS interne de base plus ses volumes. On ne peut pas mettre un volume dans un volume, mais la doc indique de voir ce lien pour ce genre de choses.

Une liste des types de volumes.

On peut faire du glusterfs, du cephfs, du nfs, de l'iscsi, etc.

Déploiement d'une application (avec un fichier)

* Doc

Le passage par des fichiers va nous permettre de nous y retrouver un peu plus et de faire des parallèles avec docker et docker-compose. Notamment, cela va faciliter la gestion des replicas et la gestion du stockage.

Charts

Les fichiers en yaml utilisés par k8s pour décrire des objets s'appellent des charts. Il s'agit tout simplement de fichiers décrivant un appel à l'API de k8s.

Ses 4 premiers champs sont à peu près toujours les mêmes:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: qqchose
spec:

  • apiVersion: Quelle version de l'API K8s on utilise. Là, c'est un peu le bordel : ça change d'un objet à l'autre.
  • kind : quel genre d'objet on veut créer.
  • metadata : Des données aidant à identifier l'objet : un name, un UID, et éventuellement un namespace.
  • spec : les spécifications pour cet objet.

ReplicaSet

Un ReplicaSet (ensemble de réplicas en français) a pour but de maintenir un ensemble stable de Pods à un moment donné. Cet objet est souvent utilisé pour garantir la disponibilité d'un certain nombre identique de Pods.

C'est en gros un objet qui va gérer la replication des pods. Il est défini par plusieurs choses:

  • un sélecteur qui précise comment savoir quels pods il peut posséder
  • le nombre de replicas qu'il doit maintenir en vie
  • un template de pod qu'il va pouvoir utiliser pour en créer de nouveaux.

Le replicaSet se base sur un champ de métadonnée (metadata.ownerReferences) du pod qui précise son replicaSet. Il peut identifier des nouveaux pods à acquérir avec son sélecteur, si celui-ci n'as pas de "ownerReferences".

Exemple de fichier de replicaSet:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: frontend
  labels:
    app: guestbook
    tier: frontend
spec:
  # modify replicas according to your case
  replicas: 3
  selector:
    matchLabels:
      tier: frontend
  template:
    metadata:
      labels:
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: gcr.io/google_samples/gb-frontend:v3


Exemple de deployment

Exemple de deployment commenté:

#Version de l'API k8s utilisée pour ce déploiement
#visible avec kubectl api-versions
apiVersion: apps/v1
#Ce fichier représente un déploiement (complet donc)
kind: Deployment
#Section définissant les métadonnées du deploiement
metadata:
  #Son nom
  name: nginx-deployment
  labels:
    #Ce label est réutilisé plus bas pour le replicaSet
    app: nginx
#Sepcifications du déploiement
spec:
  #3 replicas...
  replicas: 3
  #...identifié par le label nginx
  selector:
    matchLabels:
      app: nginx
  #Template de pod pour le replicaSet:
  #c'est en fait ici qu'on va dire quel conteneur on veut.
  template:
    metadata:
      labels:
        #Ne pas oublier le label
        app: nginx
    spec:
      containers:
        #Spécification du conteneur
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

J'écris donc un déploiement pour mon application de prise de notes.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sqnotes
  labels:
    app: sqnotes
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sqnotes
  template:
    metadata:
      labels:
        app: sqnotes
    spec:
      containers:
      - name: sqnotes
        image: squi/sqnotes:1.0
        ports:
        - containerPort: 8080

et je le lance avec:

kubectl apply -f sqnotes.yml

Ça fonctionne... sauf que je n'ai pas le service.

Exemple de service

Puisqu'un service est un objet (comme un pod, comme un replicaSet, etc...) on peut le décrire dans un fichier yaml avant de le balancer à l'API.

Exemple:

#version de l'API... toujours pas compris ce champ
apiVersion: v1
#c'est un service
kind: Service
#avec un nom
metadata:
  name: my-service
spec:
  #son type
  type: ClusterIP
  #il s'applique en fonction d'un label
  selector:
    app: MyApp
  ports:
    #le port 80 du node donnera sur le port 9376 du conteneur.
    - protocol: TCP
      port: 80
      targetPort: 9376

Je vais donc écrire un service de type NodePort pour mon appli:

apiVersion: v1
kind: Service
metadata:
  name: sqnotes-ingress
spec:
  type: NodePort
  selector:
    app: sqnotes
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30081

Et voilà. En accédant au port 30081 de mon node, j'accède à mon appli.

TODO : apprendre à gérer le LB.

Exemple de volume

J'ai un déploiement, un service, il me manque un volume. Nous allons donc devoir créer deux objets:

  • Un persistent volume
  • Un persistent volume claim

Les deux vont dans le même fichier. Je dispose en outre d'un serveur nfs sur nas.sq.lan.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: sqnotes-data
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  nfs:
    server: nas.sq.lan
    path: "/k8s-data/sqnotes"
  mountOptions:
    - nfsvers=4.2

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sqnotes-data
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ""
  resources:
    requests:
      storage: 10Gi
  volumeName: sqnotes-data

Les options sont assez explicites pour la plupart. L'option accessModes offre plusieurs options:

  • ReadWriteOnce – the volume can be mounted as read-write by a single node. => Faux... C'est les déploiements, pas les pods.
  • ReadOnlyMany – the volume can be mounted read-only by many nodes.
  • ReadWriteMany – the volume can be mounted as read-write by many nodes.

Je dois ensuite modifier fichier de déploiement pour qu'il prenne en compte ses volumes.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sqnotes
  labels:
    app: sqnotes
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sqnotes
  template:
    metadata:
      labels:
        app: sqnotes
    spec:
      containers:
        - name: sqnotes
          image: squi/sqnotes:1.0
          ports:
            - containerPort: 8080
          volumeMounts:
            - mountPath: /app/data
              name: sqnotes-data
            - mountPath: /etc/timezone
              name: sqnotes-timezone
              readOnly: true
            - mountPath: /etc/localtime
              name: sqnotes-localtime
              readOnly: true
      volumes:
        - name: sqnotes-data
          persistentVolumeClaim:
            claimName: sqnotes-data
        - name: sqnotes-timezone
          hostPath:
            path: /etc/timezone
        - name: sqnotes-localtime
          hostPath:
            path: /etc/localtime

Et voilà !

Ingress

Un ingress est un objet qui gère les accès externes à un service, généralement en HTTP. En gros, c'est un reverse-proxy : il peut fournir du load balancing, du ssl, des vhosts.

Ingress controller : HAProxy

Avant d'avoir un ingress, il nous faut un contrôleur pour ce dernier. Il en existe des pelletées (cf la doc). Je vais me baser sur HAProxy, car je le connais bien.

Pour l'installer, il faut passer par Helm.

helm repo add haproxy-ingress https://haproxy-ingress.github.io/charts
mkdir haproxy-ingress && cd haproxy-ingress

Je créée ensuite un fichier haproxy-inigress-values.yaml dans lequel je mets:

controller:
  hostNetwork: true

Je n'ai pas trop compris, ça. La doc haproxy mentionne toutes les options possibles. Concernant celle-ci, ce n'est pas clair... K8s est décidemment mal documenté et foutrement compliqué pour rien.

Enfin, je peux passer à l'install.

helm install haproxy-ingress haproxy-ingress/haproxy-ingress\
  --create-namespace --namespace ingress-controller\
  --version 0.13.6\
  -f haproxy-ingress-values.yaml

Note : je pourrais remplacer "install" par "upgrade" pour une màj.

Je peux voir qu'il fonctionne:

kubectl get pods --all-namespaces | grep ingress
ingress-controller   haproxy-ingress-9c898b6bf-w2ckb           0/1     Pending     0          8m17s
ingress-controller   svclb-haproxy-ingress-zkgqj               0/2     Pending     0          8m16s
ingress-controller   svclb-haproxy-ingress-7fxfw               0/2     Pending     0          8m16s

Déployer mon appli sur mon ingress

Je peux ensuite créer un fichier d'ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sqnotes
spec:
  rules:
  - host: notes.sq.lan
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: sqnotes
            port:
              number: 80


...ça marche pas ;_;. Ou plutôt si, ça marche, mais seulement depuis l'intérieur de mon cluster. Je demande plus de détails:

319 root@swarm1|sqnotes-k8s > kubectl describe svc sqnotes-ingress
Name:              sqnotes-ingress
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=sqnotes
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.43.225.129
IPs:               10.43.225.129
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         10.42.0.20:8080,10.42.1.27:8080,10.42.1.28:8080
Session Affinity:  None
Events:            <none>

JE SAIS PAS. Merde.

Helm

C'est le gestionnaire de paquets pour k8s. Il permet de partager des charts (les fichiers de conf de k8S sont appellés des charts), un peu à la manière du dockerhub. Je vais commencer par l'installer. Pour ça, je vais passer par un repo.

curl https://baltocdn.com/helm/signing.asc | sudo apt-key add -
sudo apt-get install apt-transport-https --yes
echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

Helm se base sur une variable d'environnement pour retrouver le cluster kub. Je dois donc ajouter cet export à mon zshrc / bashrc:

export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

Voilà. Je peux désormais trouver des charts sur le site officiel et les installer avec helm.


Commandes courantes

= Appliquer tous les fichiers du répertoire courant

kubectl apply -f .

Supprimer tous les objets des fichiers du répertoire courant

kubectl delete -f .

Voir mes pods et leur répartition

kubectl get pods -o wide

Logs d'un pod

kubectl logs <pod_id>