Maîtriser les Architectures Serverless : Développer des Applications Scalables et Économiques
Maîtriser les Architectures Serverless : Développer des Applications Scalables et Économiques

Projet Pratique : Construction d'une Application Serverless Complète

Contexte du cours : Maîtriser les Architectures Serverless : Développer des Applications Scalables et Économiques

Introduction : L'Ère du Serverless en Action

Bienvenue à cette leçon pratique qui vous guidera à travers la construction d'une application serverless de bout en bout. Après avoir exploré les concepts théoriques et les avantages des architectures serverless, il est temps de mettre les mains dans le cambouis et de concrétiser nos connaissances. Ce projet vous permettra de comprendre comment assembler différentes briques serverless pour créer une application réelle, scalable et économique.

Nous allons concevoir une application de gestion de produits simple, exposant une API REST pour interagir avec un catalogue de produits. Le frontend sera une application web statique, déployée sur un service de stockage d'objets, interagissant avec notre API serverless.

Pourquoi un projet pratique est-il essentiel ?

  • Intégration des connaissances : Appliquer les concepts appris (Lambda, API Gateway, bases de données NoSQL, déploiement) dans un contexte unifié.
  • Compréhension holistique : Voir comment les différentes pièces du puzzle serverless s'emboîtent pour former une solution complète.
  • Maîtrise des outils : Se familiariser avec les frameworks et outils de déploiement spécifiques au serverless.
  • Anticipation des défis : Identifier les défis pratiques et les meilleures pratiques lors du développement serverless.

1. Définition du Projet : Catalogue de Produits Serverless

Notre application sera un système de gestion de catalogue de produits. Elle permettra de :

  • Créer de nouveaux produits.
  • Lister tous les produits.
  • Récupérer les détails d'un produit spécifique.
  • Mettre à jour un produit existant.
  • Supprimer un produit.

1.1 Objectifs Techniques

  • Développer une API REST robuste et sans serveur.
  • Utiliser une base de données NoSQL gérée.
  • Déployer une application frontend statique.
  • Mettre en œuvre l'authentification et l'autorisation simples.
  • Automatiser le déploiement.

1.2 Technologies Clés

Nous nous appuierons principalement sur les services AWS, accompagnés d'outils populaires pour le développement serverless :

  • AWS Lambda : Fonctions "Function as a Service" (FaaS) pour l'exécution du code backend.
  • Amazon API Gateway : Point d'entrée pour l'API REST, gérant les requêtes HTTP et les routant vers les fonctions Lambda.
  • Amazon DynamoDB : Base de données NoSQL rapide et hautement disponible, gérée par AWS.
  • Amazon S3 : Stockage d'objets pour héberger les fichiers statiques du frontend.
  • Amazon CloudFront : Réseau de diffusion de contenu (CDN) pour distribuer le frontend et servir les requêtes API avec faible latence.
  • Amazon Cognito : Service d'identité pour l'authentification des utilisateurs.
  • Serverless Framework : Outil CLI pour le déploiement et la gestion de notre infrastructure serverless.
  • Node.js (ou Python) : Langage de programmation pour les fonctions Lambda.
  • React (ou Vue/Angular) : Framework JavaScript pour le développement du frontend.

1.3 Architecture Générale du Projet

Voici une vue d'ensemble simplifiée de notre architecture serverless :

graph TD
    User -->|HTTP Request| CloudFront
    CloudFront --(1)--> S3[S3 (Frontend)]
    CloudFront --(2)--> APIGW[API Gateway]
    APIGW --> Lambda[AWS Lambda (Backend Functions)]
    Lambda --> DynamoDB[DynamoDB (Product Data)]
    User -->|Authentication| Cognito[Amazon Cognito]
    Cognito --> APIGW
    Lambda -- Monitoring --> CloudWatch[CloudWatch Logs/Metrics]
    APIGW -- Monitoring --> CloudWatch

