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 |
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
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
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
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 |
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 }}/`
});
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"
index.html, ngsw.json, ngsw-worker.js), vous restez largement dans le free tier (~90/mois).
--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.
- ✅ 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-1validé en DNS - ✅ CloudFront Function pour la réécriture des routes SPA
- ✅ Cache-Control différencié :
immutablesur les bundles hashés,no-cachesurindex.html - ✅ Response Headers Policy avec HSTS, CSP, X-Frame-Options DENY
- ✅ Service worker (
ngsw-worker.js) servi enno-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.