Skip to content

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: [""]
- Pas de Docker daemon nécessaire - Pas d'AWS CLI nécessaire - Authentification ECR native via variables AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY - Pas de mode privilégié requis


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 tag latest est retiré et le promote devient 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"
- L'image dĂ©ployĂ©e (:$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
Quand une MR est ouverte sur la branche, seul le pipeline MR tourne (plus de doublon branche + MR sur le mĂȘme commit).

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_FLAGS global (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 de latest) + curl durci (-f --retry) — 2026-06-15