Explication :

  1. L'utilisateur accède à l'application via CloudFront, qui distribue les fichiers du frontend depuis S3.
  2. Les requêtes API du frontend sont également routées via CloudFront vers API Gateway.
  3. API Gateway reçoit les requêtes, les valide (éventuellement via Cognito pour l'authentification) et les transmet aux fonctions Lambda appropriées.
  4. Les fonctions Lambda exécutent la logique métier, interagissent avec DynamoDB pour la persistance des données, et renvoient une réponse à API Gateway.
  5. CloudWatch collecte les logs et métriques de tous les services pour la surveillance et le débogage.
  6. Cognito gère l'authentification des utilisateurs, fournissant des tokens JWT que API Gateway peut valider.

2. Étape 1 : Initialisation et Configuration de l'Environnement

Avant de coder, nous devons préparer notre environnement de développement.

2.1 Prérequis Logiciels

  • Node.js et npm (ou yarn) : Pour les fonctions Lambda (si vous utilisez Node.js) et le frontend.
  • AWS CLI : Pour configurer vos informations d'identification AWS et interagir avec les services.
  • Serverless Framework CLI : L'outil principal pour définir et déployer votre infrastructure serverless.
# Installation de l'AWS CLI (si ce n'est pas déjà fait)
pip install awscli --upgrade --user

# Configuration des informations d'identification AWS
aws configure
# Vous devrez entrer votre AWS Access Key ID, AWS Secret Access Key, région par défaut et format de sortie.

# Installation du Serverless Framework CLI
npm install -g serverless

2.2 Création du Squelette du Projet

Le Serverless Framework nous permet de générer rapidement la structure de base d'un projet serverless.

# Créer un nouveau service serverless
sls create --template aws-nodejs --path product-catalog-api
cd product-catalog-api

Ceci crée un répertoire product-catalog-api avec un fichier serverless.yml et une fonction Lambda d'exemple.

3. Étape 2 : Développement du Backend Serverless (API REST)

Nous allons maintenant implémenter les fonctions Lambda et définir les routes API Gateway.

3.1 Définition de l'Infrastructure avec serverless.yml

Le fichier serverless.yml est le cœur de votre service serverless. Il décrit toutes les ressources AWS nécessaires (fonctions Lambda, API Gateway, DynamoDB, etc.) et comment elles sont interconnectées.

Voici un extrait simplifié pour commencer :

# serverless.yml
service: product-catalog-api

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs18.x # Ou python3.9 si vous préférez Python
  region: eu-west-3 # Choisissez votre région AWS
  stage: dev # Environnement de déploiement (dev, staging, prod)
  # Permissions IAM nécessaires pour les fonctions Lambda
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
            - dynamodb:Scan
            - dynamodb:Query
          Resource: "arn:aws:dynamodb:${aws:region}:*:table/${self:custom.productsTableName}"

custom:
  productsTableName: 'products-table-${sls:stage}'

functions:
  createProduct:
    handler: handler.createProduct
    events:
      - httpApi:
          path: /products
          method: post
  getProducts:
    handler: handler.getProducts
    events:
      - httpApi:
          path: /products
          method: get
  getProduct:
    handler: handler.getProduct
    events:
      - httpApi:
          path: /products/{id}
          method: get
  updateProduct:
    handler: handler.updateProduct
    events:
      - httpApi:
          path: /products/{id}
          method: put
  deleteProduct:
    handler: handler.deleteProduct
    events:
      - httpApi:
          path: /products/{id}
          method: delete

resources:
  Resources:
    ProductsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.productsTableName}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST # Mode On-Demand, vous payez uniquement pour ce que vous utilisez.

