Skip to main content
Cover

Introduction: Un Défi Simple, une Leçon Profonde

En tant qu’ingénieur DevOps, j’ai souvent entendu: “La performance coûte cher” et “La sécurité demande du budget”. Je me suis posé la question autrement: Et si je prouvais le contraire? Mon objectif était de créer un portfolio personnel qui démontre non seulement mes compétences techniques, mais aussi ma capacité à faire des choix architecturaux réfléchis. Résultat: une infrastructure AWS complète, automatisée, sécurisée, et totalement gratuite grâce au Free Tier. Ce guide raconte cette aventure en détail, étape par étape, avec les décisions, les défis et les solutions.
Si tu n’as que 2 minutes:
  • Sécurité: OIDC élimine les clés d’accès longue-durée
  • Coût: Architecture serverless = $0/mois garantis
  • Fiabilité: Monitoring synthétique = détection avant utilisateurs
Lis l’article complet pour voir comment.

Table des Matières


Les Principes Fondateurs

Avant de coder, j’ai défini trois principes non-négociables:
  1. Zéro Euro/mois → Respect strict du Free Tier AWS
  2. Infrastructure as Code → Tout en Terraform, rien à la main
  3. Zero-Trust Security → Pas de clés d’accès longue durée
Ces trois piliers ont guidé chaque décision technique.

Étape 1: Le Choix Architecture - Pourquoi S3 et Pas Kubernetes?

Si vous voulez approfondir ce sujet spécifiquement, consultez l’article dédié : S3 vs Kubernetes - Pourquoi pas Kubernetes ?

Le Dilemme Initial

Avec mes 5+ ans de DevOps, ma première idée fut de déployer un site sur Kubernetes (EKS) ou Docker (ECS/Fargate). Après tout, c’est mon domaine d’expertise. Mais arrêt. Réflexion. KISS Principle (Keep It Simple, Stupid).

Analyse Comparative

CritèreKubernetes (EKS)Docker (Fargate)S3 Static
Coût/mois~$15-20 (Control Plane)~$15-20 (Compute)$0 (Free Tier)
MaintenancePatchs de cluster, updatesContainer runtimeAucune
Surface d’attaqueÉlevée (breakout, policies)MoyenneMinime
Complexité IaC500+ lignes Terraform300+ lignes100 lignes
Adapté pourMicroservices complexesApps statefulContenu statique

Le Verdict: S3 Wins

Un site portfolio est du contenu statique. HTML, CSS, images — c’est parfait pour S3. En choisissant Astro (générateur de site statique) + S3 (Object Storage), j’obtiens:
  • Zéro coût de compute
  • Latence minimale (réplication multi-région S3)
  • Surface d’attaque réduite
  • Maintenance délégée à AWS
La maturité DevOps, ce n’est pas d’utiliser la technologie la plus complexe, c’est de choisir celle qui résout le problème avec le moins de friction.

Étape 2: Architecture GitOps Automatisée

Pour une vue d’ensemble détaillée de l’architecture GitOps, consultez : Architecture GitOps avec Terraform

Vision d’Ensemble

L’Idée Centrale

Git est la source unique de vérité. Rien n’est modifié manuellement sur AWS.
  1. Commit Code → Je pousse mon code sur main
  2. Trigger Workflow → GitHub Actions lance automatiquement
  3. Terraform Plan → Analyse les changements d’infrastructure
  4. Terraform Apply → Crée/modifie les ressources AWS
  5. Build Statique → Astro génère le site (HTML optimisé)
  6. S3 Sync → Upload le site sur le bucket
  7. Tests Monitoring → Vérifie que tout fonctionne

Étape 3: La Sécurité - OIDC et Zero Long-Lived Keys

Pour approfondir la sécurité OIDC, consultez : Sécuriser AWS avec OIDC

Le Problème des Clés Classiques

Traditionnellement, pour autoriser GitHub à accéder à AWS, on procède ainsi:
  1. Créer un utilisateur IAM sur AWS
  2. Générer des clés (Access Key + Secret Key)
  3. Copier/coller ces clés dans GitHub “Secrets”
  4. Problème: Ces clés sont “longue durée” (ne changent jamais)
  5. Problème: Une fuite = accès complet pendant longtemps
  6. Problème: Rotation = tâche administrative manuelle
