Cloud & Déploiement angularforall.com

- Déployer Angular sur AWS S3 et CloudFront avec CI/CD

Angular Aws S3 Cloudfront Ci-Cd Github-Actions Deploiement Cdn Cloud Devops Iam Https Cache Spa
Déployer Angular sur AWS S3 et CloudFront avec CI/CD

Déployez votre application Angular sur AWS S3 et CloudFront avec un pipeline CI/CD complet : configuration HTTPS, cache CDN et invalidation.

Architecture S3 + CloudFront pour Angular

Une application Angular compilée avec ng build --configuration production est un ensemble de fichiers statiques : index.html, bundles JavaScript hashés (main-X.js), feuilles de style et assets. Aucun runtime Node.js n'est requis pour la servir. AWS propose deux briques natives pour ce besoin : S3 comme stockage objet à haute disponibilité, et CloudFront comme CDN global avec terminaison HTTPS gratuite.

Schéma de l'infrastructure cible

┌──────────────┐      ┌──────────────────┐      ┌─────────────────┐
│   Browser    │ ───▶ │  CloudFront CDN  │ ───▶ │   S3 Bucket     │
│ (utilisateur)│ HTTPS│  450+ POPs        │ OAC  │  (origine)      │
└──────────────┘      │  Cache index/JS   │      │  Bloque public  │
                      │  HTTPS auto       │      └─────────────────┘
                      │  HTTP/3 + Brotli  │
                      └──────────┬───────┘
                                 │
                                 │ Logs
                                 ▼
                      ┌──────────────────┐
                      │  CloudWatch /    │
                      │  S3 Access Logs  │
                      └──────────────────┘

CI/CD : GitHub Actions → ng build → aws s3 sync → invalidation CloudFront

Pourquoi cette stack pour une SPA Angular ?

Critère S3 + CloudFront EC2 / Node + Nginx
Coût mensuel (50k visites/2 Mo) ~5 € ~25 € (t3.small + ALB)
Maintenance OS/patch Aucune Mensuelle
Scalabilité Automatique (illimitée) Auto Scaling à configurer
Latence mondiale 20-80 ms (POPs locaux) 100-300 ms selon région
HTTPS + certificat ACM gratuit ACM ou Let's Encrypt manuel
Coût free tier la 1ère année : 5 Go de stockage S3, 20 000 GET / 2 000 PUT par mois, et 1 To de transfert sortant CloudFront. Suffisant pour un site Angular typique en dessous de 100k visites mensuelles. Au-delà du free tier, comptez 0,085 $/Go en Europe et 0,02 $/10 000 requêtes HTTPS.

Prérequis et IAM least-privilege

Avant de commencer, créez un compte AWS, installez la CLI, et préparez un utilisateur IAM dédié au déploiement avec les droits minimaux nécessaires. Ne jamais utiliser le compte root ni l'access key d'un utilisateur admin pour le pipeline.

Outils à installer en local

# Installer AWS CLI v2 (Linux/macOS)
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

# Vérifier
aws --version
# aws-cli/2.15.x Python/3.11.x

# Installer Angular CLI 18+
npm install -g @angular/cli@latest
ng version
# Angular CLI: 18.2.x | Node: 20.x.x

# Configurer un profil AWS local pour les tests
aws configure --profile angularforall
# AWS Access Key ID:     AKIAXXXXXXXXXXXX
# AWS Secret Access Key: wJalrXX...
# Default region:        eu-west-3
# Default output:        json

Politique IAM least-privilege pour le pipeline

L'utilisateur IAM dédié au CI/CD doit pouvoir uniquement uploader sur un bucket précis et invalider une distribution précise. Aucun droit IAM, EC2 ou facturation. Voici la policy JSON à attacher.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DeployListBucket",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::angularforall-prod"
    },
    {
      "Sid": "DeployUploadObjects",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl",
        "s3:DeleteObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::angularforall-prod/*"
    },
    {
      "Sid": "DeployInvalidateCloudFront",
      "Effect": "Allow",
      "Action": [
        "cloudfront:CreateInvalidation",
        "cloudfront:GetInvalidation",
        "cloudfront:ListInvalidations"
      ],
      "Resource": "arn:aws:cloudfront::123456789012:distribution/E1A2B3C4D5E6F7"
    }
  ]
}

