Comment installer un cluster kubernetes

Cela fait longtemps que je souhaite installer un cluster kubernetes, créer mon site, mon blog et j’ai mis longtemps à m’y mettre. Mais ça y est, et voici un petit retour d’expérience sur la mise en place d’un site sur un serveur kubernetes.

Disclaimer : j’ai pas choisi le chemin le plus simple, mais j’ai voulu tirer profit de l’expérience acquise sur kubernetes et d’autres outils devops.


Mise en place

Pré-requis

Avant toute chose, j’ai pris un serveur dédié chez OVH, un kimsufi pour commencer qui coûte 9€ hors taxes. J’ai ensuite pris un nom de domaine, à 3€ et configuré ses DNS pour le faire pointer vers mon nouveau serveur. J’y ai installé un Ubuntu 22.04.

Installer un cluster kubernetes

Une fois connecté en SSH à mon serveur, j’ai commencé par installer microk8s. C’est un outil pour installer un cluster kubernetes comme son nom l’indique, qui vient avec toute l’artillerie d’un devops. On peut donc y installer un cluster kubernetes, des plugins, l’utiliser pour de l’IoT, sur desktop… et il est ready to prod !
Pour l’installer il suffit juste de le faire via snap grâce à cette commande :

$ sudo snap install microk8s --classic

Vous pouvez aussi suivre la documentation qui est plutôt claire dessus.
En une ligne de commande, j’ai pu installer un cluster kubernetes sur mon serveur dédié, que j’ai pu démarrer simplement :

$ microk8s start

Préparation

La première chose que j’ai voulu faire était de déployer une première app via une CI pour pouvoir ensuite me concentrer sur l’important à mon sens et itérer. Comme l’a dit quelqu’un de très sensé :

Un des ingrédients d’un side project réussi, est de le mettre en prod

Tom32i™

Git

Pour démarrer, je me suis créé un projet pour versionner tout mon code, autant pour l’application que pour l’infrastructure. Je l’ai ensuite poussé sur GitHub.

Du code (en go)

J’ai ensuite écrit un simple « Hello, world! » en go qui écoute sur le port 8089.

package main

import (
	"fmt"
	"net/http"
)

func main() {
	fmt.Println("Listening on port 8089...")
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, world!")
	})
	http.ListenAndServe(":8089", nil)
}

Docker

Une fois que ce bout de code compile bien, j’ai décidé de le builder et de l’exécuter dans une image docker. D’ailleurs, k8s étant un orchestrateur de conteneurs, il est nécessaire de se builder des images pour les fournir à k8s qui se chargera de les faire tourner. La manière la plus simple est d’ajouter ces lignes dans un Dockerfile.

[...]
RUN CGO_ENABLED=0 go build -o /go/bin/app /go/src/app/cmd/main.go

CMD ["/app"]

Pour ensuite tester mon image, j’ai exécutée cette petite commande qui m’assure qu’elle se build bien comme il faut :

$ docker build -f path/to/Dockerfile . -t repository/name:tag

Et nous voilà avec une petite image docker prête à être exécutée par k8s !
Mais pour le moment elle est sur ma machine. Je l’ai d’abord envoyée manuellement sur un repository privé sur docker hub.

$ docker push repository/image

Configuration

La dernière étape de cette préparation est maintenant de configurer premièrement ma machine locale, pour que je sois capable d’installer mes template helm depuis ma CLI, et ensuite de configurer microk8s pour ajouter les éléments dont je vais avoir besoin.

K8S

Pour ma configuration locale, il faut récupérer la configuration. Elle est disponible une fois que l’on a pu installer le cluster kubernetes.

$ microk8s.kubectl config view --raw

Cette commande permet d’afficher le contenu du fichier de configuration de mon cluster. Je l’ai récupérée sur ma machine et je l’ai placée dans un fichier ~/.kube./config. Dans ma nouvelle configuration, il faut également changer l’adresse IP locale par l’IP de mon serveur dédié.

Helm

J’ai aussi besoin d’installer helm pour pouvoir installer des templates depuis ma machine vers mon cluster pour me simplifier la vie, plutôt que de lancer des commandes kubectl apply. Pour l’installer, il m’a suffit de lancer simplement une commande homebrew et le tour est joué.

$ brew install helm

Pour vérifier que tout fonctionne bien, j’ai tenté de lister les charts installés :

$ helm list
NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSION

En récapitulant, j’ai :

  • installé un cluster k8s
  • installé l’outillage nécéssaire
  • créé l’image d’une application http que j’ai envoyé sur un registry

Tout semble prêt maintenant pour commencer à écrire notre premier chart helm pour déployer notre application dans notre cluster !

Déploiement

Microk8s

Avant de passer à l’étape du déploiement, il faut que je m’assure que mon serveur k8s dispose des bons droits pour pouvoir pull l’image docker nécessaire pour lancer mon pod. Il y a plusieurs manières de procéder, comme d’ajouter un secret qui contient les informations d’identification de docker. Cette configuration est détaillée sur la documentation de kubernetes.

