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 :
--installme permet de forcer l’installation de ma release, si elle n’existe pas--waitpermet d’attendre que tous les composants soient prêts (pods, ReplicaSet…)--atomicpermet de rollback et de supprimer toutes les ressources dans le cas où l’installation/upgrade échoue--namespacepour spécifier dans quel namespace k8s installer la release--create-namespaceforce 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.--debugseulement 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 😊