Créer l'utilisateur via la CLI

# Créer l'utilisateur (sans console password)
aws iam create-user --user-name github-actions-angularforall

# Sauvegarder la policy dans un fichier puis l'attacher
aws iam create-policy \
  --policy-name AngularForAllDeployPolicy \
  --policy-document file://deploy-policy.json

aws iam attach-user-policy \
  --user-name github-actions-angularforall \
  --policy-arn arn:aws:iam::123456789012:policy/AngularForAllDeployPolicy

# Générer access key + secret (à copier dans GitHub Secrets)
aws iam create-access-key --user-name github-actions-angularforall
Sécurité — éviter les access keys long-lived : Pour un projet sérieux, préférez OIDC GitHub Actions ↔ IAM Role. GitHub émet un token JWT que AWS échange contre des credentials temporaires (15 min). Aucune secret static à stocker, rotation automatique. Couvert plus loin dans la section CI/CD.
Le bucket name (angularforall-prod) doit être globalement unique sur AWS. Si déjà pris, ajoutez un suffixe : angularforall-prod-eu3 ou un identifiant aléatoire.

Build Angular en mode production

Une SPA Angular livrée en production doit être optimisée : tree-shaking activé, AOT compilation, source maps désactivées (ou cachées), et hash dans les noms de fichiers pour permettre un cache long terme.

environment.prod.ts — variables propres au prod

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiBaseUrl: 'https://api.angularforall.com/v1',
  cdnBaseUrl: 'https://cdn.angularforall.com',
  sentryDsn:  'https://abc123@sentry.io/4567',
  // Feature flags pilotés depuis le build (pas le runtime)
  features: {
    chatbot:   true,
    analytics: true,
    debugPane: false,
  },
  // Version injectée par le pipeline (process.env.GITHUB_SHA tronqué)
  buildVersion: '__BUILD_VERSION__',
};

angular.json — configuration production stricte

{
  "projects": {
    "angularforall": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "outputPath":           "dist/angularforall",
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with":    "src/environments/environment.prod.ts"
                }
              ],
              "optimization":         true,
              "outputHashing":        "all",
              "sourceMap":            false,
              "namedChunks":          false,
              "extractLicenses":      true,
              "vendorChunk":          false,
              "buildOptimizer":       true,
              "budgets": [
                { "type": "initial",      "maximumWarning": "500kb", "maximumError": "1mb"   },
                { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" }
              ],
              "serviceWorker":        "ngsw-config.json"
            }
          }
        }
      }
    }
  }
}

Activer le service worker pour le mode offline

# Ajouter le PWA package (génère ngsw-config.json + ngsw-worker.js)
ng add @angular/pwa --project=angularforall
// ngsw-config.json — stratégie de cache du service worker
{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index":   "/index.html",
  "assetGroups": [
    {
      "name":         "app",
      "installMode":  "prefetch",
      "resources": {
        "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
      }
    },
    {
      "name":         "assets",
      "installMode":  "lazy",
      "updateMode":   "prefetch",
      "resources": {
        "files": ["/assets/**", "/*.(svg|cur|jpg|jpeg|png|webp|gif|otf|ttf|woff|woff2)"]
      }
    }
  ],
  "dataGroups": [
    {
      "name":  "api-articles",
      "urls":  ["/api/articles/**"],
      "cacheConfig": {
        "maxSize":     200,
        "maxAge":      "1d",
        "timeout":     "5s",
        "strategy":    "freshness"
      }
    }
  ]
}

Commande de build et vérification locale

# Build production complet
ng build --configuration production

# Sortie attendue dans dist/angularforall/browser/
# ├── index.html
# ├── main-A1B2C3D4.js          (hash → cache 1 an)
# ├── polyfills-E5F6.js
# ├── styles-G7H8.css
# ├── ngsw-worker.js
# ├── ngsw.json
# └── assets/

# Tester en local exactement comme en prod (sans live-reload)
npx http-server dist/angularforall/browser -p 4200 -c-1