Helm

Création de la release

Pour commencer à déployer ma petite application sur mon cluster, il m’a suffit de lancer un init pour ma release helm :

$ helm create my_release

Cette commande génère tout un tas de fichiers nécessaires pour avoir un set de déploiement packagé en release, et templatisé plutôt que d’appliquer chaque manifeste kubernetes à la main, un par un.
J’ai ensuite configuré mon template pour coller à mon besoin :

image:
  repository: private/repository
  # Pour ne pas utiliser le cache lors du déploiement
  pullPolicy: Always
  # latest par défaut
  tag: ""
service:
  type: LoadBalancer
  port: 8089 # Le port sur lequel mon app écoute
  selectorLabels:
    name: nginx

En parallèle, j’ai besoin d’installer un ingress-controller pour être capable d’écouter le traffic et de le rediriger vers le bon service. Via microk8s, c’est assez simple de l’activer :

microk8s enable ingress

Je peux maintenant spécifier dans ma configuration helm, l’activation d’un ingress :

ingress:
  enabled: true
  className: nginx
  annotations:
    kubernetes.io/ingress.class: nginx
  hosts:
    - host: nom.de.domaine.tld
      paths:
        - path: /
          pathType: ImplementationSpecific

Pour terminer, j’ai pu tester ma configuration helm en essayant de l’appliquer sur mon cluster. Normalement avec un .kube/config correct, je peux lancer l’installation de ma release helm de cette manière.

Installation de la release

$ helm upgrade release_name ./path/to/helm --install --wait --atomic --namespace=k8s_namespace --values=./path/to/values.yaml --create-namespace --debug

Cette commande prends plusieurs arguments et options spécifiques. D’abord je fais un upgrade pour mettre à jour l’installation si la release, même si elle n’est pas présente. Ensuite je donne en arguments le nom de ma release, ainsi que le chemin du dossier helm. Je spécifie également quelques options nécessaires :

  • --install me permet de forcer l’installation de ma release, si elle n’existe pas
  • --wait permet d’attendre que tous les composants soient prêts (pods, ReplicaSet…)
  • --atomic permet de rollback et de supprimer toutes les ressources dans le cas où l’installation/upgrade échoue
  • --namespace pour spécifier dans quel namespace k8s installer la release
  • --create-namespace force la création d’un namespace s’il est pas présent. Ça m’a permis de ne pas me soucier de le créer en amont ou non.
  • --debug seulement pour avoir un peu plus de verbosité

Il y a également une option qui m’a pas mal aidé au début, c’était de spécifier l’adresse et le port de mon serveur kubernetes : --kube-apiserver=https://CLUSTER_IP:PORT

Si tout se passe bien la sortie de la console devrait afficher quelque chose comme ça :

Release "my_release" has been upgraded. Happy Helming!

🎉 Ça y est, on a installé notre première release helm sur notre cluster k8s flambant neuf. En me rendant sur l’endpoint spécifié dans l’ingress, je peux enfin voir mon « Hello, World! » s’afficher !

Https

Il est possible d’installer un certificat SSL/TLS Let’s Encrypt automatiquement ainsi que le renouvellement de certificat sur le cluster. Pour ça, j’ai du dans un premier temps installer un certificat manager sur mon cluster :

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.1.1/cert-manager.yaml

Une fois mon certificat manager installé, j’ai eu quelques nouveaux pods de créés sur le namespace cert-manager

Ensuite j’ai installé un cluster-issuer qui va s’occuper de faire les demandes de certificat à Let’s Encrypt. J’ai créé un fichier clusterissuer.yaml et j’y ai ajouté cette configuration :

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: lets-encrypt
spec:
  acme:
    email: email@domain.tld
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: lets-encrypt-private-key
    solvers:
      - http01:
          ingress:
            class: public

Après un petit kubectl apply -f clusterissuer.yaml de mon fichier, je peux enfin modifier la configuration de mon ingress et lui ajouter ceci :

ingress:
  enabled: true
  className: nginx
  annotations:
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    cert-manager.io/cluster-issuer: lets-encrypt
  hosts:
    - host: nom.de.domaine.tld
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls:
    - secretName: site-tls
      hosts:
        - nom.de.domaine.tld

Le plus important ici a été de faire le lien avec le cluster-issuer grâce à cette annotation : cert-manager.io/cluster-issuer. J’ai également activé le TLS dans ma configuration, et après avoir lancé un upgrade de ma release helm, mon site est désormais accessible via https !

Debug

Pour débugger les charts installé, et les ressources k8s que j’ai créé, il m’a été très utile d’interagir avec mon serveur k8s. Par exemple, lorsque la sortie de la console helm indique qu’un pod est en attendre d’être démarré. La commande microk8s kubectl get pods -n mon-namespace pour récupérer les noms des pods, puis microk8s kubectl describe pod -n mon-namespace pour afficher la configuration installée. La commande describe permet aussi d’afficher les Events, indispensable pour identifier les potentielles erreurs lorsqu’un pod n’arrive pas à démarrer.

