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 :
- L'utilisateur accède à l'application via CloudFront, qui distribue les fichiers du frontend depuis S3.
- Les requêtes API du frontend sont également routées via CloudFront vers API Gateway.
- API Gateway reçoit les requêtes, les valide (éventuellement via Cognito pour l'authentification) et les transmet aux fonctions Lambda appropriées.
- 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.
- CloudWatch collecte les logs et métriques de tous les services pour la surveillance et le débogage.
- 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.createProductsignifie la fonctioncreateProductdanshandler.js).events: Déclencheurs pour la fonction. Ici, nous utilisonshttpApipour exposer nos fonctions via API Gateway. Chaque événement spécifie lepathet lamethodHTTP.
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 DynamoDBProductsTableavecidcomme 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
uuidpour 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
eventcontenant 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 tablePRODUCTS_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 dansserverless.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 :
- Compresser votre code et vos dépendances.
- Créer un stack CloudFormation sur AWS.
- Déployer vos fonctions Lambda.
- Configurer API Gateway, les rôles IAM, et la table DynamoDB.
- 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
pushvers la branchemain. - 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.