Construisez une API REST serverless avec AWS Lambda et Node.js TypeScript : API Gateway, déploiement SAM, cold start, monitoring CloudWatch, IAM.
Architecture serverless avec Lambda
AWS Lambda exécute du code à la demande, sans serveur à provisionner ni mettre à jour. Vous facturez uniquement le temps de calcul effectivement consommé, à la milliseconde. Combiné à API Gateway, Lambda devient le moteur d'une API REST fully managed capable de gérer des pics de charge instantanément, de zéro à plusieurs milliers de requêtes par seconde.
Une API serverless typique repose sur quatre briques AWS : API Gateway (entrée HTTP), Lambda (logique métier), un stockage d'état (DynamoDB, RDS Proxy ou S3) et CloudWatch (logs et métriques). Le tout déclaré en infrastructure-as-code via AWS SAM (Serverless Application Model) ou Terraform.
Schéma logique
Client HTTP
│
▼
API Gateway (HTTPS, throttling, auth)
│
▼
Lambda Function (Node.js 20, TypeScript)
│
├──> DynamoDB / RDS Proxy
├──> Secrets Manager (clés API, credentials)
└──> CloudWatch Logs + X-Ray traces
Lambda vs Cloud Run vs Azure Functions
| Critère | AWS Lambda | GCP Cloud Run | Azure Functions |
|---|---|---|---|
| Modèle d'exécution | Function-as-a-Service | Container-as-a-Service | Function-as-a-Service |
| Free tier mensuel | 1 M requêtes + 400k GB-s | 2 M requêtes + 360k GB-s | 1 M requêtes + 400k GB-s |
| Prix au-delà / 1 M req | 0,20 $ | 0,40 $ | 0,20 $ |
| Cold start Node.js 20 | 100-300 ms | 200-800 ms | 200-500 ms |
| Durée max d'exécution | 15 minutes | 60 minutes | 10 min (consumption) |
| Mémoire max | 10 240 MB | 32 GB | 14 GB (premium) |
| Provisioned concurrency | Oui (Provisioned Concurrency) | Oui (min instances) | Oui (Premium plan) |
| IaC officiel | SAM, CDK, Terraform | gcloud, Terraform | Bicep, Terraform |
Setup AWS CLI et AWS SAM
AWS SAM (Serverless Application Model) est l'outil officiel pour développer et déployer des applications Lambda. Il étend CloudFormation avec une syntaxe simplifiée pour les ressources serverless, et propose un environnement de développement local (sam local invoke, sam local start-api) qui simule API Gateway et Lambda en Docker.
Installation des outils CLI
# 1. 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
# Sur macOS via Homebrew
brew install awscli
# Sur Windows : télécharger AWSCLIV2.msi sur aws.amazon.com/cli
# 2. Installer AWS SAM CLI
brew tap aws/tap
brew install aws-sam-cli
# Vérifier les versions
aws --version # aws-cli/2.17+
sam --version # SAM CLI 1.120+
node --version # v20.x recommandé
docker --version # requis pour sam local
Configurer les credentials AWS
# Créer un utilisateur IAM dédié au déploiement (console AWS)
# Permissions : AdministratorAccess pour le bootstrap, à restreindre ensuite
aws configure --profile angularforall-dev
# AWS Access Key ID : AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key : wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region : eu-west-3
# Default output format : json
# Vérifier l'identité utilisée
aws sts get-caller-identity --profile angularforall-dev
Initialiser un projet SAM TypeScript
# Créer un projet vierge avec template TypeScript
sam init \
--runtime nodejs20.x \
--name afa-lambda-api \
--app-template hello-world-typescript \
--package-type Zip
cd afa-lambda-api
# Structure générée
.
├── README.md
├── events/ # payloads de test
│ └── event.json
├── hello-world/ # source TypeScript
│ ├── app.ts # handler
│ ├── package.json
│ ├── tsconfig.json
│ └── tests/
│ └── unit/test-handler.test.ts
├── samconfig.toml # config sam deploy
└── template.yaml # IaC : Lambda + API Gateway
hello-world-typescript utilise esbuild en backend de build (déclaré dans Metadata.BuildMethod du template.yaml). esbuild est 50x plus rapide que tsc et produit un bundle minifié optimisé pour le cold start.
Première fonction Lambda TypeScript
Une fonction Lambda Node.js exporte un handler qui reçoit un event et un context, et retourne une réponse. Avec API Gateway en proxy, l'event est typé APIGatewayProxyEventV2 (HTTP API) ou APIGatewayProxyEvent (REST API). Les types sont fournis par @types/aws-lambda.
package.json minimal
{
"name": "afa-lambda-api",
"version": "1.0.0",
"description": "API serverless AngularForAll",
"main": "app.js",
"scripts": {
"build": "esbuild app.ts --bundle --minify --platform=node --target=node20 --outdir=dist",
"test": "jest",
"lint": "eslint . --ext .ts"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.620.0",
"@aws-sdk/lib-dynamodb": "^3.620.0",
"@aws-sdk/client-secrets-manager": "^3.620.0"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.140",
"@types/node": "^20.14.0",
"esbuild": "^0.23.0",
"typescript": "^5.5.0",
"jest": "^29.7.0"
}
}
tsconfig.json strict
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": ".",
"lib": ["ES2022"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}
Handler typé pour HTTP API
// hello-world/app.ts
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
Context,
} from 'aws-lambda';
interface ResponseBody {
message: string;
requestId: string;
path: string;
timestamp: string;
}
// Headers communs réutilisés sur toutes les réponses
const COMMON_HEADERS = {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store',
'X-Frame-Options': 'DENY',
};
export const handler = async (
event: APIGatewayProxyEventV2,
context: Context,
): Promise<APIGatewayProxyResultV2> => {
// Logs structurés JSON — parsables par CloudWatch Insights
console.log(JSON.stringify({
level: 'info',
requestId: context.awsRequestId,
method: event.requestContext.http.method,
path: event.rawPath,
}));
// Récupération paramètres path et query string
const name = event.queryStringParameters?.name ?? 'monde';
const body: ResponseBody = {
message: `Bonjour ${name}, depuis Lambda Node.js 20`,
requestId: context.awsRequestId,
path: event.rawPath,
timestamp: new Date().toISOString(),
};
return {
statusCode: 200,
headers: COMMON_HEADERS,
body: JSON.stringify(body),
};
};
Test local avec sam local invoke
# Build du bundle TypeScript
sam build
# Invoquer la fonction avec un event simulé
sam local invoke HelloWorldFunction --event events/event.json
# Démarrer une API locale sur http://127.0.0.1:3000
sam local start-api
# Dans un autre terminal
curl "http://127.0.0.1:3000/hello?name=Said"
# {"message":"Bonjour Said, depuis Lambda Node.js 20",...}
3000 peut être changé avec --port.
API Gateway : routes HTTP et REST
AWS propose deux saveurs d'API Gateway : HTTP API (rapide, moins cher, recommandé pour la majorité des cas) et REST API (plus de fonctionnalités : caching, request validation, usage plans, AWS WAF natif). Pour une API CRUD classique, HTTP API suffit largement et coûte ~70 % moins cher (1 $/M req contre 3,50 $/M req).
template.yaml — HTTP API multi-routes
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AngularForAll - API serverless multi-routes
Globals:
Function:
Runtime: nodejs20.x
Architectures: [arm64] # 20% moins cher que x86_64
MemorySize: 256 # MB - sweet spot prix/perf
Timeout: 10 # secondes
LoggingConfig:
LogFormat: JSON
Environment:
Variables:
NODE_OPTIONS: '--enable-source-maps'
STAGE: !Ref Stage
TABLE_NAME: !Ref ItemsTable
Parameters:
Stage:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
Resources:
# API Gateway HTTP API (v2) — la plus économique
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
StageName: !Ref Stage
CorsConfiguration:
AllowOrigins:
- https://angularforall.com
- http://localhost:4200
AllowMethods: [GET, POST, PUT, DELETE, OPTIONS]
AllowHeaders: [Content-Type, Authorization]
MaxAge: 600
# GET /items
ListItemsFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: node20
Sourcemap: true
EntryPoints: [src/handlers/list-items.ts]
External: ['@aws-sdk/*'] # déjà dans le runtime, ne pas bundler
Properties:
CodeUri: .
Handler: src/handlers/list-items.handler
Events:
Api:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Path: /items
Method: GET
# POST /items
CreateItemFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
EntryPoints: [src/handlers/create-item.ts]
Properties:
CodeUri: .
Handler: src/handlers/create-item.handler
Events:
Api:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Path: /items
Method: POST
# DynamoDB on-demand
ItemsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub 'afa-items-${Stage}'
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
Outputs:
ApiEndpoint:
Description: URL publique de l'API
Value: !Sub 'https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}'
Handler CRUD avec DynamoDB
// src/handlers/list-items.ts
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
} from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
// Init HORS du handler — réutilisé entre invocations chaudes
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.TABLE_NAME!;
interface Item {
id: string;
title: string;
createdAt: string;
}
export const handler = async (
_event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> => {
try {
const result = await docClient.send(new ScanCommand({
TableName: TABLE_NAME,
Limit: 100,
}));
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: (result.Items ?? []) as Item[],
count: result.Count ?? 0,
}),
};
} catch (err) {
console.error('list-items error', err);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};
Validation et parsing du body POST
// src/handlers/create-item.ts
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
} from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { randomUUID } from 'crypto';
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE_NAME = process.env.TABLE_NAME!;
interface CreateItemPayload {
title: string;
description?: string;
}
// Validation simple sans dépendance externe
function validate(payload: unknown): payload is CreateItemPayload {
if (typeof payload !== 'object' || payload === null) return false;
const p = payload as Record<string, unknown>;
return typeof p.title === 'string' && p.title.length > 0 && p.title.length <= 200;
}
export const handler = async (
event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> => {
if (!event.body) {
return { statusCode: 400, body: JSON.stringify({ error: 'Body required' }) };
}
let payload: unknown;
try {
payload = JSON.parse(event.body);
} catch {
return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON' }) };
}
if (!validate(payload)) {
return { statusCode: 422, body: JSON.stringify({ error: 'Invalid payload' }) };
}
const item = {
id: randomUUID(),
title: payload.title,
description: payload.description ?? '',
createdAt: new Date().toISOString(),
};
await docClient.send(new PutCommand({
TableName: TABLE_NAME,
Item: item,
ConditionExpression: 'attribute_not_exists(id)',
}));
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json', Location: `/items/${item.id}` },
body: JSON.stringify(item),
};
};
DynamoDBClient et DynamoDBDocumentClient en module scope (hors du handler) permet la réutilisation entre invocations chaudes. C'est la différence entre 5 ms et 80 ms par requête.
IAM least-privilege et Secrets Manager
Chaque fonction Lambda assume un rôle IAM. Par défaut, SAM en crée un avec AWSLambdaBasicExecutionRole (logs CloudWatch). Pour accéder à DynamoDB, S3, Secrets Manager ou tout autre service, il faut ajouter explicitement les permissions — au plus juste, jamais * sur Resource ni Action.
Politiques IAM granulaires dans template.yaml
CreateItemFunction:
Type: AWS::Serverless::Function
Properties:
# ... build et code config ...
Policies:
# Pattern SAM : politiques pré-définies par service
- DynamoDBCrudPolicy:
TableName: !Ref ItemsTable
- SecretsManagerReadWrite:
SecretArn: !Ref ApiKeysSecret
# Politique custom plus stricte
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource: !GetAtt ItemsTable.Arn
Condition:
StringEquals:
# Limite l'accès aux items du tenant courant uniquement
'dynamodb:LeadingKeys':
- '${aws:PrincipalTag/tenantId}'
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Ref ApiKeysSecret
Condition:
StringEquals:
'aws:RequestedRegion': eu-west-3
Variables d'environnement non sensibles
Globals:
Function:
Environment:
Variables:
# Valeurs publiques OK en clair
STAGE: !Ref Stage
LOG_LEVEL: info
TABLE_NAME: !Ref ItemsTable
REGION: !Ref AWS::Region
# ⚠️ JAMAIS de secret en variable d'environnement claire
# API_KEY: 'sk_live_xxx' ❌ INTERDIT
Stocker un secret dans Secrets Manager
# Créer le secret via CLI (rotation automatique disponible)
aws secretsmanager create-secret \
--name afa/prod/stripe-api-key \
--description "Clé API Stripe production AngularForAll" \
--secret-string '{"apiKey":"sk_live_xxx","webhookSecret":"whsec_yyy"}' \
--region eu-west-3
# Référencer dans template.yaml
ApiKeysSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub 'afa/${Stage}/stripe-api-key'
Description: Credentials Stripe
GenerateSecretString:
SecretStringTemplate: '{"apiKey":""}'
GenerateStringKey: webhookSecret
PasswordLength: 32
ExcludeCharacters: '"@/\\'
Lire le secret depuis Lambda avec cache
// src/lib/secrets.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({});
interface StripeSecret {
apiKey: string;
webhookSecret: string;
}
// Cache en mémoire — vit le temps du conteneur Lambda chaud (15-45 min)
let cached: { value: StripeSecret; expires: number } | null = null;
const TTL_MS = 5 * 60 * 1000; // 5 minutes
export async function getStripeSecret(): Promise<StripeSecret> {
if (cached && cached.expires > Date.now()) {
return cached.value; // hit cache — pas d'appel AWS
}
const secretId = process.env.STRIPE_SECRET_ARN!;
const result = await client.send(new GetSecretValueCommand({ SecretId: secretId }));
if (!result.SecretString) {
throw new Error('Secret string vide');
}
const value = JSON.parse(result.SecretString) as StripeSecret;
cached = { value, expires: Date.now() + TTL_MS };
return value;
}
Lambda Layer pour code partagé
# Architecture : src/layers/common/nodejs/
# Lambda monte le layer sous /opt/nodejs/node_modules
# Tous les handlers peuvent require ces modules sans les bundler
CommonLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: !Sub 'afa-common-${Stage}'
Description: Logger, secrets cache, validators partagés
ContentUri: src/layers/common/
CompatibleRuntimes: [nodejs20.x]
CompatibleArchitectures: [arm64]
RetentionPolicy: Retain # garde les anciennes versions
# Utilisation dans un handler
ListItemsFunction:
Type: AWS::Serverless::Function
Properties:
Layers:
- !Ref CommonLayer
- Aucun rôle Lambda avec
"Resource": "*"sauf logs CloudWatch - Secrets stockés dans Secrets Manager ou SSM Parameter Store, jamais en variable d'env claire
- Politiques SAM pré-définies (
DynamoDBCrudPolicy,S3ReadPolicy) — préférées aux custom policies - Conditions IAM restrictives (région, tenant, IP source) sur les actions sensibles
- CloudTrail activé sur toutes les régions pour audit des appels IAM
Déploiement, monitoring et alarmes
sam deploy --guided orchestre l'upload du code, la création du stack CloudFormation et la configuration d'API Gateway en une commande. Pour les environnements suivants (staging, prod), samconfig.toml conserve les paramètres et sam deploy seul suffit.
Premier déploiement guidé
# Build optimisé production
sam build
# Premier déploiement (création du bucket S3 d'artefacts)
sam deploy --guided
# Réponses recommandées :
# Stack Name [sam-app]: afa-lambda-api-dev
# AWS Region [eu-west-3]: eu-west-3
# Parameter Stage [dev]: dev
# Confirm changes before deploy: Y
# Allow SAM CLI IAM role creation: Y
# Disable rollback: N
# HelloWorldFunction may not have authorization defined: y
# Save arguments to configuration file: Y
# SAM configuration file [samconfig.toml]: samconfig.toml
# SAM configuration environment [default]: dev
# Déploiement suivant — un seul command
sam deploy --config-env dev
# Pour staging/prod
sam deploy --config-env prod --parameter-overrides Stage=prod
samconfig.toml multi-environnement
version = 0.1
[default.global.parameters]
stack_name = "afa-lambda-api"
[dev.deploy.parameters]
stack_name = "afa-lambda-api-dev"
region = "eu-west-3"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=\"dev\""
resolve_s3 = true
[prod.deploy.parameters]
stack_name = "afa-lambda-api-prod"
region = "eu-west-3"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=\"prod\""
confirm_changeset = true # validation manuelle avant chaque deploy prod
resolve_s3 = true
Logs structurés et CloudWatch Insights
// src/lib/logger.ts — logger JSON minimaliste
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogContext {
requestId?: string;
userId?: string;
[key: string]: unknown;
}
export function log(level: LogLevel, message: string, ctx: LogContext = {}): void {
console.log(JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
service: 'afa-lambda-api',
stage: process.env.STAGE,
...ctx,
}));
}
# Requête CloudWatch Insights — top 10 erreurs des dernières 24h
fields @timestamp, level, message, requestId, error
| filter level = "error"
| stats count() by message
| sort count desc
| limit 10
# Latence p50/p95/p99 par endpoint
fields @duration, path
| stats pct(@duration, 50) as p50,
pct(@duration, 95) as p95,
pct(@duration, 99) as p99 by path
| sort p99 desc
Alarmes CloudWatch dans template.yaml
ErrorRateAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub 'afa-${Stage}-error-rate'
AlarmDescription: Plus de 1% d'erreurs sur 5 minutes
Namespace: AWS/Lambda
MetricName: Errors
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
Dimensions:
- Name: FunctionName
Value: !Ref CreateItemFunction
AlarmActions:
- !Ref AlertTopic
DurationP99Alarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub 'afa-${Stage}-latency-p99'
Namespace: AWS/Lambda
MetricName: Duration
ExtendedStatistic: p99
Period: 60
EvaluationPeriods: 3
Threshold: 3000 # 3 secondes
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: !Ref CreateItemFunction
AlarmActions:
- !Ref AlertTopic
AlertTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Sub 'afa-alerts-${Stage}'
Subscription:
- Protocol: email
Endpoint: alerts@angularforall.com
Tracing X-Ray
# Activer X-Ray dans Globals
Globals:
Function:
Tracing: Active # tracing actif sur toutes les fonctions
Api:
TracingEnabled: true # idem côté API Gateway
# Permission IAM auto-ajoutée par SAM : AWSXrayWriteOnlyAccess
// Instrumentation AWS SDK avec X-Ray
import { AWSXRayRecorder } from 'aws-xray-sdk-core';
import { captureAWSv3Client } from 'aws-xray-sdk';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// captureAWSv3Client wrap le client : chaque appel devient un sous-segment X-Ray
const ddb = captureAWSv3Client(new DynamoDBClient({}));
Optimisation du cold start
Un cold start survient quand AWS doit créer un nouveau conteneur Lambda : téléchargement du code, démarrage du runtime Node.js, exécution de l'init du module. Pour Node.js 20, comptez 100 à 800 ms selon la taille du bundle et l'usage du VPC. Au-delà de 1 seconde, l'expérience utilisateur se dégrade nettement.
Bundle minimal avec esbuild
# SAM délègue à esbuild via Metadata
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true # réduit ~40% la taille
Target: node20
Sourcemap: true # source maps pour stack traces
Format: esm # ESM autorise le top-level await
OutExtension: ['.js=.mjs']
EntryPoints:
- src/handlers/list-items.ts
External:
- '@aws-sdk/*' # déjà présents dans le runtime nodejs20.x
- 'aws-xray-sdk-core' # via Lambda Layer si utilisé
Banner:
js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);'
ESM et top-level await
// src/handlers/with-config.mts — handler ESM
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import { getStripeSecret } from '../lib/secrets.js';
// Top-level await — exécuté UNE SEULE FOIS au cold start
// Les invocations chaudes suivantes utilisent directement la valeur résolue
const stripeConfig = await getStripeSecret();
console.log('Lambda init complete', { hasKey: !!stripeConfig.apiKey });
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
// stripeConfig est déjà résolu — zéro latence supplémentaire
return {
statusCode: 200,
body: JSON.stringify({ ok: true, env: process.env.STAGE }),
};
};
ARM64 (Graviton) — perf et économies
Globals:
Function:
Architectures: [arm64] # 20% moins cher, 19% plus rapide en moyenne
# Vérifier la compat des dépendances natives (rare avec Node.js)
# Si module natif fail : Architectures: [x86_64]
Provisioned Concurrency pour les fonctions critiques
CreateOrderFunction:
Type: AWS::Serverless::Function
Properties:
# ... config ...
AutoPublishAlias: live # crée alias 'live' à chaque deploy
DeploymentPreference:
Type: AllAtOnce # ou Canary10Percent5Minutes pour blue/green
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5 # 5 conteneurs toujours chauds
# Coût : 0,015 $ par GB-heure d'exécution provisionnée
# Exemple : 5 conteneurs × 256 MB × 730 h/mois = 5 × 0,25 × 730 × 0,015
# = ~14 $/mois pour 5 instances toujours chaudes
# Justifié si la fonction reçoit du trafic continu et que la latence p99 importe
Mesurer l'impact d'une optim
// Mesurer le temps d'init via context
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
const initStart = Date.now();
// ... imports lourds ...
const initDuration = Date.now() - initStart;
export const handler: APIGatewayProxyHandlerV2 = async (event, context) => {
// Premier invoke d'un conteneur : context.callbackWaitsForEmptyEventLoop
// CloudWatch logue automatiquement REPORT avec Init Duration
console.log(JSON.stringify({
initDuration,
requestId: context.awsRequestId,
}));
// ...
};
# Logs CloudWatch typiques après l'invoke
REPORT RequestId: xxx
Duration: 45.21 ms
Billed Duration: 46 ms
Memory Size: 256 MB
Max Memory Used: 89 MB
Init Duration: 142.33 ms ← cold start visible ici
@aws-sdk/* via la directive esbuild gagne 5 à 15 MB.
- Bundle esbuild minifié + tree-shaking actif (
Minify: true) - ARM64 par défaut sauf incompatibilité native
- Memory à 256 MB minimum (CPU proportionnel à la mémoire)
- Tracing X-Ray actif avec sampling configuré
- Logs JSON structurés + retention CloudWatch limitée à 30 jours
- Alarmes Errors et Duration p99 configurées avec SNS
- Provisioned Concurrency uniquement sur les fonctions critiques à fort trafic
- Secrets dans Secrets Manager + cache mémoire 5 min
- IAM least-privilege validé via
aws iam simulate-principal-policy
Conclusion
AWS Lambda combiné à Node.js 20, TypeScript et SAM forme aujourd'hui une stack mature pour construire des APIs serverless rapides à itérer et économiques à opérer. Le free tier généreux (1 million de requêtes/mois à vie), la facturation à la milliseconde et l'absence d'infrastructure à maintenir font de cette stack un choix pertinent pour les MVP, les APIs publiques à charge variable et les microservices événementiels.
Les points-clés à retenir : décrire toute l'infrastructure dans template.yaml, bundler agressivement avec esbuild, externaliser le SDK AWS, stocker les secrets dans Secrets Manager avec cache mémoire, appliquer IAM least-privilege via les politiques SAM pré-définies, et instrumenter dès le départ avec CloudWatch Logs JSON + X-Ray. Provisioned Concurrency reste réservée aux fonctions critiques car son coût grimpe vite.
Pour aller plus loin : explorez AWS CDK (Infrastructure-as-Code en TypeScript), Powertools for AWS Lambda (utilities officielles : logger, tracer, metrics, parameters), Lambda Function URLs (alternative gratuite à API Gateway pour les webhooks simples) et Lambda SnapStart (cold start quasi-éliminé, disponible pour Node.js depuis 2024).