Cloud & Déploiement angularforall.com

- AWS Lambda : API serverless Node.js TypeScript

Aws-Lambda Serverless Node-Js Typescript Api-Gateway Aws-Sam Cloudwatch Cold-Start Iam Secrets-Manager Dynamodb Esbuild Devops Cloud-Aws
AWS Lambda : API serverless Node.js TypeScript

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
Coûts en pratique : une petite API REST (500 000 requêtes/mois, 200 ms d'exécution moyenne, 128 MB) coûte environ 5 $/mois incluant Lambda, API Gateway HTTP et CloudWatch Logs. Free tier : 1 million de requêtes Lambda et 1 million de requêtes API Gateway HTTP gratuites par mois à vie.

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
Le template 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",...}
SAM local utilise Docker : le runtime Node.js 20 est pullé depuis le registry public.ecr.aws/lambda/nodejs:20. Premier lancement : ~30 secondes (download de l'image), invocations suivantes : instantanées. Le port par défaut 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),
  };
};
Optimisation cruciale : initialiser 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
Coût Secrets Manager : 0,40 $ par secret par mois + 0,05 $ pour 10 000 appels API. Pour les configs non-sensibles fréquemment lues, préférez Parameter Store (Standard tier gratuit jusqu'à 10 000 paramètres). Lambda peut lire les deux avec le même pattern de cache en mémoire.
Checklist sécurité IAM :
  • 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({}));
X-Ray facture 5 $ par million de traces enregistrées et 0,50 $ par million scannées. Pour une API à fort trafic, configurez un sampling rate (par défaut : 1 requête/sec + 5 % du reste) via une règle X-Ray pour éviter de tracer 100 % du trafic.

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
Règle empirique cold start Node.js 20 : bundle < 1 MB minifié, ARM64 + 256 MB de mémoire = init < 200 ms dans 95 % des cas. Au-delà de 1 MB, chaque MB ajoute ~50 ms d'init. Externaliser @aws-sdk/* via la directive esbuild gagne 5 à 15 MB.
Checklist mise en production :
  • 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).

Partager