ADR 005 â GitLab CI Build Pipeline â Lessons Apprises
Contexte
Sprint 3 â Ajout du stage build pour pusher les images Docker vers ECR AWS.
Décisions et problÚmes rencontrés
1. Docker-in-Docker abandonnĂ© â Kaniko â
ProblĂšme : Docker-in-Docker (docker:24-dind) + AWS CLI sur Alpine = incompatible. AWS CLI v2 nĂ©cessite glibc mais Alpine utilise musl libc. MĂȘme avec gcompat, Python 3.14 bundlĂ© dans AWS CLI v2 ne fonctionne pas.
Erreurs rencontrées :
Failed to load Python shared library libpython3.14.so.1.0:
dladdr1: symbol not found
Solution : Kaniko
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
2. Variables GitLab Protected â 401 ECR
ProblĂšme : Variables AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY marquĂ©es "Protected". Les branches feature/* ne sont pas protĂ©gĂ©es â variables non injectĂ©es â 401 Unauthorized.
Solution : Décocher "Protected", garder "Masked" uniquement.
Protected : â (bloque les pipelines feature/*)
Masked : â
(cache la valeur dans les logs)
RÚgle : Protected = uniquement pour les secrets de production (prod branch). Masked = suffisant pour la sécurité des logs CI.
3. Kaniko cache â vieilles couches servies
ProblĂšme : Avec --cache=true, Kaniko servait d'anciennes couches pip mĂȘme aprĂšs mise Ă jour de requirements.txt. Trivy dĂ©tectait les anciennes versions des packages.
Solution : Utiliser --cache=false pour forcer le rebuild complet.
--cache=false
Note : --no-cache n'existe pas dans Kaniko (flag Docker uniquement).
4. Trivy image scan â CVEs packages systĂšme Debian
ProblĂšme : python:3.10-slim (Debian 13.4) contient wheel et jaraco.context installĂ©s via apt dans /usr/lib/python3/dist-packages/. Trivy dĂ©tecte ces versions systĂšme mĂȘme quand pip installe des versions plus rĂ©centes dans /usr/local/lib/.
CVEs dĂ©tectĂ©s : - 14 CVEs OS Debian â Status "affected" (pas de fix disponible) - CVE-2026-23949 : jaraco.context â path traversal - CVE-2026-24049 : wheel â privilege escalation
Justification exclusion .trivyignore : Ces packages sont des outils de build pip, jamais utilisés à runtime par FastAPI. Les CVEs concernent le traitement de wheel files et tar archives malveillants que notre serveur web ne traite jamais.
Solution long terme : Multi-stage build + migration Python 3.12 (Sprint 4)
FROM python:3.12-slim AS builder
# installer toutes les dépendances
FROM python:3.12-slim AS runtime
# copier uniquement les packages nécessaires
# zĂ©ro outils de build â zĂ©ro CVE wheel/jaraco
5. Pattern Candidate / Scan / Promote â
Architecture finale adoptée :
build(candidate) â scan-image â promote(SHA+latest)
- build-candidate : image poussée avec tag SHA-candidate
- trivy-image-scan : scan depuis ECR avant promotion
- promote-image : si scan OK â tag SHA dĂ©finitif + latest
RÚgle DevSecOps : Jamais pousser une image non scannée vers le tag latest/prod.
Mise à jour (#65, INC-050) : ce pattern a évolué avec le passage du repo ECR en
IMMUTABLE. Le taglatestest retiré et lepromotedevient un retag par digest (plus de rebuild). Voir section 6.
6. ECR IMMUTABLE â promote par digest (#65, INC-050) â
ProblĂšme :
Le passage du repo ECR en image_tag_mutability = IMMUTABLE (#64, durcissement tfsec) casse le pattern de la section 5 :
- :latest est mutable par nature â put refusĂ© Ă chaque run aprĂšs le premier
- :$SHA-candidate / :$SHA re-poussĂ©s Ă l'identique au re-run d'un commit â TAG_INVALID
- promote-image rebuildait l'image (Kaniko) au lieu de la retaguer â l'image promue n'Ă©tait pas exactement l'image scannĂ©e par Trivy
Solution : vrai promote = retag par digest
# build-candidate : tag unique par pipeline (jamais de collision en immutable)
CANDIDATE_TAG: candidate-$CI_COMMIT_SHA-$CI_PIPELINE_ID
# promote-image (aws-cli, plus de rebuild Kaniko)
MANIFEST=$(aws ecr batch-get-image --repository-name "$ECR_REPOSITORY" \
--image-ids imageTag="$CANDIDATE_TAG" --query 'images[0].imageManifest' --output text)
aws ecr put-image --repository-name "$ECR_REPOSITORY" \
--image-tag "$CI_COMMIT_SHA" --image-manifest "$MANIFEST"
:$SHA) pointe sur le digest exact scannĂ© par Trivy â scan == deploy
- :latest supprimé (non consommé par le deploy, incompatible immutable)
- Guard d'idempotence : si :$SHA existe déjà , le job skip (re-run d'un commit déjà promu)
- Lifecycle ECR : les tags candidate-* expirent Ă 3 jours (artefacts jetables)
- Aucune permission IAM ajoutée : ecr:BatchGetImage + ecr:PutImage déjà couverts par ecr-push
RÚgle DevSecOps : Sur un repo immutable, le promote est un retag par digest, jamais un rebuild. Un rebuild au moment du promote peut produire une image différente de celle qui a passé le scan de sécurité.
7. main = branche d'archive : la chaĂźne image ne tourne plus sur main (#57) â
ProblĂšme :
AprĂšs le passage au workflow develop â main (release de fin de sprint), deux dĂ©fauts sont apparus sur main :
- build-candidate Ă©chouait au merge develop â main (variables indisponibles pendant la fenĂȘtre de dĂ©protection de la branche protĂ©gĂ©e), et rebuilder une image dĂ©jĂ buildĂ©e + promue sur develop est de toute façon redondant.
- Un commit sur develop avec une MR ouverte (develop â main) dĂ©clenchait deux pipelines en parallĂšle sur le mĂȘme commit (pipeline de branche + pipeline MR).
Solution :
- main devient une branche d'archive de release : seuls test + security y tournent. La chaßne image (build-candidate, trivy-image-scan, promote-image) et le deploy sont retirés de main. L'image est buildée, scannée et promue par digest une seule fois, depuis develop.
- RĂšgle anti-doublon dans workflow:rules :
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never
RĂšgle :
Le dĂ©ploiement se fait depuis develop (cluster Ă©phĂ©mĂšre mono-env). main ne rejoue pas la chaĂźne image : promouvoir deux fois la mĂȘme image est inutile et fragile. Le multi-env (deploy prod depuis main) est repoussĂ© au Sprint 5.
8. ReproductibilitĂ© CI : clean flags global + images Ă©pinglĂ©es par digest (#57) â
ProblĂšme :
- Les jobs statiques (tfsec, kube-linter) pouvaient relire des fichiers laissés par un job précédent sur le runner self-hosted (INC-049) ; seul tfsec portait le garde-fou.
- Les images des jobs Ă©taient rĂ©fĂ©rencĂ©es par tag mobile (:latest, :debug, :3.12-slim...). Un tag peut pointer sur un contenu diffĂ©rent d'un run Ă l'autre â build non reproductible, rĂ©gression silencieuse possible (classe INC-044).
Solution :
- GIT_CLEAN_FLAGS: -ffdx remonté en variable globale : tous les jobs partent d'un arbre vraiment propre (-x purge aussi les fichiers gitignorés). Le garde-fou local de tfsec devient redondant et est retiré.
- Toutes les images de jobs épinglées par digest @sha256:... (python, postgres, trivy, tfsec, alpine, kaniko, aws-cli). L'image exécutée est exactement celle qui a été vérifiée.
Maintenance :
Les digests des tags mobiles (:latest) doivent ĂȘtre bumpĂ©s pĂ©riodiquement pour rĂ©cupĂ©rer les correctifs (image runner, CLI). Bump = re-inspect du tag et mise Ă jour du @sha256. Les images utilisĂ©es par plusieurs jobs (Trivy, aws-cli) sont dĂ©finies via une ancre YAML unique (.trivy_image, .awscli_image en tĂȘte de .gitlab-ci.yml, #57) : le bump se fait Ă un seul endroit. â ïž Pinner l'image Trivy / Tfsec ne fige pas leur base de vulnĂ©rabilitĂ©s : elle est tĂ©lĂ©chargĂ©e au runtime, pas embarquĂ©e dans l'image.
Binaires téléchargés au runtime (#92, INC-057) : un outil récupéré via curl au runtime est aussi une dépendance non pinnée. kube-linter (téléchargé dans le job kube-linter-scan car l'image officielle est distroless) est désormais épinglé par version (KUBE_LINTER_VERSION: v0.8.3, plus de releases/latest/) et le curl est durci (-fsSL --retry 3 --retry-delay 2 --retry-connrefused). Le -f est essentiel : sans lui, une réponse HTTP d'erreur transitoire est sauvée comme corps (HTML) et casse l'étape tar avec un message trompeur (INC-057).
Pipeline infra (#78) : le mĂȘme traitement est Ă©tendu Ă .gitlab-ci-infra.yml. Les images aws-cli / terraform:1.15 / ansible sont Ă©pinglĂ©es par digest via des ancres (.awscli_image / .terraform_image / .ansible_image, aws-cli alignĂ© sur le digest du pipeline app pour la cohĂ©rence). Les before_script de setup, jusqu'ici dupliquĂ©s, sont factorisĂ©s en .terraform_setup / .ansible_setup rĂ©utilisĂ©s via !reference (les jobs ajoutent ensuite leurs lignes spĂ©cifiques). Refacto Ă comportement strictement identique : l'Ă©quivalence des 5 jobs (image, before_script aplati, script, rules, variables) a Ă©tĂ© vĂ©rifiĂ©e sur le merged_yaml retournĂ© par l'API lint GitLab, avant/aprĂšs. La logique d'apply/teardown n'est pas touchĂ©e (elle vit dans ansible/*.yml + aws-stop.sh), donc aucun risque de rĂ©gression INC-016 sur le teardown.
9. SBOM CycloneDX (#68) â
Quoi : le job trivy-image-scan génÚre, en plus du scan, un SBOM CycloneDX de l'image candidate (trivy image --format cyclonedx --output gl-sbom.cdx.json), exposé en artifact (artifacts:reports:cyclonedx + paths).
Pourquoi l'intĂ©grer au scan plutĂŽt qu'un job dĂ©diĂ© : l'image candidate est dĂ©jĂ pull et le cache de la base Trivy est dĂ©jĂ chaud â une invocation Trivy de plus, aucun job supplĂ©mentaire. Le SBOM porte exactement sur l'image scannĂ©e (= image promue par digest), donc inventaire et scan sont cohĂ©rents par construction.
Inventaire, pas un gate : la génération SBOM n'a ni --exit-code ni --severity (elle ne doit jamais faire échouer le pipeline). C'est une photo des composants (OS + libs), pas un contrÎle.
â ïž Limite tier Free : la Dependency List UI de GitLab qui consomme reports:cyclonedx est Ultimate-only. En Free, la valeur est l'artifact SBOM lui-mĂȘme (tĂ©lĂ©chargeable, conservable, signable, et base d'un futur scan VEX / vuln management continu). Le reports:cyclonedx est dĂ©jĂ cĂąblĂ© pour le jour oĂč le tier change.
Pipeline finale
Stages :
test â security â build â scan-image â promote
Sur feature/* + MR :
test â
security â
build â
scan-image â
promote âïž (skippĂ©)
Sur develop :
test â
security â
build â
scan-image â
promote â
(+ deploy manuel si DEPLOY=true)
Sur main (archive de release, #57) :
test â
security â
build âïž scan-image âïž promote âïž deploy âïž
Variables GitLab CI requises
| Variable | Protected | Masked | Description |
|---|---|---|---|
| SECRET_KEY | â | â | JWT secret |
| AWS_ACCESS_KEY_ID | â | â | IAM GitLab CI |
| AWS_SECRET_ACCESS_KEY | â | â | IAM GitLab CI |
| AWS_DEFAULT_REGION | â | â | eu-west-3 |
| ECR_REGISTRY | â | â | 199167114788.dkr.ecr.eu-west-3.amazonaws.com |
| ECR_REPOSITORY | â | â | fastapi-eks/fastapi |
Références
- Ticket : #23 ci: add build stage
- Date : 2026-05-11
- Kaniko version : v1.24.0-debug (épinglé par digest, #57)
- Trivy version : 0.70.0
- Mise Ă jour : #65 (INC-050) promote par digest, ECR IMMUTABLE â 2026-06-03
- Mise Ă jour : #57 main = branche d'archive (chaĂźne image hors main) + anti-doublon
workflow:rulesâ 2026-06-11 - Mise Ă jour : #57
GIT_CLEAN_FLAGSglobal (INC-049) + images CI Ă©pinglĂ©es par digest (INC-044) â 2026-06-11 - Mise Ă jour : #57 DRY
.gitlab-ci.yml(ancres.trivy_image/.awscli_image/.rules_mr_develop) â 2026-06-11 - Mise Ă jour : #78 pinning digest + DRY (
!reference) du pipeline infra.gitlab-ci-infra.ymlâ 2026-06-14 - Mise Ă jour : #92 (INC-057) kube-linter Ă©pinglĂ© par version (
v0.8.3, plus delatest) +curldurci (-f --retry) â 2026-06-15