# Analyser la taille des bundles
npm install -D source-map-explorer
npx source-map-explorer dist/angularforall/browser/main-*.js
Depuis Angular 17, le dossier de sortie est dist/<projet>/browser/ par défaut (préparation pour SSR). C'est ce sous-dossier que vous synchroniserez vers S3, pas le parent.

Création et configuration du bucket S3

Le bucket S3 doit être privé (pas d'accès public direct), accessible uniquement via CloudFront grâce à l'Origin Access Control (OAC). Cela évite que le bucket soit listé publiquement et que les utilisateurs contournent le CDN.

Création du bucket via la CLI

# Région Paris (eu-west-3) — privilégier la région la plus proche des utilisateurs
aws s3api create-bucket \
  --bucket angularforall-prod \
  --region eu-west-3 \
  --create-bucket-configuration LocationConstraint=eu-west-3 \
  --profile angularforall

# Bloquer TOUT accès public (best practice)
aws s3api put-public-access-block \
  --bucket angularforall-prod \
  --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# Activer le versioning (rollback rapide en cas de mauvais déploiement)
aws s3api put-bucket-versioning \
  --bucket angularforall-prod \
  --versioning-configuration Status=Enabled

# Activer le chiffrement at-rest (AES-256, gratuit)
aws s3api put-bucket-encryption \
  --bucket angularforall-prod \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      }
    }]
  }'

Politique de cycle de vie pour limiter les coûts

{
  "Rules": [
    {
      "ID":     "CleanOldVersions",
      "Status": "Enabled",
      "Filter": { "Prefix": "" },
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 30
      },
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      }
    }
  ]
}
# Appliquer le lifecycle (sauvegarder en lifecycle.json)
aws s3api put-bucket-lifecycle-configuration \
  --bucket angularforall-prod \
  --lifecycle-configuration file://lifecycle.json

Bucket policy autorisant uniquement CloudFront

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid":       "AllowCloudFrontServicePrincipalReadOnly",
      "Effect":    "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action":    "s3:GetObject",
      "Resource":  "arn:aws:s3:::angularforall-prod/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1A2B3C4D5E6F7"
        }
      }
    }
  ]
}

Premier upload de test

# Synchroniser le dossier build vers S3
# --delete : supprime sur S3 les fichiers absents en local (clean deploy)
aws s3 sync dist/angularforall/browser/ s3://angularforall-prod/ \
  --delete \
  --profile angularforall

# Vérifier le contenu uploadé
aws s3 ls s3://angularforall-prod/ --recursive --human-readable --summarize

# Lister les versions (utile pour rollback)
aws s3api list-object-versions --bucket angularforall-prod --max-keys 10
Coût S3 maîtrisé : Pour une SPA Angular de 5 Mo après build, les frais de stockage sont d'environ 0,12 $/an. Les coûts viennent quasi exclusivement du transfert sortant via CloudFront, pas de S3 directement (puisque le trafic passe par le CDN).

Distribution CloudFront, HTTPS et routes SPA

CloudFront sert deux rôles : terminaison HTTPS (certificat ACM gratuit) et CDN mondial. Pour une SPA, il faut configurer une réécriture des routes : sans cela, naviguer en direct vers /admin/users renvoie une erreur 404 du bucket S3 alors que c'est une route Angular client-side valide.

Émettre un certificat HTTPS via ACM

# IMPORTANT : ACM doit être en us-east-1 (Virginie) pour CloudFront
aws acm request-certificate \
  --domain-name           angularforall.com \
  --subject-alternative-names www.angularforall.com \
  --validation-method     DNS \
  --region                us-east-1

# AWS retourne un CertificateArn, puis liste les CNAME à créer
aws acm describe-certificate \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/xxxx-yyyy \
  --region us-east-1 \
  --query 'Certificate.DomainValidationOptions[].ResourceRecord'

# Ajouter les CNAME dans Route 53 ou votre registrar pour valider
# Statut "ISSUED" sous 5-30 minutes une fois les CNAME propagés

Créer la distribution CloudFront