Une fois à l’aise avec ces commandes, j’ai utilisé un outil qui simplifie grandement la vie sur ma machine : k9s. Cet outil permet de se connecter à son serveur k8s en CLI et permet de naviguer aisément entre les différentes ressources.

Automatisation (CI/CD)

Maintenant que je sais installer une release helm sur mon serveur k8s, j’aimerais pouvoir travailler en faisant des pull requests sur mon repository personnel, et au merge sur main, déployer automatiquement mon projet. Il me faut alors une pipeline d’intégration et de déploiement continue.

Pour cette pipeline, j’ai décidé d’utiliser Github Actions. C’est possible de l’utiliser gratuitement pour un repository privé ou public, dans la limite d’environ 33 heures par mois. C’est à mon sens amplement suffisant pour un side project.

J’ai commencé par créer un dossier .github à la racine de mon projet, puis un dossier workflows. Le nom est important pour que Github puisse reconnaitre les actions définies à l’intérieur du dossier. Enfin j’ai créé une action appelée deployment.yaml.

name: 🚀 Deployment

on:
  push:
    branches:
      - "main"
  workflow_dispatch: {}

Grâce à cette configuration, Github est capable de lancer un runner au moment du merge sur master, ou de lancer ce workflow à la main n’importe quand.
Mon action se divise ensuite en deux job distincts. Le premier me permet de builder une image docker de mon projet et de l’envoyer sur mon repository docker privé, et le deuxième job me permet d’appliquer ma configuration helm sur mon serveur kubernetes.

Builder l’image docker

Avant d’ajouter mon premier job Github Actions, il m’a fallu renseigner mes identifiants Docker Hub dans les paramètres de mon repository, dans l’onglet Secrets and variables > Actions. Ensuite j’ai pu ajouter simplement mes nouveaux secrets : mon login docker hub ainsi que mon mot de passe.

Pour builder automatiquement mon image docker, je peux déclarer maintenant un premier job qui va se connecter à Docker Hub grâce aux secrets renseignés :

jobs:
  docker-build:
    name: Build docker image
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Set up docker tag
        run: echo "DOCKER_TAG=${BRANCH_NAME/\//-}" >> $GITHUB_ENV

J’utilise l’action docker/setup-buildx-action qui n’est pas nécessaire pour l’exemple, mais qui est recommandée pour pouvoir builder des images multi-plateformes. J’ai eu aussi besoin de générer un tag pour mon image, en me basant sur la branche courante.
Et pour terminer, il suffit d’envoyer cette nouvelle image dans le repository distant. Et bien sur, une action est aussi faite pour ça :

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: repository/app:${{ env.DOCKER_TAG }}

En lançant ce job, mon image se build et est envoyée sur docker.

Appliquer la configuration helm

Le deuxième job que j’ai écrit pour automatiser tout ça est celui qui va me permettre d’installer ma release helm sur mon serveur k8s. Mais avant il y a quelques configurations à vérifier. Comme j’ai besoin de me connecter à mon cluster via les runners de Github, il faut également que j’ajoute dans les secrets de mon repository le contenu de mon fichier .kube/config. De la même manière que pour les identifiants docker, j’ai ajouté un nouveau secret appelé KUBE_CONFIG avec comme valeur le contenu de mon fichier de configuration encodé en base 64.

$ cat ~/.kube/config | base64 | pbcopy

Une fois le secret enregistré, notre runner devrait être capable de s’identifier auprès de notre cluster pour installer la nouvelle version de notre chart. Passons alors au job.
Il est plutôt concis et consiste en deux étapes. La première afin d’injecter notre configuration kube dans le runner, puis de lancer la commande d’installation de notre chart helm :

  helm-deploy:
    name: Upgrading helm chart
    runs-on: ubuntu-latest
    needs: docker-build
    steps:
      - uses: actions/checkout@v4
      - name: Config
        run: |
          mkdir $HOME/.kube
          echo "${{ secrets.KUBE_CONFIG }}" | base64 --decode > $HOME/.kube/config
      - name: Deploy
        uses: WyriHaximus/github-action-helm3@v4.0
        with:
          exec: helm upgrade release_name ./path/to/helm --install --wait --atomic --namespace=k8s_namespace --values=./path/to/values.yaml --create-namespace --debug

Il n’y a plus qu’a merger ce code sur la branche main et voir le résultat :

Conclusion

Et voilà ! Après toutes ces étapes, j’ai été capable d’installer un cluster kubernetes, de le configurer et d’automatiser les déploiements sur celui-ci grâce à une CI 🎉
Tout n’est évidemment pas parfait, et je vais continuer à travailler dessus pour corriger ce qui me semble important à mon échelle.
Je suis également preneur de feedbacks, n’hésitez pas à me donner vos tips et points de vue en commentaire 😊

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Retour en haut