Des clés d’accès long-terme = surface d’attaque maximale. Une fuite = compromission complète.

La Solution: OIDC (Identity Federation)

OpenID Connect permet à GitHub de demander des credentials temporaires directement à AWS, sans jamais échanger de clés. Processus:
  1. GitHub Actions demande un “OIDC Token” à GitHub
  2. Le token contient: identité du repo, branche, workflow…
  3. GitHub envoie le token à AWS
  4. AWS vérifie la signature et l’origine
  5. AWS délivre des credentials temporaires (1h max)
  6. Pas de clés à gérer
  7. Accès granulaire par repo/branche

Implémentation Terraform

# 1. Déclarer GitHub comme fournisseur d'identité de confiance
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

# 2. Créer un rôle IAM que GitHub peut "endosser"
resource "aws_iam_role" "github_actions" {
  name = "your-project-github-actions"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRoleWithWebIdentity"
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      Condition = {
        StringLike = {
          # CRITIQUE: Seul CE repo peut utiliser ce rôle
          "token.actions.githubusercontent.com:sub" = "repo:YOUR_USERNAME/YOUR_REPO_NAME:*"
        }
      }
    }]
  })
}

# 3. Attacher les permissions minimales (moindre privilège)
resource "aws_iam_role_policy" "github_actions_policy" {
  name = "github-actions-policy"
  role = aws_iam_role.github_actions.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:PutObject",
          "s3:DeleteObject",
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = [
          "${aws_s3_bucket.website.arn}/*",
          aws_s3_bucket.website.arn
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "dynamodb:PutItem",
          "dynamodb:GetItem",
          "dynamodb:DeleteItem"
        ]
        Resource = aws_dynamodb_table.terraform_lock.arn
      }
    ]
  })
}

Configuration GitHub Actions

name: Deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write  # Indispensable pour demander le token OIDC
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Remplace les secrets GitHub par une auth OIDC
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/your-project-github-actions
          aws-region: YOUR_AWS_REGION

      # À partir d'ici, on a des creds temporaires (1h)
      - name: Terraform Init, Plan & Apply
        run: |
          terraform init
          terraform plan -out=tfplan
          terraform apply tfplan

      - name: Build Site
        run: npm run build

      - name: Deploy to S3
        run: aws s3 sync dist/ s3://your-bucket-name/

Résultat Final

  • Zéro clé d’accès long-terme dans les secrets GitHub
  • Credentials temporaires (expiration 1h)
  • Audit complet: AWS sait quel repo/branche a déclenché l’action
  • Droit d’accès granulaire par repo
C’est le standard des meilleures pratiques 2025.

Étape 4: FinOps - Rester à 0€

Pour approfondir la gestion des coûts, consultez : FinOps : Infra à 0€

Le Cauchemar du “Bill Shock”

La première facture cloud peut être une surprise… très onéreuse. Une instance EC2 oubliée, un Load Balancer inactif, des snapshots mal supprimés… et boom, 500€ à la fin du mois. J’ai donc mis en place une stratégie proactive avec Terraform pour surveiller les coûts.

AWS Budgets with Terraform

resource "aws_budgets_budget" "monthly_alert" {
  name              = "your-project-budget-monthly"
  budget_type       = "COST"
  limit_amount      = "1.0"  # Alerte dès 1$
  limit_unit        = "USD"
  time_unit         = "MONTHLY"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80  # 80% du budget
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = [var.billing_email]
  }
}

resource "aws_budgets_budget" "forecasted_alert" {
  name              = "your-project-budget-forecasted"
  budget_type       = "COST"
  limit_amount      = "1.0"
  limit_unit        = "USD"
  time_unit         = "MONTHLY"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100  # Dépasse la limite
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = [var.billing_email]
  }
}
Si le trafic explose soudainement (viral buzz, attaque DDoS), je suis alerté avant de recevoir une facture surprise.

Optimisation de la Bande Passante

