Runners GitLab CI managés
Ce guide explique comment configurer vos pipelines GitLab CI/CD pour fonctionner sur les runners Gitlab-CI managés de France Nuage, qui n'autorisent pas le mode privilégié pour des raisons de sécurité.
Objectif
Construire et déployer des images de conteneurs sans Docker-in-Docker (dind), en utilisant des outils compatibles avec un environnement Kubernetes non-privilégié.
Prérequis
- Un projet GitLab configuré pour utiliser les runners France Nuage
- Un registre de conteneurs (GitLab Container Registry, etc.)
- Des credentials de registre stockés dans les variables CI/CD
Les outils à utiliser
BuildKit Rootless - Construction d'images
BuildKit est le moteur de build de nouvelle génération de Docker. Le mode rootless permet de construire des images sans daemon Docker et sans privilèges root.
Quand l'utiliser : Pour toute construction d'image (équivalent à docker build)
Image officielle : moby/buildkit:rootless
Avantages par rapport à Kaniko :
- Meilleure performance et utilisation mémoire
- Cache plus efficace
- Support natif des builds multi-plateformes
- Activement maintenu
Crane - Opérations sur registre
Crane manipule les images dans les registres sans Docker.
Quand l'utiliser :
- Copier une image entre registres (équivalent à
docker tag+docker push) - Extraire des fichiers d'une image (
docker cp) - Authentification aux registres
Installation :
curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
Ce qu'il ne faut PAS faire
| Interdit | Pourquoi | Alternative recommandée |
|---|---|---|
services: [docker:dind] | Nécessite le mode privilégié | BuildKit rootless |
docker build | Pas de daemon Docker | BuildKit rootless |
docker push/pull | Pas de daemon Docker | Crane ou BuildKit |
docker run | Pas de daemon Docker | Image directe dans image: |
docker cp | Pas de daemon Docker | crane export |
image: docker:* | Inutile sans daemon | Image spécifique au job |
Variable DOCKER_HOST | Pas de daemon | Supprimer |
Variable DOCKER_TLS_CERTDIR | Pas de daemon | Supprimer |
pull_policy: [Always] | Non autorisé par le runner | Ne pas spécifier (utilise le défaut) |
Pourquoi pas de Docker-in-Docker (DinD) ?
Les runners Kubernetes France Nuage n'autorisent pas le mode privileged: true et donc le DinD pour 3 raisons :
- Sécurité : Le mode privilégié donne un accès root complet au nœud hôte, on oublie tout de suite :)
- Isolation : Les conteneurs privilégiés peuvent échapper à leur sandbox
- Multi-tenancy : Sur un cluster partagé, cela compromettrait l'isolation entre clients
Ce qu'il FAUT faire
1. Construction d'image avec BuildKit Rootless
build:
stage: build
image:
name: moby/buildkit:rootless
entrypoint: ['sh', '-c']
variables:
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true
Points clés :
entrypoint: ['sh', '-c']est obligatoire pour exécuter les commandes shellBUILDKITD_FLAGS: --oci-worker-no-process-sandboxdésactive le sandboxing (requis sans privilèges)buildctl-daemonless.shlance BuildKit sans daemon persistant- Utilisez des tags immutables (SHA) pour la traçabilité
2. Authentification aux registres
GitLab Container Registry :
before_script:
- mkdir -p ~/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n $CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD | base64)\"}}}" > ~/.docker/config.json
3. Cache de build
BuildKit supporte le cache distant pour accélérer les builds :
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--import-cache type=registry,ref=$CI_REGISTRY_IMAGE:cache
--export-cache type=inline
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true
4. Extraction de fichiers depuis une image
Pour exécuter des tests avec les dépendances de l'image construite et respecter le principe de construction immuable (immutable build) :
test:
image: node:20 # Si vous avez un projet en NodeJS
before_script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane export $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - | tar -xf - -C / usr/src/app
script:
- cd /usr/src/app
- npm test
Attention : L'image de base du job (image: node:20) doit avoir la même version de Node.js que celle du Dockerfile pour éviter les erreurs de modules natifs (NODE_MODULE_VERSION mismatch).
5. Copie entre registres
Pour déployer vers plusieurs régions :
deploy:
script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane copy $SOURCE_IMAGE $DEST_IMAGE
6. Build multi-plateforme
BuildKit excelle pour les builds multi-architectures :
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--opt platform=linux/amd64,linux/arm64
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true
Principe de build immutable
Nous vous conseillons de suivre le principe "build once, deploy everywhere" :
- Un seul build : Construisez l'image une fois dans le stage
build - Tag immutable : Utilisez
${CI_COMMIT_SHA}dans le tag - Réutilisation : Les jobs suivants extraient ou utilisent cette image
- Pas de rebuild : Ne reconstruisez jamais pour les tests ou le déploiement
build ──► tests (extraient l'app) ──► security scan ──► deploy (copie l'image)
│ │ │ │
└──────────────┴─────────────────────────┴────────────────────┘
Même image partout
Erreurs courantes
"Cannot connect to Docker daemon"
Cause : Vous utilisez une commande docker sans daemon.
Solution : Remplacez par BuildKit rootless (build) ou Crane (opérations registre).
"could not connect to buildkitd.sock"
Cause : BuildKit tente d'utiliser un socket Unix au lieu du mode daemonless.
Solution : Utilisez buildctl-daemonless.sh (pas buildctl) et vérifiez que BUILDKITD_FLAGS est défini.
"Unauthorized" lors de crane export
Cause : Crane n'est pas authentifié au registre.
Solution : Ajoutez crane auth login avant crane export.
"NODE_MODULE_VERSION mismatch"
Cause : L'image du job utilise une version Node.js différente de celle du Dockerfile.
Solution : Alignez image: avec la version du Dockerfile.
"exec format error"
Cause : Image construite pour une architecture différente.
Solution : Spécifiez --opt platform=linux/amd64 dans BuildKit.
"invalid pull policy for container"
Cause : Votre configuration GitLab CI (ou un template inclus) spécifie un pull_policy (ex: [Always]) qui n'est pas autorisé par le runner Kubernetes.
ERROR: Job failed: invalid pull policy for container "build":
pull_policy ([Always]) defined in GitLab pipeline config is not one of the allowed_pull_policies ([])
Solution : Ne spécifiez pas de pull_policy dans votre configuration. Le runner utilisera sa politique par défaut. Si vous incluez un template externe qui définit pull_policy, remplacez-le par une configuration inline :
# ❌ Évitez les templates qui définissent pull_policy
include:
- project: 'some-org/some-template'
file: '/template.gitlab-ci.yml'
# ✅ Préférez une configuration inline sans pull_policy
my-job:
stage: build
image: my-image:latest
# Ne pas spécifier pull_policy - le runner utilisera sa politique par défaut
script:
- echo "Build"
Exemple complet (app nodejs)
Voici un pipeline complet illustrant toutes les bonnes pratiques :
# .gitlab-ci.yml - Pipeline sans mode privilégié pour France Nuage
# Utilise BuildKit rootless pour la construction d'images
variables:
# Tag immutable basé sur le SHA du commit
BUILD_IMAGE: '${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHA}'
# Configuration BuildKit rootless
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
stages:
- build
- test
- security
- deploy
# ============================================
# STAGE: BUILD - Construction avec BuildKit
# ============================================
build:
stage: build
image:
name: moby/buildkit:rootless
entrypoint: ['sh', '-c']
before_script:
- mkdir -p ~/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n $CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD | base64)\"}}}" > ~/.docker/config.json
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--import-cache type=registry,ref=$CI_REGISTRY_IMAGE:cache
--export-cache type=inline
--output type=image,name=$BUILD_IMAGE,push=true
rules:
- when: on_success
# ============================================
# TEMPLATE: Extraction de l'app depuis l'image
# ============================================
.extract_app: &extract_app
# IMPORTANT: Doit correspondre à la version Node.js du Dockerfile
image: node:20
before_script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane export $BUILD_IMAGE - | tar -xf - -C / usr/src/app
# ============================================
# STAGE: TEST - Tests sur l'image construite
# ============================================
test:
<<: *extract_app
stage: test
coverage: '/Lines[^:]+\:\s+(\d+\.\d+)\%/'
script:
- cd /usr/src/app
- npm test
artifacts:
reports:
junit: /usr/src/app/coverage/junit.xml
paths:
- /usr/src/app/coverage/
rules:
- when: on_success
lint:
<<: *extract_app
stage: test
script:
- cd /usr/src/app
- npm run lint
rules:
- when: on_success
# ============================================
# STAGE: SECURITY - Scans de vulnérabilités
# ============================================
image-security:
stage: security
image: aquasec/trivy:0.56.2
allow_failure: true
script:
- trivy image
--exit-code 0
--format template
--template "@/contrib/gitlab.tpl"
-o gl-container-scanning-report.json
$BUILD_IMAGE
- trivy image --exit-code 1 --severity CRITICAL $BUILD_IMAGE
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
rules:
- when: on_success
dependency-scan:
stage: security
image: ghcr.io/google/osv-scanner:v1.9.1
allow_failure: true
before_script:
- apk add --no-cache curl
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane export $BUILD_IMAGE - | tar -xf - usr/src/app/package-lock.json --strip-components=3
script:
- /osv-scanner --lockfile package-lock.json
rules:
- when: on_success
# ============================================
# STAGE: DEPLOY - Déploiement en production
# ============================================
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
# Optionnel: copier vers un registre dédié
- crane copy $BUILD_IMAGE $PRODUCTION_REGISTRY/$PROJECT:$CI_COMMIT_SHA
# Déployer sur Kubernetes
- envsubst < deploy/deployment.yaml | kubectl apply -f -
- kubectl rollout status deployment/my-app
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Ressources
Prochaines étapes
Consultez les Concepts pour comprendre l'architecture France Nuage.