GitHub Actions est devenu le standard pour la CI/CD. Ce guide couvre les patterns avancés : matrices de build, environnements réutilisables, secrets, déploiements multi-cloud et optimisation des coûts.
Concepts fondamentaux de GitHub Actions
GitHub Actions est un moteur d'automatisation intégré à GitHub. Il exécute des workflows définis en YAML, déclenchés par des événements du cycle de vie Git ou des planifications. Comprendre ses primitives est essentiel avant d'aller vers les patterns avancés.
- Workflow : fichier YAML dans
.github/workflows/, contient un ou plusieurs jobs. Déclenché par un événement (push,pull_request,schedule,workflow_dispatch,workflow_call) - Job : unité d'exécution qui tourne sur un runner. Les jobs sont parallèles par défaut ; utilisez
needspour les séquencer - Step : commande shell ou action réutilisable à l'intérieur d'un job. Les steps d'un même job partagent le filesystem
- Runner : machine qui exécute le job — GitHub-hosted (ubuntu-latest, windows-latest, macos-latest) ou self-hosted
- Action : step réutilisable empaqueté (
uses: actions/checkout@v4) — 20 000+ disponibles sur GitHub Marketplace - Context : variables d'environnement dynamiques exposées par GitHub —
github.sha,github.ref,github.event, etc.
Pipeline CI/CD complet Node.js : lint → test → build → ECR → ECS
Voici un pipeline de production complet pour une application Node.js déployée sur ECS Fargate. Il illustre les patterns essentiels : cache npm, tests parallèles, build Docker avec cache de couches, OIDC AWS et déploiement avec rollback automatique.
# .github/workflows/ci-cd.yml
name: CI/CD Production
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: eu-west-1
ECR_REPOSITORY: mon-app
ECS_CLUSTER: production
ECS_SERVICE: mon-app-service
CONTAINER_NAME: mon-app
permissions:
id-token: write # Requis pour OIDC
contents: read
pull-requests: write # Pour commenter les PRs
jobs:
# ─── Lint et vérifications statiques ───────────────────────
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
# ─── Tests unitaires et d'intégration ──────────────────────
test:
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
# ─── Build et push Docker vers ECR ─────────────────────────
build:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
outputs:
image: ${{ steps.build-push.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push to ECR
id: build-push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NODE_ENV=production
BUILD_SHA=${{ github.sha }}
# ─── Déploiement ECS ────────────────────────────────────────
deploy:
runs-on: ubuntu-latest
needs: build
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Update ECS service
run: |
aws ecs update-service --cluster ${{ env.ECS_CLUSTER }} --service ${{ env.ECS_SERVICE }} --force-new-deployment
- name: Wait for deployment to complete
run: |
aws ecs wait services-stable --cluster ${{ env.ECS_CLUSTER }} --services ${{ env.ECS_SERVICE }}
echo "Deployment complete: ${{ github.sha }}"
Workflows réutilisables et composite actions : principe DRY
Les workflows réutilisables (workflow_call) permettent de centraliser la logique CI/CD partagée entre plusieurs dépôts. C'est l'équivalent des fonctions réutilisables en programmation — indispensable pour les organisations avec de nombreux projets.
# .github/workflows/reusable-deploy-ecs.yml
# Workflow réutilisable — peut être appelé depuis n'importe quel dépôt de l'org
name: Deploy to ECS (Reusable)
on:
workflow_call:
inputs:
environment:
required: true
type: string
description: "staging ou production"
image-tag:
required: true
type: string
ecs-cluster:
required: true
type: string
ecs-service:
required: true
type: string
aws-region:
required: false
type: string
default: "eu-west-1"
secrets:
AWS_ROLE_ARN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ inputs.aws-region }}
- name: Deploy to ECS
run: |
aws ecs update-service --cluster ${{ inputs.ecs-cluster }} --service ${{ inputs.ecs-service }} --force-new-deployment
aws ecs wait services-stable --cluster ${{ inputs.ecs-cluster }} --services ${{ inputs.ecs-service }}
---
# Dans un autre dépôt : appel du workflow réutilisable
# .github/workflows/deploy.yml
jobs:
deploy-staging:
uses: mon-org/devops-workflows/.github/workflows/reusable-deploy-ecs.yml@main
with:
environment: staging
image-tag: ${{ github.sha }}
ecs-cluster: staging-cluster
ecs-service: mon-app-staging
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_STAGING_ROLE_ARN }}
deploy-production:
needs: deploy-staging
uses: mon-org/devops-workflows/.github/workflows/reusable-deploy-ecs.yml@main
with:
environment: production
image-tag: ${{ github.sha }}
ecs-cluster: prod-cluster
ecs-service: mon-app-prod
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_PROD_ROLE_ARN }}
Matrices de build : tests parallèles multi-versions
Les matrices de build permettent de tester en parallèle sur plusieurs versions de Node.js, plusieurs systèmes d'exploitation, ou plusieurs configurations. Elles réduisent drastiquement le temps total de CI.
jobs:
test-matrix:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
include:
# Ajouter une configuration spéciale pour Node 22 sur macOS
- node: 22
os: macos-latest
experimental: true
exclude:
# Node 18 ne supporte pas certaines features Windows
- node: 18
os: windows-latest
fail-fast: false # Continuer même si une combinaison échoue
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm test
- name: Test résultats
if: always() # Toujours uploader, même en cas d'échec
uses: actions/upload-artifact@v4
with:
name: test-results-node${{ matrix.node }}-${{ matrix.os }}
path: test-results/
Stratégies de cache : optimiser le temps de CI
Le cache est le levier le plus impactant pour réduire la durée des pipelines. Un pipeline Node.js sans cache peut prendre 4 à 6 minutes ; avec un cache bien configuré, 45 secondes à 1 minute 30.
# Cache node_modules — économise 2-3 min par run
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Cache Docker layers avec GitHub Actions Cache Backend
# (plus efficace que Docker Hub pour les grands projets)
- uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:latest
- uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max # mode=max cache toutes les couches intermédiaires
# Cache Next.js build
- uses: actions/cache@v4
with:
path: |
~/.npm
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
${{ runner.os }}-nextjs-
Environnements et protection rules : gouvernance des déploiements
Les environnements GitHub Actions permettent d'isoler les secrets par cible de déploiement et d'imposer des règles d'approbation avant tout déploiement en production. C'est la gate de gouvernance indispensable pour les équipes avec des contraintes de change management.
# Configuration dans GitHub UI : Settings → Environments → production
# Protection rules :
# [ ] Required reviewers : 2 membres de l'équipe SRE
# [ ] Wait timer : 5 minutes (fenêtre d'annulation)
# [ ] Deployment branches : main uniquement
jobs:
deploy-staging:
environment:
name: staging
url: https://staging.mon-app.com
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh staging
deploy-production:
needs: [deploy-staging, integration-tests]
environment:
name: production
url: https://mon-app.com
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh production
# Ce job attend l'approbation des reviewers configurés
# avant d'exécuter les steps
OIDC avec AWS : zéro secret stocké dans GitHub
L'authentification OIDC élimine le besoin de stocker des credentials AWS (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) dans les secrets GitHub. GitHub émet un token JWT signé que AWS valide directement via STS. C'est la méthode la plus sécurisée et celle que nous recommandons systématiquement.
# 1. Terraform : créer le provider OIDC et le rôle IAM
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
resource "aws_iam_role" "github_actions_deploy" {
name = "GitHubActionsDeployRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.github_actions.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
# Restriction au dépôt et à la branche main uniquement
"token.actions.githubusercontent.com:sub" = "repo:mon-org/mon-repo:ref:refs/heads/main"
}
}
}]
})
}
# 2. Workflow : utilisation OIDC (aucun secret AWS stocké)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsDeployRole
aws-region: eu-west-1
role-session-name: GitHubActions-${{ github.run_id }}
# Pas de aws-access-key-id ni aws-secret-access-key !
Self-hosted runners sur ECS Fargate : ephémères et rentables
Les runners GitHub-hosted coûtent 0,008 $/min. Un pipeline de 10 minutes coûte 0,08 $ — soit 80 $ pour 1 000 runs. Avec des runners self-hosted sur ECS Fargate Spot, le coût tombe à ~0,001 $/min. Pour les organisations avec un volume élevé, l'économie est significative. L'autre avantage : les runners tournent dans votre VPC et accèdent à vos ressources privées sans exposition publique.
# Architecture : Actions Runner Controller (ARC) sur EKS
# Les runners sont des pods Kubernetes éphémères — scale to zero hors charge
# runner-deployment.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: m2c-runners
namespace: github-runners
spec:
replicas: 0 # Scale à zéro quand aucun job en attente
template:
spec:
repository: mon-org/mon-repo
image: summerwind/actions-runner:latest
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
serviceAccountName: github-runner-sa
nodeSelector:
node-role: runners
---
# HorizontalRunnerAutoscaler : scale en fonction de la file de jobs
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
name: m2c-runners-autoscaler
spec:
scaleTargetRef:
name: m2c-runners
minReplicas: 0
maxReplicas: 10
metrics:
- type: TotalNumberOfQueuedAndInProgressWorkflowRuns
repositoryNames:
- mon-org/mon-repo
Gestion des secrets : GitHub Secrets vs AWS Secrets Manager
Les secrets GitHub sont adaptés pour les petites équipes et les secrets statiques. Pour des équipes plus grandes avec rotation des secrets, des secrets partagés entre environnements ou des secrets dynamiques, AWS Secrets Manager (accessible via OIDC) est préférable :
# Pattern recommandé : fetch des secrets AWS au runtime via OIDC
# Aucun secret sensible stocké dans GitHub
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-west-1
- name: Fetch secrets from AWS Secrets Manager
run: |
DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id prod/app/db-password --query SecretString --output text)
API_KEY=$(aws secretsmanager get-secret-value --secret-id prod/app/api-key --query SecretString --output text)
echo "DB_PASSWORD=$DB_PASSWORD" >> $GITHUB_ENV
echo "::add-mask::$DB_PASSWORD" # Masquer dans les logs
echo "::add-mask::$API_KEY"
# Alternative : utiliser l'action officielle
- uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
secret-ids: |
prod/app/db-password
prod/app/api-key
parse-json-secrets: true
Optimisation des workflows : path filters et concurrency groups
Pour les monorepos ou les projets avec plusieurs composants indépendants, déclencher tous les tests à chaque commit est inefficace. Les path filters et concurrency groups permettent d'optimiser radicalement le temps de CI.
# Path filters : ne builder que ce qui a changé
on:
push:
paths:
- 'backend/**' # Déclencher seulement si le backend a changé
- '!docs/**' # Ignorer les changements de doc
- '!*.md'
# Concurrency groups : annuler les runs précédents sur la même branche
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Annule le run précédent si nouvelle PR pushée
# Pour la branche main, ne pas annuler (laisser les déploiements se terminer)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# Conditions avancées : différencier PR vs push main
jobs:
deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# Ce job ne tourne que sur push main, pas sur les PRs
Monitoring et coûts : GitHub Actions dashboard et Datadog CI
GitHub Actions fournit un dashboard natif (Actions tab → tous les workflows, durées, taux de succès). Pour une observabilité avancée des pipelines CI, Datadog CI propose des métriques détaillées : durée par step, flaky tests, coût par pipeline.
# Intégration Datadog CI dans GitHub Actions
- name: Install Datadog CI
run: npm install -g @datadog/datadog-ci
- name: Run tests with Datadog tracing
run: npm test
env:
DD_CIVISIBILITY_AGENTLESS_ENABLED: true
DD_API_KEY: ${{ secrets.DD_API_KEY }}
DD_SITE: datadoghq.eu
# Coûts GitHub-hosted runners (2025) :
# ubuntu-latest (2 vCPU) : $0.008/min
# ubuntu-latest-4-cores : $0.016/min
# ubuntu-latest-8-cores : $0.032/min
# windows-latest : $0.016/min
# macos-latest : $0.08/min
# Pour 1 000 runs de 10 min sur ubuntu :
# GitHub-hosted : 1000 × 10 × $0.008 = $80/mois
# Self-hosted Fargate Spot : ~$8/mois (économie 90%)
Sécurisation des workflows
Les workflows GitHub Actions peuvent être vecteurs d'attaque si mal configurés. Les bonnes pratiques essentielles :
- Épingler les actions par commit SHA :
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683plutôt que@v4— empêche les supply chain attacks via tag hijacking - Permissions minimales : déclarer
permissions: read-allau niveau workflow et accorder uniquement ce dont chaque job a besoin - PRs depuis des forks : les workflows déclenchés par
pull_requestdepuis un fork n'ont pas accès aux secrets — c'est une protection intentionnelle à ne pas contourner - CODEOWNERS sur les workflows : ajouter
.github/workflows/ @team-devopsdans CODEOWNERS pour que toute modification de workflow passe en revue
Conclusion
GitHub Actions couvre le spectre complet de la CI/CD, de la simple automatisation de tests à des déploiements multi-environnements complexes avec gouvernance. Les workflows réutilisables éliminent la duplication entre projets, les matrices de build parallélisent les tests sur toutes les configurations cibles, et l'intégration OIDC avec AWS supprime la dernière raison de stocker des credentials cloud dans GitHub. Pour les organisations avec un volume élevé de builds, les runners self-hosted sur ECS Fargate Spot réduisent les coûts de CI de 80 à 90 %. GitHub Actions est aujourd'hui suffisamment mature pour remplacer Jenkins dans la quasi-totalité des cas d'usage — avec une courbe d'apprentissage infiniment plus courte et une intégration Git native que Jenkins ne peut pas égaler.
