Introduction
Déployer sur Kubernetes depuis GitLab CI/CD devient fluide dès que les éléments clés sont maîtrisés : le GitLab runner, le kubeconfig, l'authentification au registry et les vérifications de rollout. Ce guide privilégie des correctifs concrets et fournit des extraits adaptables immédiatement. Vous verrez comment préparer un kubeconfig sûr, diagnostiquer les pannes de pipeline et de cluster, valider les rollouts et exécuter un pilote local avant d'élargir. Les mots-clés à garder en tête pour une recherche efficace et une mise en œuvre reproductible : GitLab CI/CD, Kubernetes deployment, GitLab runner, kubeconfig et CI/CD troubleshooting.
Vue d'ensemble du workflow
Un chemin simple et traçable du commit à des pods sains isole les pannes au bon endroit. Découpez chaque étape en job distinct pour une visibilité maximale :
- Build : créer une image conteneur taguée avec le SHA du commit.
- Push : s'authentifier au registry et pousser l'image.
- Configure : fournir kubectl et un kubeconfig à privilèges minimaux.
- Apply : appliquer les manifests ou exécuter Helm upgrade --install.
- Wait : attendre le statut de rollout avec un timeout.
- Verify : lancer des smoke checks contre Service/Ingress.
- Roll back : revenir au dernier ReplicaSet valide en cas d'échec de vérification.
Gardez chaque étape dans un job séparé pour savoir précisément où ça casse.
Mise en place : runners et kubeconfig
ServiceAccount et RBAC (portée namespace)
Utilisez un ServiceAccount dédié avec le minimum de privilèges dans le namespace cible :
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deployer
namespace: demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-deployer
namespace: demo
rules:
- apiGroups: ["", "apps", "extensions"]
resources: ["pods", "deployments", "replicasets", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer
namespace: demo
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: demo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ci-deployer
Créez un token court (Kubernetes 1.24+) :
kubectl -n demo create token ci-deployer > ci-deployer.token
Bâtissez un kubeconfig avec votre API server et la CA, puis stockez le YAML complet comme variable CI protégée et masquée nommée KUBE_CONFIG_B64 (encodé base64) ou KUBE_CONFIG_YAML (multiligne).
Écrire le kubeconfig dans le job
# .gitlab-ci.yml (extrait kubeconfig)
before_script:
- mkdir -p ~/.kube
- |
if [ -n "$KUBE_CONFIG_B64" ]; then
echo "$KUBE_CONFIG_B64" | base64 -d > ~/.kube/config
else
echo "$KUBE_CONFIG_YAML" > ~/.kube/config
fi
- chmod 600 ~/.kube/config
Choix de l'image du runner
Utilisez une image job incluant kubectl (par exemple, bitnami/kubectl: latest) ou installez kubectl à l'exécution. Si vous construisez des images en CI avec docker-in-docker, le runner doit être en mode privilégié. À défaut, privilégiez un builder comme Kaniko ou BuildKit en mode rootless.
Pipeline de référence : build, push, deploy
stages: [build, deploy]
variables:
IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
KUBE_NAMESPACE: "demo"
# Build et push via Docker-in-Docker (runner privilégié requis)
build:
stage: build
image: docker:25
services:
- name: docker:25-dind
command: ["--mtu=1460"]
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker build -t "$IMAGE" .
- docker push "$IMAGE"
rules:
- if: "$CI_COMMIT_BRANCH"
# Déploiement avec kubectl apply et attente du rollout
deploy:
stage: deploy
image: bitnami/kubectl: latest
before_script:
- mkdir -p ~/.kube
- if [ -n "$KUBE_CONFIG_B64" ]; then echo "$KUBE_CONFIG_B64" | base64 -d > ~/.kube/config; else echo "$KUBE_CONFIG_YAML" > ~/.kube/config; fi
- chmod 600 ~/.kube/config
script:
# S'assurer que le namespace existe
- kubectl get ns "$KUBE_NAMESPACE" || kubectl create ns "$KUBE_NAMESPACE"
# Créer/mettre à jour le secret d'extraction d'image (par namespace)
- |
kubectl -n "$KUBE_NAMESPACE" create secret docker-registry gitlab-regcred \
--docker-server="$CI_REGISTRY" \
--docker-username="$CI_REGISTRY_USER" \
--docker-password="$CI_REGISTRY_PASSWORD" \
--docker-email="[email protected]" \
--dry-run=client -o yaml | kubectl apply -f -
# Appliquer les manifests (Deployment référence imagePullSecrets)
- kubectl -n "$KUBE_NAMESPACE" apply -f k8s/
# Optionnel : forcer l'image avec le tag de commit
- kubectl -n "$KUBE_NAMESPACE" set image deploy/myapp myapp-container="$IMAGE" --record=true || true
# Attendre le rollout avec timeout
- kubectl -n "$KUBE_NAMESPACE" rollout status deploy/myapp --timeout=120s
# Smoke check simple via ClusterIP avec busybox curl (optionnel)
- kubectl -n "$KUBE_NAMESPACE" run tmp-curl --image=busybox:1.36 --restart=Never --rm -it -- curl -sS myapp:8080/healthz
environment:
name: demo/$CI_COMMIT_REF_NAME
url: https://demo.example.com
rules:
- if: "$CI_COMMIT_BRANCH == \"main\""
Exemple de Deployment (k8s/deploy.yaml) :
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
serviceAccountName: ci-deployer
imagePullSecrets:
- name: gitlab-regcred
containers:
- name: myapp-container
image: registry.example.com/group/project: CHANGE_ME
imagePullPolicy: Always
ports:
- containerPort: 8080
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 10
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 8080
targetPort: 8080
Dépannage : échecs de pipeline
Symptômes et vérifications rapides :
- Job bloqué en pending
- Cause : aucun runner compatible ou runner occupé.
- Correctif : alignez les tags du job et ceux du runner. Vérifiez l'état du runner dans les paramètres projet.
- docker build échoue ou ne peut joindre docker
- Cause : dind non activé ou runner non privilégié.
- Correctif : activez le mode privilégié, ou basculez vers Kaniko. Avec dind, validez DOCKER_HOST et DOCKER_TLS_CERTDIR.
- kubectl: command not found
- Cause : l'image job n'intègre pas kubectl.
- Correctif : utilisez une image avec kubectl (ex. bitnami/kubectl) ou installez-le dans le job.
- The request could not be authenticated or forbidden
- Cause : kubeconfig invalide, token expiré, ou RBAC insuffisant.
- Correctif : regénérez le token du ServiceAccount, vérifiez l'URL de l'API et la CA dans le kubeconfig, et accordez les verbes requis sur les ressources du namespace ciblé.
- apply échoue avec no matches for kind ou invalid
- Cause : apiVersion erronée ou schéma invalide.
- Correctif : validez localement :
kubectl apply --dry-run=client -f k8s/. Figez des apiVersions valides.
- Namespace not found
- Cause : déploiement dans un namespace inexistant.
- Correctif : créez le namespace avant l'application.
Dépannage : erreurs d'extraction d'image
Pod en ErrImagePull ou ImagePullBackOff :
- Mauvais tag d'image
- Vérifiez le tag exact dans le Deployment :
kubectl -n demo get deploy myapp -o jsonpath='{.spec.template.spec.containers[0].image}'. - Préférez les tags immuables :
$CI_COMMIT_SHORT_SHA.
- Authentification au registry manquante
- Assurez-vous qu'un Secret docker-registry existe dans le namespace et soit référencé par imagePullSecrets ou le ServiceAccount.
- Recréez le secret si les identifiants ont changé :
kubectl -n demo delete secret gitlab-regcred --ignore-not-found
kubectl -n demo create secret docker-registry gitlab-regcred \
--docker-server="$CI_REGISTRY" \
--docker-username="$CI_REGISTRY_USER" \
--docker-password="$CI_REGISTRY_PASSWORD" \
--docker-email="[email protected]"
- Politique et cache
- Avec :latest, définissez imagePullPolicy: Always pour éviter les images périmées.
- Mieux : épinglez le tag du commit et laissez IfNotPresent.
- Pulls inter-namespaces
- Les Secrets sont limités au namespace. Créez le même secret dans chaque namespace cible.
Pour confirmer, supprimez le Pod en échec et laissez le ReplicaSet le recréer, ou déclenchez un rollout : kubectl rollout restart deploy/myapp.
Dépannage : rollout et debugging
Automatisez l'attente et enrichissez les logs en cas d'échec :
# Attendre avec timeout
kubectl -n demo rollout status deploy/myapp --timeout=120s || {
echo "Rollout did not complete in time";
kubectl -n demo get pods -o wide;
kubectl -n demo describe deploy/myapp;
kubectl -n demo get events --sort-by=.lastTimestamp | tail -n 50;
exit 1;
}
Si le rollout échoue ou si les pods crashent :
- Describe et events
kubectl -n demo describe pod <name>révèle les soucis de pull, scheduling et probes.kubectl -n demo get events --sort-by=.lastTimestampremonte les erreurs récentes.
- Logs
kubectl -n demo logs deploy/myapp --all-containers=true --tail=200pour capter les erreurs applicatives.
- Probes et ressources
- Une readiness défaillante bloque le rollout. Vérifiez chemin et port. Si l'app démarre lentement, augmentez initialDelaySeconds.
- Contrôlez CPU/mémoire (requests/limits). OOMKilled indique une mémoire trop faible.
- Services et Ingress
- Assurez-vous que les sélecteurs du Service correspondent exactement aux labels des Pods.
- Vérifiez que targetPort == containerPort.
- Pour l'Ingress, validez host, TLS et l'état des backends. Ces points relèvent de Kubernetes Deployments, Kubernetes Ingress et Kubernetes debugging.
- ConfigMaps et Secrets
- Des clés manquantes ou des montages erronés causent souvent des CrashLoopBackOff. Comparez le spec du Pod en cours avec vos manifests :
kubectl -n demo get pod <name> -o yaml.
Rollback rapide :
kubectl -n demo rollout undo deploy/myapp
Contrôles de sécurité scriptés en CI
Ajoutez des pré/post-checks pour réduire l'incertitude :
# Valider les manifests avant apply
kubectl -n demo apply --dry-run=client -f k8s/
# Voir les diffs
kubectl -n demo diff -f k8s/ || true
# Après rollout, confirmer readiness et santé HTTP
kubectl -n demo get pods -l app=myapp -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.phase}{"\n"}{end}'
# Tester via ClusterIP depuis un Pod temporaire
kubectl -n demo run netcheck --image=busybox:1.36 --rm -it --restart=Never -- \
sh -c 'wget -qO- http://myapp:8080/healthz'
Plan pilote local
Démarrez petit, mesurez, et facilitez l'inspection locale avant d'étendre.
Portée
- Un service nommé myapp dans un namespace de staging demo-pilot.
- Tags immuables basés sur le SHA du commit.
Pipeline
- Jobs : build, deploy, verify.
- Pré-déploiement :
kubectl apply --dry-run=client -f k8s/etkubectl diff -f k8s/. - Déploiement : apply des manifests, image sur SHA, attente 120s.
- Vérification : curl /healthz, récupération des logs, liste des events.
Succès mesurable
- Rollout terminé en moins de 2 minutes.
- L'endpoint de santé retourne 200.
- Aucun ImagePullBackOff ni CrashLoopBackOff dans les 50 derniers events.
Sécurité et rollback
- Gardez
kubectl rollout undo deploy/myappexécuté en cas d'échec. - Namespace et ServiceAccount séparés. Aucune donnée de production.
Quand c'est stable sur quelques runs, répliquez le même schéma vers un staging plus large ou un canary.
Conclusion
Séparez les étapes de déploiement, fournissez un kubeconfig sécurisé, et scriptiez l'attente de rollout et les vérifications pour raccourcir le temps de diagnostic. Commencez par le plan pilote, mesurez les résultats, puis étendez une fois que vous observez régulièrement des rollouts propres, des probes saines et des retours arrière simples et rapides.