Explication de l'extrait serverless.yml :

  • service : Nom de votre service serverless.
  • frameworkVersion : Version du Serverless Framework utilisée.
  • provider : Définit le fournisseur cloud (AWS), le runtime des fonctions Lambda, la région et le stage (environnement).
  • iam.role.statements : Accorde les permissions nécessaires à vos fonctions Lambda pour interagir avec DynamoDB. Il est crucial de suivre le principe du moindre privilège.
  • custom : Section pour définir des variables personnalisées (ici, le nom de la table DynamoDB).
  • functions : Liste de toutes vos fonctions Lambda. Pour chaque fonction :
    • handler : Le chemin vers le fichier et le nom de la fonction JavaScript/Python à exécuter (ex: handler.createProduct signifie la fonction createProduct dans handler.js).
    • events : Déclencheurs pour la fonction. Ici, nous utilisons httpApi pour exposer nos fonctions via API Gateway. Chaque événement spécifie le path et la method HTTP.
  • resources : Section pour définir des ressources AWS supplémentaires que Serverless Framework doit créer ou gérer. Ici, nous définissons notre table DynamoDB ProductsTable avec id comme clé primaire.

3.2 Implémentation des Fonctions Lambda (Node.js)

Créons le fichier handler.js (ou handler.py si Python) qui contiendra notre logique métier. Nous allons implémenter les opérations CRUD pour les produits.

// handler.js
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand, GetCommand, UpdateCommand, DeleteCommand, ScanCommand } = require("@aws-sdk/lib-dynamodb");
const { v4: uuidv4 } = require('uuid');

// Initialisation du client DynamoDB
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

const PRODUCTS_TABLE_NAME = process.env.PRODUCTS_TABLE_NAME; // Nom de la table injecté via variable d'environnement

// Fonction utilitaire pour envoyer des réponses HTTP
const buildResponse = (statusCode, body) => {
    return {
        statusCode: statusCode,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*', // Autoriser les requêtes depuis n'importe quelle origine pour le développement
            'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
            'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
        },
        body: JSON.stringify(body),
    };
};

/**
 * Crée un nouveau produit.
 * Méthode: POST /products
 */
module.exports.createProduct = async (event) => {
    try {
        const data = JSON.parse(event.body);
        const productId = uuidv4();
        const product = { id: productId, ...data, createdAt: new Date().toISOString() };

        const command = new PutCommand({
            TableName: PRODUCTS_TABLE_NAME,
            Item: product
        });

        await docClient.send(command);

        return buildResponse(201, { message: 'Produit créé avec succès', product });
    } catch (error) {
        console.error("Erreur lors de la création du produit:", error);
        return buildResponse(500, { message: 'Erreur interne du serveur', error: error.message });
    }
};

/**
 * Récupère tous les produits.
 * Méthode: GET /products
 */
module.exports.getProducts = async (event) => {
    try {
        const command = new ScanCommand({
            TableName: PRODUCTS_TABLE_NAME,
        });

        const { Items } = await docClient.send(command);

        return buildResponse(200, Items);
    } catch (error) {
        console.error("Erreur lors de la récupération des produits:", error);
        return buildResponse(500, { message: 'Erreur interne du serveur', error: error.message });
    }
};

/**
 * Récupère un produit par ID.
 * Méthode: GET /products/{id}
 */
module.exports.getProduct = async (event) => {
    try {
        const { id } = event.pathParameters;

        const command = new GetCommand({
            TableName: PRODUCTS_TABLE_NAME,
            Key: { id }
        });

        const { Item } = await docClient.send(command);

        if (!Item) {
            return buildResponse(404, { message: 'Produit non trouvé' });
        }

        return buildResponse(200, Item);
    } catch (error) {
        console.error("Erreur lors de la récupération du produit:", error);
        return buildResponse(500, { message: 'Erreur interne du serveur', error: error.message });
    }
};

/**
 * Met à jour un produit existant.
 * Méthode: PUT /products/{id}
 */