{
  "CallerReference":  "angularforall-2026-05-08",
  "Comment":          "Angular SPA production",
  "Enabled":          true,
  "DefaultRootObject":"index.html",
  "Origins": {
    "Quantity": 1,
    "Items": [
      {
        "Id":         "S3-angularforall-prod",
        "DomainName": "angularforall-prod.s3.eu-west-3.amazonaws.com",
        "S3OriginConfig": { "OriginAccessIdentity": "" },
        "OriginAccessControlId": "E2ABCDE3FGHIJK"
      }
    ]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId":       "S3-angularforall-prod",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": {
      "Quantity": 2,
      "Items": ["GET", "HEAD"],
      "CachedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"] }
    },
    "Compress":               true,
    "CachePolicyId":          "658327ea-f89d-4fab-a63d-7e88639e58f6",
    "ResponseHeadersPolicyId":"67f7725c-6f97-4210-82d7-5512b31e9d03"
  },
  "ViewerCertificate": {
    "ACMCertificateArn":      "arn:aws:acm:us-east-1:123456789012:certificate/xxxx-yyyy",
    "SSLSupportMethod":       "sni-only",
    "MinimumProtocolVersion": "TLSv1.2_2021"
  },
  "Aliases": {
    "Quantity": 2,
    "Items": ["angularforall.com", "www.angularforall.com"]
  },
  "PriceClass": "PriceClass_100",
  "HttpVersion": "http2and3"
}
# Créer la distribution avec le fichier ci-dessus
aws cloudfront create-distribution \
  --distribution-config file://distribution.json

# Suivre l'état (Deployed = prêt, ~10-15 min de propagation)
aws cloudfront get-distribution \
  --id E1A2B3C4D5E6F7 \
  --query 'Distribution.Status'

CloudFront Function — réécrire les routes Angular

Une CloudFront Function (JS pur, exécution edge ultra-rapide) intercepte chaque requête. Si l'URL ne correspond pas à un fichier (pas d'extension), elle réécrit l'URI vers /index.html. Le router Angular reprend ensuite la main côté client.

// spa-routing.js — fonction edge CloudFront
function handler(event) {
  var request = event.request;
  var uri     = request.uri;

  // Si l'URI a déjà une extension (.js, .css, .ico, etc.), on laisse passer
  if (uri.match(/\.[a-zA-Z0-9]{2,5}$/)) {
    return request;
  }

  // Si l'URI ne se termine pas par /, on réécrit vers /index.html
  // ex: /admin/users      → /index.html
  // ex: /products/123/buy → /index.html
  request.uri = '/index.html';
  return request;
}
# Créer la fonction
aws cloudfront create-function \
  --name           AngularSpaRouting \
  --function-code  fileb://spa-routing.js \
  --function-config Comment="Reecrit toutes les routes SPA vers index.html",Runtime=cloudfront-js-2.0

# Publier (passe en mode LIVE)
aws cloudfront publish-function \
  --name AngularSpaRouting \
  --if-match ETV123ABC

# Associer la fonction à la distribution (champ FunctionAssociations dans la default-cache-behavior)
# {
#   "FunctionAssociations": {
#     "Quantity": 1,
#     "Items": [{
#       "FunctionARN": "arn:aws:cloudfront::123456789012:function/AngularSpaRouting",
#       "EventType":   "viewer-request"
#     }]
#   }
# }

Stratégie de cache HTTP par type de fichier