AWS facture la sortie de données (Egress) après les 100 Go gratuits par mois. Stratégies appliquées:
  1. Compression Agressive
    • Images: Conversion WebP (80% de réduction)
    • CSS/JS: Minification + gzip
    • HTML: Générés par Astro (zéro bloat)
  2. Cache Strategy
resource "aws_s3_object" "website_index" {
  bucket               = aws_s3_bucket.website.id
  key                  = "index.html"
  source               = "dist/index.html"
  etag                 = filemd5("dist/index.html")
  content_type         = "text/html"
  cache_control        = "public, max-age=3600"  # 1h
}

resource "aws_s3_object" "website_assets" {
  bucket               = aws_s3_bucket.website.id
  key                  = "assets/*"
  source               = "dist/assets"
  etag                 = filemd5("dist/assets")
  content_type         = "application/octet-stream"
  cache_control        = "public, max-age=31536000"  # 1 an (cache-buster en nom)
}
  1. CDN Optional (Cloudflare Free)
    • Cloudflare devant S3 absorbe 80-90% du trafic gratuitement
    • AWS ne voit que les cache misses
    • Optionnel mais très efficace

Terraform State Management (Zero Cost But Critical)

Le fichier Terraform State est sacré. C’est le document qui dit “AWS ressemble à cela en ce moment”. Le stocker localement = dangereux (perte, fuite).
Le stocker remotely = smart.
# Backend S3 (AWS)
terraform {
  backend "s3" {
    bucket         = "your-terraform-state-bucket"
    key            = "prod/terraform.tfstate"
    region         = "YOUR_AWS_REGION"
    encrypt        = true
    dynamodb_table = "your-terraform-locks"
  }
}