module.exports.updateProduct = async (event) => {
    try {
        const { id } = event.pathParameters;
        const data = JSON.parse(event.body);

        let updateExpression = 'set ';
        const expressionAttributeValues = {};
        const expressionAttributeNames = {};
        let first = true;

        for (const key in data) {
            if (key !== 'id' && data.hasOwnProperty(key)) {
                if (!first) updateExpression += ', ';
                const attrName = `#${key}`;
                const attrValue = `:${key}`;
                updateExpression += `${attrName} = ${attrValue}`;
                expressionAttributeNames[attrName] = key;
                expressionAttributeValues[attrValue] = data[key];
                first = false;
            }
        }
        updateExpression += ', #updatedAt = :updatedAt';
        expressionAttributeNames['#updatedAt'] = 'updatedAt';
        expressionAttributeValues[':updatedAt'] = new Date().toISOString();

        const command = new UpdateCommand({
            TableName: PRODUCTS_TABLE_NAME,
            Key: { id },
            UpdateExpression: updateExpression,
            ExpressionAttributeNames: expressionAttributeNames,
            ExpressionAttributeValues: expressionAttributeValues,
            ReturnValues: 'ALL_NEW' // Retourne l'élément mis à jour
        });

        const { Attributes } = await docClient.send(command);

        if (!Attributes) {
            return buildResponse(404, { message: 'Produit non trouvé' });
        }

        return buildResponse(200, { message: 'Produit mis à jour avec succès', product: Attributes });
    } catch (error) {
        console.error("Erreur lors de la mise à jour du produit:", error);
        return buildResponse(500, { message: 'Erreur interne du serveur', error: error.message });
    }
};

/**
 * Supprime un produit.
 * Méthode: DELETE /products/{id}
 */
module.exports.deleteProduct = async (event) => {
    try {
        const { id } = event.pathParameters;

        const command = new DeleteCommand({
            TableName: PRODUCTS_TABLE_NAME,
            Key: { id },
            ReturnValues: 'ALL_OLD' // Retourne l'élément avant suppression
        });

        const { Attributes } = await docClient.send(command);

        if (!Attributes) {
            return buildResponse(404, { message: 'Produit non trouvé' });
        }

        return buildResponse(200, { message: 'Produit supprimé avec succès', product: Attributes });
    } catch (error) {
        console.error("Erreur lors de la suppression du produit:", error);
        return buildResponse(500, { message: 'Erreur interne du serveur', error: error.message });
    }
};

Explication du code Lambda :

  • Initialisation: On importe les modules nécessaires des SDK AWS pour DynamoDB et uuid pour générer des IDs uniques.
  • buildResponse: Une fonction utilitaire pour formater les réponses HTTP, y compris les headers CORS (Access-Control-Allow-Origin) qui sont essentiels pour permettre à votre frontend de communiquer avec votre API.
  • Fonctions CRUD (createProduct, getProducts, etc.):
    • Chaque fonction est exportée pour être accessible par Lambda.
    • Elles reçoivent un objet event contenant toutes les informations de la requête HTTP (corps, paramètres de chemin, etc.).
    • Elles utilisent DynamoDBDocumentClient (qui simplifie les interactions avec DynamoDB en gérant la sérialisation/désérialisation) pour effectuer les opérations sur la table PRODUCTS_TABLE_NAME.
    • Les erreurs sont capturées et renvoient une réponse 500.
  • Variable d'environnement PRODUCTS_TABLE_NAME: Le nom de la table DynamoDB est passé à la fonction Lambda via une variable d'environnement, définie dans serverless.yml. C'est une bonne pratique pour rendre votre code plus adaptable.

N'oubliez pas d'installer les dépendances nécessaires dans votre projet :

npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb uuid

4. Étape 3 : Déploiement du Backend

Une fois le serverless.yml et les fonctions Lambda définies, le déploiement est un jeu d'enfant avec Serverless Framework.

# Déploiement initial de votre service
sls deploy --verbose

Cette commande va :

  1. Compresser votre code et vos dépendances.
  2. Créer un stack CloudFormation sur AWS.
  3. Déployer vos fonctions Lambda.
  4. Configurer API Gateway, les rôles IAM, et la table DynamoDB.
  5. Afficher les endpoints de votre API.

Prenez note des URLs de votre API Gateway. Elles ressembleront à https://xxxxxxxxx.execute-api.eu-west-3.amazonaws.com/dev/products.

