Déployer à chaque push avec la CI

Un ServiceAccount qui ne peut toucher qu'à ton namespace, son kubeconfig en secret de CI, et un pipeline qui construit l'image à chaque push, la tague avec le commit, et la déploie.

intermédiaire~35 min de manipulation
#kubernetes#ci#github-actions#gitlab-ci#kustomize#rbac

Pas encore validée de bout en bout — sois le premier.Signaler un problème

Brouillon — pas encore exécuté de bout en bout. Cette page est écrite mais son auteur ne l'a pas encore déroulée sur une vraie machine. Des commandes peuvent être fausses : lis avant de lancer, et dis-nous ce qui casse.

En un coup d'œilDu git push aux pods qui tournent
Ta forge
Ton cluster
pushon: pushdocker pushkubectl apply -krolloutpull :sha
Toigit push origin main
Dépôt
GitHub Actionsbuild → push → deploy
Registre:<sha>
API server:6443
indicatDeployment ·

Un push sur main lance le pipeline : il construit l'image, la pousse dans le registre taguée avec le SHA du commit, puis, avec un kubeconfig limité à ${NAMESPACE}, applique les manifestes avec ce tag et attend le rollout.

La page précédente déployait indicat à la main depuis ton portable, avec un kubeconfig qui est cluster-admin. Cette page confie le travail à GitHub Actions : un push sur main construit l'image, la tague avec le SHA du commit, la pousse dans le registre et la déploie, avec une identité qui ne peut toucher qu'à .

Avant de commencer

Un ServiceAccount pour le pipeline

deploy/ci-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deploy
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-deploy
rules:
- apiGroups: [""]
resources: [pods, pods/log, services]
verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [apps]
resources: [deployments, replicasets]
verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [batch]
resources: [jobs]
verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [autoscaling]
resources: [horizontalpodautoscalers]
verbs: [get, list, watch, create, update, patch, delete]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deploy
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ci-deploy
subjects:
- kind: ServiceAccount
name: ci-deploy
namespace:
$kubectl -n apply -f deploy/ci-rbac.yaml
Vérification
$kubectl -n kube-system auth can-i get secrets --as=system:serviceaccount::ci-deploy
Retour attendu
no

Un kubeconfig pour le pipeline

deploy/ci-token.yaml
apiVersion: v1
kind: Secret
metadata:
name: ci-deploy-token
annotations:
kubernetes.io/service-account.name: ci-deploy
type: kubernetes.io/service-account-token
$kubectl -n apply -f deploy/ci-token.yaml
$TOKEN=$(kubectl -n get secret ci-deploy-token -o jsonpath='{.data.token}' | base64 -d)
$CA=$(kubectl -n get secret ci-deploy-token -o jsonpath='{.data.ca\.crt}')
$cat > ci-kubeconfig.yaml <<EOF
$apiVersion: v1
$kind: Config
$clusters:
$ - name: k3s
$ cluster: { server: "https://:6443", certificate-authority-data: $CA }
$users:
$ - name: ci-deploy
$ user: { token: $TOKEN }
$contexts:
$ - name: ci
$ context: { cluster: k3s, user: ci-deploy, namespace: }
$current-context: ci
$EOF
Vérification
$kubectl --kubeconfig ci-kubeconfig.yaml auth can-i create deployments
Retour attendu
yes

Le secret de CI et le registre

$gh secret set KUBECONFIG_B64 --repo < <(base64 -w0 ci-kubeconfig.yaml)
$rm ci-kubeconfig.yaml

kustomize : un seul endroit pour le tag d'image

deploy/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace:
resources: [migrate.yaml, deployment.yaml, service.yaml, ingress.yaml, hpa.yaml]
images:
- name:
newTag: latest
Vérification
$kubectl kustomize deploy/ | grep -c 'image: :latest'
Retour attendu
2

Le pipeline

.github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: :${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
env:
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
steps:
- uses: actions/checkout@v4
- name: Kubeconfig
run: |
mkdir -p ~/.kube
echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Deploy
run: |
sed -i "s|newTag: .*|newTag: ${{ github.sha }}|" deploy/kustomization.yaml
kubectl delete job indicat-migrate --ignore-not-found
kubectl apply -k deploy/ -l app=indicat-migrate
kubectl wait --for=condition=complete job/indicat-migrate --timeout=300s
kubectl apply -k deploy/
kubectl rollout status deploy/indicat --timeout=300s
  1. Le minimum : lire le code, écrire des paquets. Les permissions par défaut du token sont plus larges ; les énoncer ici les restreint pour ce workflow.
  2. Un tag par commit. Le même SHA dans git log, dans le registre et dans kubectl get deploy -o wide : plus à deviner ce qui tourne.
  3. Cache de couches dans le service de cache de GitHub ; un build qui n'a changé que le code applicatif prend quelques secondes.
  4. Rattache le job à l'environnement production : ses secrets, ses règles de protection (étape suivante), et un historique de déploiements dans la barre latérale du dépôt.
  5. Le Job d'abord, seul, sélectionné par son label ; puis tout le reste. Une migration qui échoue arrête le pipeline avant qu'un pod change.
$git add deploy/ .github/ .gitlab-ci.yml 2>/dev/null; git commit -m "ci: deploy on push"
$git push origin main
Vérification
$gh run list --repo  --workflow deploy --limit 1 --json conclusion --jq '.[0].conclusion'
Retour attendu
success
Vérification
$[ "$(kubectl -n  get deploy indicat -o jsonpath='{.spec.template.spec.containers[0].image}' | cut -d: -f2)" = "$(git rev-parse HEAD)" ] && echo running HEAD
Retour attendu
running HEAD
Si le job deploy échoue

Le log du job contient l'erreur kubectl. Par ordre de probabilité :

  • Unable to connect to the server : :6443 n'est pas joignable depuis le runner (pare-feu, adresse privée).
  • forbidden : il manque au Role une ressource que deploy/ contient maintenant ; ajoute-la dans ci-rbac.yaml.
  • error: no objects passed to apply à l'étape du Job : le label app: indicat-migrate manque sur migrate.yaml.
  • Le Job de migrations échoue : kubectl logs job/indicat-migrate depuis ton portable ; le Deployment n'a pas été touché.
  • rollout status expire : ImagePullBackOff parce que le nom d'image dans deploy/ diffère de (kustomize n'a rien remplacé), ou les pods ratent leurs sondes (le dépannage de la page précédente s'applique).

Environnements et protection

Dépôt Settings → Environments → production : ajoute des Required reviewers, et restreins Deployment branches à main. Settings → Branches → Add rule sur main : exige une pull request et des status checks.

Rollback

Chaque commit a son image. Revenir en arrière, c'est déployer une plus ancienne : relance le pipeline du dernier bon commit, ou, depuis ton portable, annule le rollout.

$gh run list --repo --workflow deploy --limit 5
$gh run rerun <run-id> --repo
$kubectl -n rollout undo deploy/indicat

Terminé

Un push sur main construit :sha, migre, déploie, et s'arrête à la première erreur, avec une identité qui ne voit pas au-delà de . Ton kubeconfig admin reste sur ton portable.

Tout a fonctionné ?

Si tu as suivi cette page jusqu'au bout sur une vraie machine, dis-le. Ta validation est datée et enregistre ta stack : le prochain lecteur sur le même chemin sait que ça marche toujours.

Cette copie est en lecture seule. Pour dire que ça marche, ou que ça ne marche pas, ouvre une issue

Seuls tes choix de stack sont enregistrés, jamais tes valeurs. Le pseudo reste sur ce navigateur.