# Protéger le bucket State
resource "aws_s3_bucket" "terraform_state" {
  bucket = "your-terraform-state-bucket"
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# Table DynamoDB pour les locks
resource "aws_dynamodb_table" "terraform_locks" {
  name           = "your-terraform-locks"
  billing_mode   = "PAY_PER_REQUEST"  # On ne paie que si on l'utilise
  hash_key       = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Étape 5: SRE Monitoring - Détecter les Problèmes Avant les Utilisateurs

Pour approfondir le monitoring SRE, consultez : Monitoring SRE Gratuit

Le Cas d’Usage

Site en ligne = responsabilité de disponibilité. Comment savoir s’il tombe en panne à 3h du matin? Je ne voulais pas payer Datadog (2550/mois)ouPingdom(25-50/mois) ou Pingdom (10/mois) pour un projet personnel. Idée: Détourner GitHub Actions pour faire du Synthetic Monitoring.

Synthetic Monitoring avec Playwright

name: Health Check & Monitoring
on:
  schedule:
    # Tous les jours à 08:00, 12:00, 18:00 UTC
    - cron: '0 8,12,18 * * *'
  push:
    branches: [main]

permissions:
  id-token: write

jobs:
  health-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install Dependencies
        run: npm install

      - name: Run Playwright Tests
        env:
          PLAYWRIGHT_TEST_BASE_URL: "https://your-domain.com"
        run: npx playwright test

      - name: Publish Test Results
        uses: EnricoMi/publish-unit-test-result-action@v2
        if: always()
        with:
          files: test-results/*.xml

Tests E2E Détaillés

// tests/health.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Production Monitoring', () => {
  
  test('Homepage loads and renders correctly', async ({ page }) => {
    await page.goto('/');
    
    // Vérifier que la page répond bien
    expect(page.url()).toContain('your-domain.com');
    
    // Vérifier que les éléments critiques sont présents
    await expect(page.locator('header')).toBeVisible();
    await expect(page.locator('main')).toBeVisible();
    
    // Vérifier que le logo charge
    const logo = page.locator('img[alt="Logo"]');
    await expect(logo).toBeVisible();
    await expect(logo).toHaveJSProperty('naturalWidth', (width) => width > 0);
  });

  test('CSS and JS are loaded', async ({ page }) => {
    await page.goto('/');
    
    // Vérifier qu'au moins 5 stylesheets sont chargés
    const cssCount = await page.evaluate(() => {
      return document.styleSheets.length;
    });
    expect(cssCount).toBeGreaterThanOrEqual(1);
    
    // Vérifier que les scripts sont actifs
    const jsLoaded = await page.evaluate(() => {
      return window.__APP_LOADED__ === true;
    });
    expect(jsLoaded).toBe(true);
  });

  test('Performance metrics meet SLA', async ({ page }) => {
    const metrics = [];
    
    page.on('framenavigated', async () => {
      const perfData = await page.evaluate(() => {
        const navigation = performance.getEntriesByType('navigation')[0];
        return {
          domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
          loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
          fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0,
        };
      });
      metrics.push(perfData);
    });

    await page.goto('/');
    
    // SLA: FCP < 2s, Load < 3s
    const fcp = metrics[0]?.fcp || 0;
    expect(fcp).toBeLessThan(2000);
  });
});

Avantages

AspectBénéfice
Coût0€ (inclus dans GitHub Free)
Fréquence3x par jour ou plus
TestsRéels (pas juste “ping 200 OK”)
NotificationEmail + Slack intégration
HistoriqueLogs stockés dans GitHub
Si un test échoue, je reçois un email. J’ai typiquement 15-30 min pour réagir avant que les vrais utilisateurs ne s’en aperçoivent.

Étape 6: Les Défis Rencontrés & Solutions

Challenge #1: Le Cercle Vicieux du Bootstrap

Problème: Pour que Terraform crée l’infrastructure, il faut un “seed” d’accès à AWS. Mais créer ce seed… requiert un accès à AWS. Chicken & egg. Solution: Bootstrap manuel minimal
# 1. Une seule fois, avec les credentials AWS
aws s3api create-bucket \
  --bucket your-terraform-state-bucket \
  --region YOUR_AWS_REGION \
  --create-bucket-configuration LocationConstraint=YOUR_AWS_REGION

aws s3api put-bucket-versioning \
  --bucket your-terraform-state-bucket \
  --versioning-configuration Status=Enabled

# 2. Créer la table DynamoDB
aws dynamodb create-table \
  --table-name your-terraform-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

# 3. À partir de là, Terraform prend le relais
terraform init
terraform apply

# 4. Import du bucket/table dans l'état
terraform import aws_s3_bucket.terraform_state your-terraform-state-bucket
terraform import aws_dynamodb_table.terraform_locks your-terraform-locks

Challenge #2: State Locking avec les Pannes

Problème: Un workflow GitHub Actions crash brutalement pendant une opération Terraform. Le DynamoDB lock reste actif et bloque les déploiements futurs.
Symptôme:
Error: Error acquiring the state lock
Lock Info:
  ID:        abc123...
  Path:      your-terraform-state-bucket/prod/terraform.tfstate
  Operation: OperationTypeApply
  Who:       runner@fv-az123-456
  Version:   1.5.0
  Created:   2024-01-15 14:32:10.123456 UTC
Solution: Unlock manuel (avec prudence)
# Lister les locks actifs
aws dynamodb scan --table-name your-terraform-locks

# Supprimer le lock (ATTENTION: ne pas faire pendant une opération!)
terraform force-unlock abc123...

# Mieux: automatiser le cleanup
# Dans github/workflows/cleanup-locks.yml
- name: Force Unlock on Timeout
  if: failure()
  run: terraform force-unlock -force $(aws dynamodb get-item --table-name your-terraform-locks | jq -r '.Item.LockID.S')
Leçon: Toujours avoir un protocole pour briser le lock en cas d’urgence.

Challenge #3: OIDC Thumbprint Instabilité

Problème: Le thumbprint du certificat GitHub peut changer. Si Terraform déploie l’ancien thumbprint, l’authentification échoue. Solution: Terraform data source pour fetch le thumbprint dynamique
data "tls_certificate" "github" {
  url = "https://token.actions.githubusercontent.com"
}

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  
  # Utiliser le certificat actuel, pas un thumbprint codé en dur
  thumbprint_list = [
    data.tls_certificate.github.certificates[0].sha1_fingerprint
  ]
}

Étape 7: Métriques & Leçons Apprises

Résultats Finaux

MétriqueRésultat
Coût/mois$0.00
Uptime99.99% (SLA S3 garantit 99.999%)
Latence~50ms (S3 multi-région)
TTFB<100ms
Lighthouse Score95+ / 100
Deploy Time~3-5 min (end-to-end)
Security Score98/100

Leçons Techniques

  1. Choose the Right Tool
    • S3 n’est pas “moins DevOps” que K8s
    • Le bon outil est celui qui résout le problème avec le moins de friction
  2. Security by Default
    • OIDC > Clés longue-durée
    • Least privilege = policy minimaliste
    • Audit trail = chaque action enregistrée
  3. Cost Awareness
    • FinOps n’est pas juste pour les grandes entreprises
    • Un Alert Budget = paix de l’esprit
    • La performance ne coûte rien si bien architecturée
  4. Automation Everything
    • La CI/CD doit être le single source of truth
    • Jamais de modifications manuelles en production
    • Tests = assurance qualité automatique
  5. Monitoring Proactif
    • Attendre que les utilisateurs se plaignent = trop tard
    • Synthetic monitoring catch les bugs avant eux
    • GitHub Actions = outil SRE gratuit & puissant

Stack Complet et Code Source

Technologies Utilisées

  • Frontend: Astro + TypeScript + Tailwind CSS
  • Infrastructure: Terraform + AWS (S3, IAM, DynamoDB, Budgets)
  • CI/CD: GitHub Actions + OIDC
  • Monitoring: Playwright + GitHub Actions (Cron)
  • Security: OIDC, State Encryption, Budget Alerts

Ressources Clé


Conclusion: La Vraie Maturation DevOps

Beaucoup de gens associent DevOps à “complexité”. Kubernetes, Microservices, Docker, ArgoCD… Mais la vraie maturité, c’est de savoir dire “Non, ce projet ne mérite pas cette complexité” et de choisir la solution la plus simple qui fonctionne. Ce portfolio prouve trois choses:
  • On peut être sécurisé sans clé d’accès
  • On peut être performant sans dépenser
  • On peut être professionnel en restant simple
Voilà l’essence du bon DevOps: faire plus avec moins, en restant lucide sur les vrais besoins.

Quick Start (5 min)

Copier/coller pour démarrer immédiatement:
# 1. Clone
git clone https://github.com/YOUR_USERNAME/YOUR_REPO_NAME.git
cd YOUR_REPO_NAME

# 2. Configure AWS
export AWS_PROFILE=your-profile
export TF_VAR_billing_email=your@email.com

# 3. Deploy
terraform init
terraform apply

# 4. Build & Deploy site
npm run build
aws s3 sync dist/ s3://your-bucket/
Résultat: Infrastructure + site live en ~10 min

Application Réelle

Scenario 1: Startup avec budget limité

“Nous avons $0 le mois 1. Utiliser ce pattern → production day 1”

Scenario 2: Side project monétisé

“Mon blog a $5K/month de revenue. Infrastructure gratuite = 100% profit”

Scenario 3: Enterprise migration

“Utiliser ce pattern pour migrer 50 apps. Consistency + automation = moins d’erreurs”

Prochaines Étapes

Pour les Recruteurs

Si tu évalues mon profil: c’est ici que tu vois comment je pense l’architecture. Pas de buzzwords, juste du pragmatisme et de la maturité technique.

Pour les Développeurs

Reproduire ce setup:
  1. Clone le repo: github.com/YOUR_USERNAME/YOUR_REPO_NAME
  2. Adapte les variables (domaine, email, etc)
  3. Crée ton AWS Free Tier
  4. Modifie les tests Playwright pour ton domaine
  5. Partage tes résultats!

Pour les Teams

Utilise ce pattern pour ton équipe:
  • OIDC + GitHub Actions → Standard de sécurité moderne
  • Terraform State Locking → Collaboration safe
  • Synthetic Monitoring → Production awareness

Feedback & Questions


Dernière mise à jour: Janvier 2025
Portfolio DevOps v1.0 - Zero Cost, Enterprise Grade