Vous pouvez tester vos endpoints avec un outil comme curl ou Postman :

# Exemple de création de produit (remplacez l'URL)
curl -X POST -H "Content-Type: application/json" -d '{"name": "Laptop XPS 15", "price": 1800, "description": "Un ordinateur portable puissant."}' https://xxxxxxxxx.execute-api.eu-west-3.amazonaws.com/dev/products

# Exemple de récupération de tous les produits
curl https://xxxxxxxxx.execute-api.eu-west-3.amazonaws.com/dev/products

5. Étape 4 : Authentification et Autorisation (Amazon Cognito)

Pour une application complète, l'authentification est cruciale. Amazon Cognito User Pools permet de gérer les utilisateurs et de fournir des jetons d'authentification (JWT) que notre API Gateway peut valider.

5.1 Configuration de Cognito dans serverless.yml

Nous allons ajouter un User Pool et un User Pool Client.

# ... (dans serverless.yml, après la section 'functions' et avant 'resources')
resources:
  Resources:
    ProductsTable:
      # ... (définition de la table DynamoDB)

    # Ajout du Cognito User Pool
    UserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: ProductsUserPool-${sls:stage}
        Schema:
          - Name: email
            Required: true
            Mutable: true
          - Name: name
            Required: false
            Mutable: true
        AutoVerifiedAttributes:
          - email
        MfaConfiguration: OFF # Pour simplifier, mais Activez-le en production
        Policies:
          PasswordPolicy:
            MinimumLength: 8
        UsernameAttributes:
          - email

    # Ajout du Cognito User Pool Client
    UserPoolClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: WebAppClient-${sls:stage}
        UserPoolId: !Ref UserPool # Référence au UserPool créé ci-dessus
        ExplicitAuthFlows:
          - ADMIN_NO_SRP_AUTH # Utiliser pour les tests simples, déconseillé en production pour les apps publiques
        GenerateSecret: false # Pour les applications côté client
        AllowedOAuthFlowsUserPoolClient: true
        SupportedIdentityProviders:
          - COGNITO
        CallbackURLs:
          - 'http://localhost:3000' # Pour le développement local du frontend
        LogoutURLs:
          - 'http://localhost:3000'
        AllowedOAuthFlows:
          - implicit
        AllowedOAuthScopes:
          - openid
          - email
          - profile

    # Ajout de l'Authorizer pour API Gateway
    ApiGatewayCognitoAuthorizer:
      Type: AWS::ApiGateway::Authorizer
      Properties:
        Name: CognitoAuthorizer
        Type: COGNITO_USER_POOLS
        IdentitySource: method.request.header.Authorization
        RestApiId: !GetAtt ApiGatewayRestApi.RestApiId
        ProviderARNs:
          - !GetAtt UserPool.Arn

5.2 Protection des Routes API Gateway

Maintenant, nous pouvons protéger nos fonctions Lambda en ajoutant l'authorizer Cognito à leurs événements HTTP.

# ... (dans serverless.yml, section 'functions')

functions:
  createProduct:
    handler: handler.createProduct
    events:
      - httpApi:
          path: /products
          method: post
          authorizer: # Ajout de l'authorizer
            type: cognito_user_pools
            pools:
              - !Ref UserPool # Référence à notre User Pool
  getProducts:
    handler: handler.getProducts
    events:
      - httpApi:
          path: /products
          method: get
          authorizer: # Protéger la lecture aussi
            type: cognito_user_pools
            pools:
              - !Ref UserPool
  # ... (Faites de même pour getProduct, updateProduct, deleteProduct)

Redéployez votre service après ces modifications : sls deploy.

Maintenant, toutes les requêtes à ces endpoints nécessiteront un jeton JWT valide fourni par Cognito.

6. Étape 5 : Intégration du Frontend

Le frontend sera une application web statique, généralement développée avec un framework comme React, Vue ou Angular. Elle sera déployée sur Amazon S3 et distribuée via CloudFront.