Fichier Cache-Control recommandé Pourquoi
index.html no-cache, no-store, must-revalidate Référence les bundles hashés — toujours frais
main-A1B2C3.js public, max-age=31536000, immutable Hash dans le nom = nouveau fichier à chaque build
styles-D4E5F6.css public, max-age=31536000, immutable Idem JS — hash garantit la fraîcheur
ngsw-worker.js no-cache Le SW doit pouvoir se mettre à jour
assets/*.webp public, max-age=2592000 30 jours — images peu changeantes
Coût CloudFront — choix du PriceClass : PriceClass_100 (USA + Europe) coûte 0,085 $/Go en Europe, contre 0,170 $/Go pour PriceClass_All (qui inclut Asie/Inde/Brésil). Si vos utilisateurs sont européens, divisez le coût par deux en restant sur PriceClass_100.

Pipeline CI/CD complet avec GitHub Actions

Le pipeline final déclenche un build Angular à chaque push sur main, synchronise les artefacts vers S3 avec les bons en-têtes Cache-Control par type de fichier, puis crée une invalidation CloudFront ciblée sur /index.html et /ngsw.json.

Configuration OIDC GitHub ↔ AWS (sans access keys)

Plutôt que de stocker une access key statique dans GitHub Secrets, configurez l'OIDC pour échanger un token JWT GitHub contre des credentials AWS temporaires.

# 1) Créer le provider OIDC GitHub dans AWS (une fois par compte AWS)
aws iam create-open-id-connect-provider \
  --url            https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

# 2) Créer un IAM Role assumable depuis le repo GitHub
aws iam create-role \
  --role-name GitHubActions-AngularForAll-Deploy \
  --assume-role-policy-document file://trust-policy.json
// trust-policy.json — limite la confiance au repo et à la branche main
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action":  "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      },
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:angularforall/site:ref:refs/heads/main"
      }
    }
  }]
}
# 3) Attacher la policy de déploiement (créée dans la section 2)
aws iam attach-role-policy \
  --role-name  GitHubActions-AngularForAll-Deploy \
  --policy-arn arn:aws:iam::123456789012:policy/AngularForAllDeployPolicy

Workflow GitHub Actions complet

# .github/workflows/deploy-prod.yml
name: Deploy to AWS S3 + CloudFront

on:
  push:
    branches: [main]
  workflow_dispatch:        # déclenchement manuel possible

# Permissions requises pour l'OIDC (token JWT)
permissions:
  id-token: write
  contents: read

env:
  AWS_REGION:        eu-west-3
  S3_BUCKET:         angularforall-prod
  CLOUDFRONT_ID:     E1A2B3C4D5E6F7
  NODE_VERSION:      '20'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci --no-audit --prefer-offline

      - name: Lint
        run: npm run lint --if-present

      - name: Unit tests
        run: npm run test -- --watch=false --browsers=ChromeHeadless

      - name: Inject build version (commit SHA)
        run: |
          SHA_SHORT=$(echo $GITHUB_SHA | cut -c1-7)
          sed -i "s/__BUILD_VERSION__/$SHA_SHORT/g" src/environments/environment.prod.ts

      - name: Build Angular production
        run: npx ng build --configuration production

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume:    arn:aws:iam::123456789012:role/GitHubActions-AngularForAll-Deploy
          role-session-name: GitHubActions-${{ github.run_id }}
          aws-region:        ${{ env.AWS_REGION }}

      - name: Upload hashed assets (immutable, cache 1 an)
        run: |
          aws s3 sync dist/angularforall/browser/ s3://${{ env.S3_BUCKET }}/ \
            --delete \
            --exclude "index.html" \
            --exclude "ngsw-worker.js" \
            --exclude "ngsw.json" \
            --exclude "*.webmanifest" \
            --cache-control "public, max-age=31536000, immutable"

      - name: Upload index.html and SW (no-cache)
        run: |
          aws s3 cp dist/angularforall/browser/index.html s3://${{ env.S3_BUCKET }}/index.html \
            --cache-control "no-cache, no-store, must-revalidate" \
            --content-type "text/html; charset=utf-8"

          aws s3 cp dist/angularforall/browser/ngsw-worker.js s3://${{ env.S3_BUCKET }}/ngsw-worker.js \
            --cache-control "no-cache" \
            --content-type "application/javascript"

          aws s3 cp dist/angularforall/browser/ngsw.json s3://${{ env.S3_BUCKET }}/ngsw.json \
            --cache-control "no-cache" \
            --content-type "application/json"

      - name: Invalidate CloudFront (paths critiques uniquement)
        run: |
          INVALIDATION_ID=$(aws cloudfront create-invalidation \
            --distribution-id ${{ env.CLOUDFRONT_ID }} \
            --paths "/index.html" "/ngsw.json" "/ngsw-worker.js" \
            --query 'Invalidation.Id' \
            --output text)
          echo "Invalidation créée : $INVALIDATION_ID"

          aws cloudfront wait invalidation-completed \
            --distribution-id ${{ env.CLOUDFRONT_ID }} \
            --id $INVALIDATION_ID

      - name: Smoke test post-deploy
        run: |
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://angularforall.com/)
          if [ "$STATUS" -ne 200 ]; then
            echo "Smoke test FAILED — status $STATUS"
            exit 1
          fi
          echo "Smoke test OK — status $STATUS"

      - name: Notify on failure
        if: failure()
        run: |
          curl -X POST -H 'Content-Type: application/json' \
            --data '{"text":"❌ Deploy AngularForAll failed on main"}' \
            ${{ secrets.SLACK_WEBHOOK_URL }}

Pipeline équivalent en environnement preview/staging

# .github/workflows/deploy-preview.yml — déploie chaque PR sur un sous-domaine
name: Deploy PR Preview

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npx ng build --configuration staging --base-href "/pr-${{ github.event.number }}/"

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActions-AngularForAll-Deploy
          aws-region:     eu-west-3

      - name: Deploy preview to /pr-NN/ prefix
        run: |
          aws s3 sync dist/angularforall/browser/ \
            s3://angularforall-preview/pr-${{ github.event.number }}/ \
            --delete

      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner:        context.repo.owner,
              repo:         context.repo.repo,
              body: `🚀 Preview déployée : https://preview.angularforall.com/pr-${{ github.event.number }}/`
            });
Le aws cloudfront wait invalidation-completed bloque le pipeline jusqu'à ce que l'invalidation soit terminée (1-3 minutes). Le smoke test qui suit garantit qu'aucun utilisateur ne tombera sur l'ancien index.html mis en cache.

Sécurité, monitoring et maîtrise des coûts

Headers de sécurité via CloudFront Response Headers Policy

CloudFront permet d'injecter des headers de sécurité sans modifier l'application. Activez ces protections de base : HSTS, anti-clickjacking, anti-MIME-sniffing, et une CSP de base.

{
  "Name":    "angularforall-security-headers",
  "Comment": "Headers sécurité pour SPA Angular",
  "SecurityHeadersConfig": {
    "StrictTransportSecurity": {
      "Override":                true,
      "AccessControlMaxAgeSec":  31536000,
      "IncludeSubdomains":       true,
      "Preload":                 true
    },
    "ContentTypeOptions":   { "Override": true },
    "FrameOptions":         { "Override": true, "FrameOption": "DENY" },
    "ReferrerPolicy":       { "Override": true, "ReferrerPolicy": "strict-origin-when-cross-origin" },
    "XSSProtection":        { "Override": true, "Protection": true, "ModeBlock": true },
    "ContentSecurityPolicy": {
      "Override":              true,
      "ContentSecurityPolicy": "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.angularforall.com"
    }
  }
}

Monitoring CloudWatch et budget alerts

# Alarme : facture estimée > 20 €/mois
aws cloudwatch put-metric-alarm \
  --alarm-name           "BillingAlert-AngularForAll-20EUR" \
  --metric-name          EstimatedCharges \
  --namespace            AWS/Billing \
  --statistic            Maximum \
  --period               21600 \
  --evaluation-periods   1 \
  --threshold            20 \
  --comparison-operator  GreaterThanThreshold \
  --dimensions           Name=Currency,Value=USD \
  --alarm-actions        arn:aws:sns:us-east-1:123456789012:billing-alerts

# Alarme : taux d'erreur 5xx CloudFront > 1% sur 5 min
aws cloudwatch put-metric-alarm \
  --alarm-name           "CloudFront-5xx-AngularForAll" \
  --metric-name          5xxErrorRate \
  --namespace            AWS/CloudFront \
  --dimensions           Name=DistributionId,Value=E1A2B3C4D5E6F7 Name=Region,Value=Global \
  --statistic            Average \
  --period               300 \
  --evaluation-periods   1 \
  --threshold            1 \
  --comparison-operator  GreaterThanThreshold \
  --alarm-actions        arn:aws:sns:us-east-1:123456789012:ops-alerts

AWS WAF — protection contre le scraping et les bots

# Web ACL minimal avec règles managées AWS (anti-bot, anti-injection)
aws wafv2 create-web-acl \
  --name             angularforall-waf \
  --scope            CLOUDFRONT \
  --region           us-east-1 \
  --default-action   Allow={} \
  --rules '[
    {
      "Name":     "AWSManagedRulesCommonRuleSet",
      "Priority": 1,
      "OverrideAction": { "None": {} },
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name":       "AWSManagedRulesCommonRuleSet"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled":   true,
        "CloudWatchMetricsEnabled": true,
        "MetricName":               "common-rules"
      }
    },
    {
      "Name":     "RateLimit2000",
      "Priority": 2,
      "Action": { "Block": {} },
      "Statement": {
        "RateBasedStatement": {
          "Limit":            2000,
          "AggregateKeyType": "IP"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled":   true,
        "CloudWatchMetricsEnabled": true,
        "MetricName":               "rate-limit"
      }
    }
  ]' \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=angularforall-waf

Rollback rapide en cas de mauvais déploiement

# Lister les versions de index.html (versioning S3 activé)
aws s3api list-object-versions \
  --bucket angularforall-prod \
  --prefix index.html \
  --max-keys 5

# Restaurer la version précédente
aws s3api copy-object \
  --bucket      angularforall-prod \
  --key         index.html \
  --copy-source angularforall-prod/index.html?versionId=ABC123XYZ \
  --metadata-directive REPLACE \
  --cache-control "no-cache, no-store, must-revalidate"

# Invalider le cache CloudFront pour appliquer immédiatement
aws cloudfront create-invalidation \
  --distribution-id E1A2B3C4D5E6F7 \
  --paths "/index.html"
Maîtrise des coûts d'invalidation : Les 1 000 premières invalidations par mois sont gratuites. Au-delà, 0,005 $ par chemin invalidé. Avec un déploiement par jour invalidant 3 chemins (index.html, ngsw.json, ngsw-worker.js), vous restez largement dans le free tier (~90/mois).
Évitez à tout prix --paths "/*" dans le pipeline régulier — chaque chemin invalidé est facturé. Réservez l'invalidation totale aux cas exceptionnels (mise à jour du WAF, changement de structure d'URL).

Conclusion et checklist mise en production

Cette stack S3 + CloudFront + GitHub Actions est aujourd'hui la solution la plus robuste et économique pour héberger une SPA Angular. Vous obtenez une infrastructure scalable à l'infini, un CDN avec 450+ POPs mondiaux, HTTPS gratuit via ACM, et un pipeline qui passe d'un push main à la production en moins de 3 minutes — le tout pour 5 € par mois en moyenne.

Les points clés à retenir : configurer CloudFront avec une OAC (jamais un bucket public), gérer les routes Angular via une CloudFront Function qui réécrit vers index.html, séparer le cache long terme des bundles hashés du no-cache sur index.html et le service worker, et utiliser OIDC côté GitHub Actions pour éviter les access keys statiques.

Checklist mise en production :
  • ✅ Bucket S3 privé avec BlockPublicAccess activé sur les 4 options
  • ✅ Versioning et chiffrement AES-256 activés sur le bucket
  • ✅ Lifecycle policy : suppression des anciennes versions après 30 jours
  • ✅ Distribution CloudFront avec OAC (pas d'OAI déprécié)
  • ✅ Certificat ACM en us-east-1 validé en DNS
  • ✅ CloudFront Function pour la réécriture des routes SPA
  • ✅ Cache-Control différencié : immutable sur les bundles hashés, no-cache sur index.html
  • ✅ Response Headers Policy avec HSTS, CSP, X-Frame-Options DENY
  • ✅ Service worker (ngsw-worker.js) servi en no-cache
  • ✅ IAM Role OIDC pour GitHub Actions (pas d'access key statique)
  • ✅ Policy de déploiement least-privilege sur 1 bucket + 1 distribution
  • ✅ Workflow GitHub Actions avec lint + test + build + smoke test
  • ✅ Invalidation ciblée sur /index.html, /ngsw.json, /ngsw-worker.js
  • ✅ CloudWatch billing alarm configuré (seuil mensuel)
  • ✅ AWS WAF avec rate-limiting et règles managées AWS
  • ✅ Procédure de rollback testée (versioning S3 + invalidation)

Pour aller plus loin : explorez CloudFront KeyValueStore pour des A/B tests edge, Lambda@Edge pour de la SSR partielle, ou intégrez un Origin Shield si votre trafic dépasse 100k requêtes/jour. La même architecture supporte aussi parfaitement les frameworks comme Astro, SvelteKit (output static) ou Next.js en mode export.

Partager