6.1 Développement du Frontend

Créez une application React (par exemple) et ajoutez un code simple pour interagir avec l'API.

# Créer une application React
npx create-react-app product-catalog-frontend
cd product-catalog-frontend
npm install aws-amplify # Pour faciliter l'interaction avec Cognito et API Gateway

Exemple de code React (src/App.js)

// src/App.js
import React, { useState, useEffect } from 'react';
import Amplify, { API, Auth } from 'aws-amplify';

// Configuration d'Amplify avec les détails de votre API Gateway et Cognito User Pool
Amplify.configure({
    Auth: {
        region: 'eu-west-3', // Votre région AWS
        userPoolId: 'eu-west-3_xxxxxxxxx', // L'ID de votre User Pool Cognito après déploiement
        userPoolWebClientId: 'yyyyyyyyyyyy', // L'ID de votre User Pool Client Cognito après déploiement
        authenticationFlowType: 'USER_PASSWORD_AUTH'
    },
    API: {
        endpoints: [
            {
                name: "productApi",
                endpoint: "https://xxxxxxxxx.execute-api.eu-west-3.amazonaws.com/dev", // L'URL de base de votre API Gateway
                custom_headers: async () => {
                    try {
                        return { Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}` }
                    } catch (e) {
                        return {}
                    }
                }
            }
        ]
    }
});

function App() {
    const [products, setProducts] = useState([]);
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');

    useEffect(() => {
        checkAuth();
    }, []);

    async function checkAuth() {
        try {
            await Auth.currentAuthenticatedUser();
            setIsAuthenticated(true);
            fetchProducts();
        } catch (error) {
            setIsAuthenticated(false);
            console.log('Not authenticated', error);
        }
    }

    async function handleLogin() {
        try {
            await Auth.signIn(username, password);
            setIsAuthenticated(true);
            fetchProducts();
        } catch (error) {
            console.error('Erreur de connexion:', error);
            alert(`Erreur de connexion: ${error.message}`);
        }
    }

    async function handleLogout() {
        try {
            await Auth.signOut();
            setIsAuthenticated(false);
            setProducts([]);
        } catch (error) {
            console.error('Erreur de déconnexion:', error);
        }
    }

    async function fetchProducts() {
        try {
            const data = await API.get('productApi', '/products');
            setProducts(data);
        } catch (error) {
            console.error('Erreur lors de la récupération des produits:', error);
            // Gérer les erreurs d'autorisation ici, par exemple en redirigeant vers la page de connexion
            if (error.response && error.response.status === 401) {
                alert("Session expirée ou non autorisée. Veuillez vous reconnecter.");
                setIsAuthenticated(false);
            }
        }
    }

    async function addProduct() {
        try {
            const newProduct = { name: `Product ${products.length + 1}`, price: 100 + products.length };
            await API.post('productApi', '/products', { body: newProduct });
            fetchProducts(); // Rafraîchir la liste
        } catch (error) {
            console.error('Erreur lors de l\'ajout du produit:', error);
        }
    }

    if (!isAuthenticated) {
        return (
            <div>
                <h1>Connexion</h1>
                <input type="email" placeholder="Email" value={username} onChange={(e) => setUsername(e.target.value)} />
                <input type="password" placeholder="Mot de passe" value={password} onChange={(e) => setPassword(e.target.value)} />
                <button onClick={handleLogin}>Se connecter</button>
            </div>
        );
    }

    return (
        <div className="App">
            <h1>Mon Catalogue de Produits Serverless</h1>
            <button onClick={handleLogout}>Déconnexion</button>
            <button onClick={addProduct}>Ajouter un produit</button>
            <ul>
                {products.map((product) => (
                    <li key={product.id}>
                        {product.name} - ${product.price} (ID: {product.id})
                    </li>
                ))}
            </ul>
        </div>
    );
}

export default App;

Important: Vous devrez remplacer les placeholders xxxxxxxxx et yyyyyyyyyyyy avec les valeurs réelles de votre API Gateway Endpoint, Cognito User Pool ID et Client ID après le déploiement de votre backend. Ces informations sont disponibles dans la sortie du sls deploy.

Pour créer un utilisateur dans Cognito (pour les tests) : allez dans la console AWS, Cognito, votre User Pool, Utilisateurs et groupes, puis "Créer un utilisateur".

6.2 Déploiement du Frontend Statique

Pour déployer le frontend, nous allons ajouter des ressources S3 et CloudFront à notre serverless.yml.

# ... (dans serverless.yml, après 'resources')

resources:
  Resources:
    # ... (Vos ressources DynamoDB et Cognito)

    # S3 Bucket pour le frontend
    FrontendBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: product-catalog-frontend-${sls:stage} # Nom unique du bucket
        AccessControl: PublicRead # Permettre la lecture publique
        WebsiteConfiguration:
          IndexDocument: index.html
          ErrorDocument: index.html # Pour les applications SPA (Single Page Application)
    
    # Bucket Policy pour le frontend (permet l'accès public)
    FrontendBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref FrontendBucket
        PolicyDocument:
          Statement:
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              Action: "s3:GetObject"
              Resource: !Join ["", ["arn:aws:s3:::", !Ref FrontendBucket, "/*"]]

    # CloudFront Distribution pour servir le frontend et l'API
    CloudFrontDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Origins:
            - Id: S3Origin
              DomainName: !GetAtt FrontendBucket.RegionalDomainName # Point vers le S3 bucket
              S3OriginConfig: {} # Pas d'identité d'accès d'origine pour les buckets publics
            - Id: ApiGatewayOrigin
              DomainName: !Join [".", [!GetAtt ApiGatewayRestApi.ApiId, "execute-api", ${self:provider.region}, "amazonaws.com"]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
                OriginSSLProtocols:
                  - TLSv1.2
          Enabled: 'true'
          DefaultRootObject: index.html
          DefaultCacheBehavior:
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods:
              - GET
              - HEAD
            CachedMethods:
              - GET
              - HEAD
            ForwardedValues:
              QueryString: 'false'
              Cookies:
                Forward: none
          # Comportement de cache pour l'API Gateway
          CacheBehaviors:
            - PathPattern: /${self:provider.stage}/* # Assurez-vous que cela correspond à votre chemin d'API Gateway
              TargetOriginId: ApiGatewayOrigin
              ViewerProtocolPolicy: redirect-to-https
              AllowedMethods:
                - GET
                - HEAD
                - OPTIONS
                - POST
                - PUT
                - PATCH
                - DELETE
              CachedMethods:
                - GET
                - HEAD
                - OPTIONS
              ForwardedValues:
                QueryString: 'true'
                Headers: # Important: transférer les headers d'autorisation
                  - Authorization
                Cookies:
                  Forward: none
          ViewerCertificate:
            CloudFrontDefaultCertificate: 'true'
          # Ajoutez un CNAME si vous avez un domaine personnalisé
          # Aliases:
          #   - myapp.mydomain.com
  
  # Ajoutez cette section "Outputs" pour récupérer l'URL CloudFront
outputs:
  FrontendUrl:
    Description: URL du Frontend hébergé sur CloudFront
    Value: !GetAtt CloudFrontDistribution.DomainName
  ApiGatewayEndpoint:
    Description: URL de l'API Gateway
    Value: !Sub 'https://${ApiGatewayRestApi}.execute-api.${self:provider.region}.amazonaws.com/${self:provider.stage}'
  CognitoUserPoolId:
    Description: ID du User Pool Cognito
    Value: !Ref UserPool
  CognitoUserPoolClientId:
    Description: ID du User Pool Client Cognito
    Value: !Ref UserPoolClient

Une fois le backend déployé avec la nouvelle configuration de CloudFront, construisez et déployez votre application frontend.

# Dans le dossier product-catalog-frontend
npm run build

# Pour copier les fichiers de build vers le bucket S3
# Assurez-vous que l'AWS CLI est configuré avec les permissions nécessaires (s3:PutObject)
aws s3 sync build/ s3://product-catalog-frontend-dev --delete

L'URL de votre application frontend sera l'URL de votre distribution CloudFront, disponible dans la sortie de sls deploy sous FrontendUrl.

7. Étape 6 : Déploiement Continu (CI/CD)

Pour un flux de travail professionnel, la mise en place d'une pipeline CI/CD est essentielle. GitHub Actions est un excellent choix pour les projets serverless.

7.1 Exemple de Pipeline GitHub Actions

Créez un fichier .github/workflows/deploy.yml dans la racine de votre projet backend :

# .github/workflows/deploy.yml
name: Serverless CI/CD

on:
  push:
    branches:
      - main # Déclencher un déploiement sur les pushes vers la branche main

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

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

      - name: Install Serverless Framework
        run: npm install -g serverless

      - name: Install dependencies
        run: npm install # Installe les dépendances de votre projet Lambda

      - name: Deploy Serverless service
        run: sls deploy --stage prod # Déployer vers l'environnement de production
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: eu-west-3 # Votre région

Explication :

  • Ce workflow se déclenche sur chaque push vers la branche main.
  • Il configure Node.js, installe le Serverless Framework et les dépendances du projet.
  • Enfin, il exécute sls deploy, utilisant les secrets GitHub pour vos informations d'identification AWS. Attention : Ne stockez jamais vos clés AWS directement dans votre code. Utilisez toujours les secrets de votre fournisseur CI/CD.

8. Étape 7 : Monitoring et Observabilité

Une fois l'application en production, il est crucial de la surveiller pour détecter les erreurs, les performances et l'utilisation.

  • Amazon CloudWatch : Collecte automatiquement les logs de vos fonctions Lambda et API Gateway. Vous pouvez y créer des tableaux de bord et des alertes.
  • AWS X-Ray : Permet de tracer les requêtes à travers les différents services (API Gateway, Lambda, DynamoDB) pour identifier les goulots d'étranglement et les erreurs. Vous devez instrumenter vos fonctions Lambda pour utiliser X-Ray.
  • Serverless Framework Dashboard : Offre des fonctionnalités de monitoring et de gestion de service intégrées.

Pour activer X-Ray pour vos fonctions Lambda, ajoutez simplement tracing: Active sous provider dans votre serverless.yml :

# ...
provider:
  name: aws
  runtime: nodejs18.x
  region: eu-west-3
  stage: dev
  tracing:
    lambda: true # Active le tracing X-Ray pour toutes les fonctions Lambda
# ...

Redéployez pour que ces changements prennent effet.

Conclusion : L'Application Serverless, Complète et Fonctionnelle

Félicitations ! Vous avez parcouru toutes les étapes de la construction d'une application serverless complète, de la conception à l'implémentation, en passant par le déploiement et la surveillance. Ce projet met en évidence les points clés des architectures serverless :

  • Faible Coût Opérationnel : Vous vous concentrez sur le code, AWS gère l'infrastructure.
  • Scalabilité Automatique : Votre application s'adapte automatiquement à la charge, sans intervention manuelle.
  • Modèle de Paiement à l'Usage : Vous ne payez que pour les ressources consommées, ce qui est très économique pour les charges de travail fluctuantes.
  • Rapidité de Développement : Le Serverless Framework et les services managés accélèrent considérablement le cycle de développement.

Ce projet n'est qu'un point de départ. Vous pouvez l'étendre avec des fonctionnalités plus avancées telles que :

  • Validation des schémas d'entrée avec API Gateway.
  • Gestion des images avec S3 et Lambda (pour le redimensionnement, par exemple).
  • Recherche avancée avec Amazon OpenSearch Service (Elasticsearch).
  • Notification d'événements avec SNS/SQS.

Continuez à explorer et à construire, car le serverless est un domaine en constante évolution et offre des opportunités illimitées pour créer des applications modernes et performantes.