Serverless Architectuur met AWS Lambda en CDK | Forrict Ga naar hoofdinhoud
Serverless AWS Ontwikkeling

Serverless Architectuur met AWS Lambda en CDK

Forrict Team
Serverless Architectuur met AWS Lambda en CDK
Bouw productie-klare serverless applicaties op AWS met Lambda, API Gateway, EventBridge en Step Functions met TypeScript CDK patronen en best practices

Serverless Architectuur met AWS Lambda en CDK

Schaalbare, kosteneffectieve serverless applicaties bouwen met AWS Lambda, API Gateway, EventBridge en Step Functions met behulp van Infrastructure as Code

Introductie

Serverless architectuur heeft een revolutie teweeggebracht in de manier waarop we applicaties bouwen en uitrollen. Door serverbeheer te abstraheren, kunnen ontwikkelaars zich concentreren op bedrijfslogica terwijl ze profiteren van automatische schaalbaarheid, pay-per-use prijsstelling en verminderde operationele overhead.

Deze uitgebreide gids onderzoekt het bouwen van productie-klare serverless applicaties op AWS met Lambda, API Gateway, EventBridge en Step Functions, allemaal gedefinieerd met AWS CDK in TypeScript. U leert architectuurpatronen, best practices en ziet praktijkvoorbeelden die u kunt aanpassen voor uw projecten.

Wat u leert:

  • Serverless architectuur fundamenten en wanneer deze te gebruiken
  • AWS Lambda best practices voor prestatie- en kostenoptimalisatie
  • REST en GraphQL API’s bouwen met API Gateway
  • Event-driven architecturen met EventBridge
  • Complexe workflows orkestreren met Step Functions
  • Complete CDK patronen voor serverless applicaties
  • Test-, monitoring- en observability strategieën

Serverless Architectuur Begrijpen

Wat is Serverless?

Serverless betekent niet “geen servers” – het betekent dat u geen servers beheert. De cloud provider handelt het provisioneren, schalen, patchen en onderhouden van de onderliggende infrastructuur af.

Belangrijkste kenmerken:

  • Geen serverbeheer: Focus op code, niet op infrastructuur
  • Automatische schaalbaarheid: Van nul tot duizenden gelijktijdige uitvoeringen
  • Pay-per-use: Betaal alleen voor daadwerkelijk verbruikte rekentijd
  • Ingebouwde hoge beschikbaarheid: Multi-AZ deployment standaard
  • Event-driven: Getriggerd door events uit verschillende bronnen

Wanneer Serverless Gebruiken

Ideale toepassingen:

  • REST API’s en GraphQL backends
  • Event processing (S3 uploads, DynamoDB streams, etc.)
  • Geplande taken en cron jobs
  • Datatransformatie en ETL pipelines
  • Webhooks en integraties
  • Microservices architecturen
  • Real-time bestandsverwerking
  • IoT backends

Wanneer alternatieven overwegen:

  • Langlopende processen (>15 minuten)
  • Applicaties die consistente sub-milliseconde latentie vereisen
  • Stateful applicaties met in-memory state
  • Legacy applicaties met aanzienlijke refactoring behoeften
  • Workloads met constant hoog verkeer (mogelijk kosteneffectiever op EC2/containers)

AWS Serverless Services Ecosysteem

Compute:        Lambda, Fargate
API:            API Gateway, AppSync (GraphQL)
Event Bus:      EventBridge
Orkestratie:    Step Functions
Opslag:         S3, DynamoDB
Messaging:      SQS, SNS
Authenticatie:  Cognito

AWS Lambda Best Practices

Lambda Functie Structuur

Aanbevolen projectstructuur:

my-serverless-app/
├── src/
│   ├── functions/
│   │   ├── api/
│   │   │   ├── get-user.ts
│   │   │   ├── create-user.ts
│   │   │   └── update-user.ts
│   │   ├── events/
│   │   │   ├── process-order.ts
│   │   │   └── send-email.ts
│   │   └── scheduled/
│   │       └── daily-report.ts
│   ├── lib/
│   │   ├── database.ts
│   │   ├── validation.ts
│   │   └── utils.ts
│   └── types/
│       └── index.ts
├── lib/
│   ├── lambda-stack.ts
│   └── api-stack.ts
└── package.json

Cold Start Optimalisatie

1. Kies de juiste runtime:

// Beste cold start prestaties
Runtime.NODEJS_20_X    // ~100-200ms
Runtime.PYTHON_3_11    // ~150-250ms

// Tragere cold starts
Runtime.JAVA_21        // ~1-3 seconden
Runtime.DOTNET_8       // ~500ms-1s

2. Minimaliseer package grootte:

// lambda/get-user.ts
// ❌ Fout: Importeer hele AWS SDK
import AWS from 'aws-sdk';

// ✅ Goed: Importeer alleen wat u nodig heeft
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';

3. Gebruik Lambda Layers voor afhankelijkheden:

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class LambdaLayerStack extends cdk.Stack {
  public readonly sharedLayer: lambda.LayerVersion;

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Creëer gedeelde afhankelijkheden layer
    this.sharedLayer = new lambda.LayerVersion(this, 'SharedDependencies', {
      code: lambda.Code.fromAsset('layers/shared-dependencies'),
      compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
      description: 'Gedeelde afhankelijkheden voor alle Lambda functies',
    });
  }
}

4. Schakel SnapStart in (Java/.NET):

const javaFunction = new lambda.Function(this, 'JavaFunction', {
  runtime: lambda.Runtime.JAVA_21,
  handler: 'com.example.Handler',
  code: lambda.Code.fromAsset('target/function.jar'),
  snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS, // Vermindert cold starts met 10x
});

Geheugen en Timeout Configuratie

Geheugen configuratie:

// Geheugen beïnvloedt zowel CPU als kosten
// Meer geheugen = Meer CPU = Snellere uitvoering

// Laag geheugen voor eenvoudige taken
const simpleFunction = new lambda.Function(this, 'SimpleFunction', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda/simple'),
  memorySize: 128, // MB - minimum, goedkoopst
  timeout: cdk.Duration.seconds(3),
});

// Medium geheugen voor typische API's
const apiFunction = new lambda.Function(this, 'ApiFunction', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda/api'),
  memorySize: 512, // MB - goede balans
  timeout: cdk.Duration.seconds(10),
});

// Hoog geheugen voor compute-intensieve taken
const computeFunction = new lambda.Function(this, 'ComputeFunction', {
  runtime: lambda.Runtime.PYTHON_3_11,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda/compute'),
  memorySize: 3008, // MB - maximum CPU
  timeout: cdk.Duration.seconds(60),
});

Pro Tip: Gebruik AWS Lambda Power Tuning om de optimale geheugenconfiguratie te vinden.

Environment Variables en Configuratie

import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ssm from 'aws-cdk-lib/aws-ssm';

// ✅ Goed: Gebruik environment variables voor configuratie
const apiFunction = new lambda.Function(this, 'ApiFunction', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda/api'),
  environment: {
    TABLE_NAME: table.tableName,
    REGION: this.region,
    LOG_LEVEL: 'INFO',
    // Secrets horen in Secrets Manager, niet in env vars
    API_URL: 'https://api.example.com',
  },
});

// Voor secrets, gebruik AWS Secrets Manager
const secret = secretsmanager.Secret.fromSecretNameV2(
  this,
  'ApiKey',
  'prod/api/key'
);

secret.grantRead(apiFunction);

// Toegang in Lambda code:
// const secretValue = await secretsManager.getSecretValue({ SecretId: process.env.SECRET_ARN });

Foutafhandeling en Retries

// lambda/handlers/process-order.ts
import { SQSEvent, Context } from 'aws-lambda';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';

const dynamodb = new DynamoDBClient({});

export const handler = async (event: SQSEvent, context: Context) => {
  // Array om mislukte berichten bij te houden
  const batchItemFailures: Array<{ itemIdentifier: string }> = [];

  for (const record of event.Records) {
    try {
      const order = JSON.parse(record.body);

      // Valideer input
      if (!order.orderId || !order.customerId) {
        throw new Error('Ongeldig order formaat');
      }

      // Verwerk order
      await dynamodb.send(
        new PutItemCommand({
          TableName: process.env.TABLE_NAME!,
          Item: {
            PK: { S: `ORDER#${order.orderId}` },
            SK: { S: `CUSTOMER#${order.customerId}` },
            status: { S: 'PROCESSED' },
            timestamp: { N: Date.now().toString() },
          },
        })
      );

      console.log(`Order succesvol verwerkt: ${order.orderId}`);
    } catch (error) {
      console.error(`Fout bij verwerken bericht: ${record.messageId}`, error);

      // Retourneer mislukt bericht ID voor SQS retry
      batchItemFailures.push({ itemIdentifier: record.messageId });
    }
  }

  // Retourneer partial batch response
  return { batchItemFailures };
};

Configureer Dead Letter Queue (DLQ):

import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';

// Creëer DLQ voor mislukte berichten
const dlq = new sqs.Queue(this, 'OrderProcessingDLQ', {
  queueName: 'order-processing-dlq',
  retentionPeriod: cdk.Duration.days(14),
});

// Creëer main queue
const queue = new sqs.Queue(this, 'OrderQueue', {
  queueName: 'order-processing-queue',
  visibilityTimeout: cdk.Duration.seconds(300),
  deadLetterQueue: {
    queue: dlq,
    maxReceiveCount: 3, // Probeer 3 keer opnieuw voordat naar DLQ gestuurd
  },
});

// Lambda functie
const processOrderFunction = new lambda.Function(this, 'ProcessOrder', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'process-order.handler',
  code: lambda.Code.fromAsset('lambda/handlers'),
  timeout: cdk.Duration.seconds(30),
});

// Voeg SQS toe als event source
processOrderFunction.addEventSource(
  new lambdaEventSources.SqsEventSource(queue, {
    batchSize: 10,
    reportBatchItemFailures: true, // Schakel partial batch responses in
  })
);

REST API’s Bouwen met API Gateway en Lambda

Complete API Stack met CDK

// lib/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export class UserApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // DynamoDB tabel voor gebruikersgegevens
    const userTable = new dynamodb.Table(this, 'UserTable', {
      tableName: 'users',
      partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      pointInTimeRecovery: true,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    // Lambda Layer voor gedeelde code
    const sharedLayer = new lambda.LayerVersion(this, 'SharedLayer', {
      code: lambda.Code.fromAsset('layers/shared'),
      compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
      description: 'Gedeelde utilities en AWS SDK',
    });

    // Lambda functies
    const getUser = new lambda.Function(this, 'GetUser', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'get-user.handler',
      code: lambda.Code.fromAsset('lambda/api'),
      layers: [sharedLayer],
      environment: {
        TABLE_NAME: userTable.tableName,
      },
      timeout: cdk.Duration.seconds(10),
      memorySize: 512,
    });

    const createUser = new lambda.Function(this, 'CreateUser', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'create-user.handler',
      code: lambda.Code.fromAsset('lambda/api'),
      layers: [sharedLayer],
      environment: {
        TABLE_NAME: userTable.tableName,
      },
      timeout: cdk.Duration.seconds(10),
      memorySize: 512,
    });

    const updateUser = new lambda.Function(this, 'UpdateUser', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'update-user.handler',
      code: lambda.Code.fromAsset('lambda/api'),
      layers: [sharedLayer],
      environment: {
        TABLE_NAME: userTable.tableName,
      },
      timeout: cdk.Duration.seconds(10),
      memorySize: 512,
    });

    const deleteUser = new lambda.Function(this, 'DeleteUser', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'delete-user.handler',
      code: lambda.Code.fromAsset('lambda/api'),
      layers: [sharedLayer],
      environment: {
        TABLE_NAME: userTable.tableName,
      },
      timeout: cdk.Duration.seconds(10),
      memorySize: 512,
    });

    // Ken DynamoDB permissies toe
    userTable.grantReadData(getUser);
    userTable.grantWriteData(createUser);
    userTable.grantWriteData(updateUser);
    userTable.grantWriteData(deleteUser);

    // CloudWatch Logs
    const logGroup = new logs.LogGroup(this, 'ApiLogs', {
      logGroupName: '/aws/apigateway/user-api',
      retention: logs.RetentionDays.ONE_WEEK,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // API Gateway REST API
    const api = new apigateway.RestApi(this, 'UserApi', {
      restApiName: 'User API',
      description: 'API voor gebruikersbeheer',
      deployOptions: {
        stageName: 'prod',
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        metricsEnabled: true,
        accessLogDestination: new apigateway.LogGroupLogDestination(logGroup),
        accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields(),
      },
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    // Request validator
    const requestValidator = new apigateway.RequestValidator(
      this,
      'RequestValidator',
      {
        restApi: api,
        validateRequestBody: true,
        validateRequestParameters: true,
      }
    );

    // Request models
    const userModel = api.addModel('UserModel', {
      contentType: 'application/json',
      schema: {
        type: apigateway.JsonSchemaType.OBJECT,
        required: ['email', 'name'],
        properties: {
          email: {
            type: apigateway.JsonSchemaType.STRING,
            format: 'email',
          },
          name: {
            type: apigateway.JsonSchemaType.STRING,
            minLength: 1,
            maxLength: 100,
          },
          age: {
            type: apigateway.JsonSchemaType.INTEGER,
            minimum: 0,
            maximum: 150,
          },
        },
      },
    });

    // API Resources en Methods
    const users = api.root.addResource('users');

    // POST /users - Creëer gebruiker
    users.addMethod('POST', new apigateway.LambdaIntegration(createUser), {
      requestValidator,
      requestModels: {
        'application/json': userModel,
      },
    });

    // GET /users/{userId} - Haal gebruiker op
    const user = users.addResource('{userId}');
    user.addMethod('GET', new apigateway.LambdaIntegration(getUser), {
      requestParameters: {
        'method.request.path.userId': true,
      },
    });

    // PUT /users/{userId} - Update gebruiker
    user.addMethod('PUT', new apigateway.LambdaIntegration(updateUser), {
      requestValidator,
      requestModels: {
        'application/json': userModel,
      },
      requestParameters: {
        'method.request.path.userId': true,
      },
    });

    // DELETE /users/{userId} - Verwijder gebruiker
    user.addMethod('DELETE', new apigateway.LambdaIntegration(deleteUser), {
      requestParameters: {
        'method.request.path.userId': true,
      },
    });

    // Outputs
    new cdk.CfnOutput(this, 'ApiUrl', {
      value: api.url,
      description: 'User API URL',
    });

    new cdk.CfnOutput(this, 'TableName', {
      value: userTable.tableName,
      description: 'DynamoDB tabel naam',
    });
  }
}

Lambda Handler Implementatie

// lambda/api/get-user.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
import { unmarshall } from '@aws-sdk/util-dynamodb';

const dynamodb = new DynamoDBClient({});
const TABLE_NAME = process.env.TABLE_NAME!;

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const headers = {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
  };

  try {
    const userId = event.pathParameters?.userId;

    if (!userId) {
      return {
        statusCode: 400,
        headers,
        body: JSON.stringify({ error: 'userId is verplicht' }),
      };
    }

    // Haal gebruiker op uit DynamoDB
    const result = await dynamodb.send(
      new GetItemCommand({
        TableName: TABLE_NAME,
        Key: {
          userId: { S: userId },
        },
      })
    );

    if (!result.Item) {
      return {
        statusCode: 404,
        headers,
        body: JSON.stringify({ error: 'Gebruiker niet gevonden' }),
      };
    }

    const user = unmarshall(result.Item);

    return {
      statusCode: 200,
      headers,
      body: JSON.stringify(user),
    };
  } catch (error) {
    console.error('Fout bij ophalen gebruiker:', error);

    return {
      statusCode: 500,
      headers,
      body: JSON.stringify({ error: 'Interne serverfout' }),
    };
  }
};
// lambda/api/create-user.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
import { v4 as uuidv4 } from 'uuid';

const dynamodb = new DynamoDBClient({});
const TABLE_NAME = process.env.TABLE_NAME!;

interface CreateUserRequest {
  email: string;
  name: string;
  age?: number;
}

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const headers = {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
  };

  try {
    if (!event.body) {
      return {
        statusCode: 400,
        headers,
        body: JSON.stringify({ error: 'Request body is verplicht' }),
      };
    }

    const userData: CreateUserRequest = JSON.parse(event.body);

    // Valideer email formaat
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(userData.email)) {
      return {
        statusCode: 400,
        headers,
        body: JSON.stringify({ error: 'Ongeldig email formaat' }),
      };
    }

    const userId = uuidv4();
    const timestamp = new Date().toISOString();

    const user = {
      userId,
      email: userData.email,
      name: userData.name,
      age: userData.age,
      createdAt: timestamp,
      updatedAt: timestamp,
    };

    // Opslaan in DynamoDB
    await dynamodb.send(
      new PutItemCommand({
        TableName: TABLE_NAME,
        Item: marshall(user),
        ConditionExpression: 'attribute_not_exists(userId)',
      })
    );

    return {
      statusCode: 201,
      headers,
      body: JSON.stringify(user),
    };
  } catch (error) {
    console.error('Fout bij aanmaken gebruiker:', error);

    return {
      statusCode: 500,
      headers,
      body: JSON.stringify({ error: 'Interne serverfout' }),
    };
  }
};

Event-Driven Architectuur met EventBridge

EventBridge Patroon

// lib/event-driven-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { Construct } from 'constructs';

export class EventDrivenStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Custom EventBridge Event Bus
    const eventBus = new events.EventBus(this, 'OrderEventBus', {
      eventBusName: 'order-events',
    });

    // Lambda functies voor verschillende event handlers
    const sendEmailFunction = new lambda.Function(this, 'SendEmail', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'send-email.handler',
      code: lambda.Code.fromAsset('lambda/events'),
      timeout: cdk.Duration.seconds(30),
    });

    const updateInventoryFunction = new lambda.Function(this, 'UpdateInventory', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'update-inventory.handler',
      code: lambda.Code.fromAsset('lambda/events'),
      timeout: cdk.Duration.seconds(30),
    });

    const notifyWarehouseFunction = new lambda.Function(this, 'NotifyWarehouse', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'notify-warehouse.handler',
      code: lambda.Code.fromAsset('lambda/events'),
      timeout: cdk.Duration.seconds(30),
    });

    // DLQ voor mislukte events
    const dlq = new sqs.Queue(this, 'EventsDLQ', {
      queueName: 'order-events-dlq',
      retentionPeriod: cdk.Duration.days(14),
    });

    // Rule: Order Geplaatst
    const orderPlacedRule = new events.Rule(this, 'OrderPlacedRule', {
      eventBus,
      ruleName: 'order-placed',
      description: 'Trigger wanneer nieuwe order geplaatst wordt',
      eventPattern: {
        source: ['order.service'],
        detailType: ['Order Placed'],
      },
    });

    // Voeg meerdere targets toe aan de rule
    orderPlacedRule.addTarget(
      new targets.LambdaFunction(sendEmailFunction, {
        deadLetterQueue: dlq,
        maxEventAge: cdk.Duration.hours(2),
        retryAttempts: 2,
      })
    );

    orderPlacedRule.addTarget(
      new targets.LambdaFunction(updateInventoryFunction, {
        deadLetterQueue: dlq,
        maxEventAge: cdk.Duration.hours(2),
        retryAttempts: 2,
      })
    );

    orderPlacedRule.addTarget(
      new targets.LambdaFunction(notifyWarehouseFunction, {
        deadLetterQueue: dlq,
        maxEventAge: cdk.Duration.hours(2),
        retryAttempts: 2,
      })
    );

    // Rule: Hoge Waarde Order (>€1000)
    const highValueOrderRule = new events.Rule(this, 'HighValueOrderRule', {
      eventBus,
      ruleName: 'high-value-order',
      description: 'Trigger voor orders boven €1000',
      eventPattern: {
        source: ['order.service'],
        detailType: ['Order Placed'],
        detail: {
          amount: [{ numeric: ['>', 1000] }],
        },
      },
    });

    // SNS topic voor hoge waarde order alerts
    const alertTopic = new cdk.aws_sns.Topic(this, 'HighValueAlertTopic');
    highValueOrderRule.addTarget(new targets.SnsTopic(alertTopic));

    // Output
    new cdk.CfnOutput(this, 'EventBusArn', {
      value: eventBus.eventBusArn,
      description: 'Order Event Bus ARN',
    });
  }
}

Events Publiceren

// lambda/api/create-order.ts
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';

const eventbridge = new EventBridgeClient({});
const EVENT_BUS_NAME = process.env.EVENT_BUS_NAME!;

interface Order {
  orderId: string;
  customerId: string;
  amount: number;
  items: Array<{ productId: string; quantity: number }>;
}

export const publishOrderPlacedEvent = async (order: Order): Promise<void> => {
  await eventbridge.send(
    new PutEventsCommand({
      Entries: [
        {
          EventBusName: EVENT_BUS_NAME,
          Source: 'order.service',
          DetailType: 'Order Placed',
          Detail: JSON.stringify({
            orderId: order.orderId,
            customerId: order.customerId,
            amount: order.amount,
            items: order.items,
            timestamp: new Date().toISOString(),
          }),
        },
      ],
    })
  );

  console.log(`Order Placed event gepubliceerd voor order: ${order.orderId}`);
};

Workflows Orkestreren met Step Functions

Step Functions State Machine

// lib/workflow-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class OrderWorkflowStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda functies voor elke stap
    const validateOrder = new lambda.Function(this, 'ValidateOrder', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'validate-order.handler',
      code: lambda.Code.fromAsset('lambda/workflow'),
      timeout: cdk.Duration.seconds(30),
    });

    const processPayment = new lambda.Function(this, 'ProcessPayment', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'process-payment.handler',
      code: lambda.Code.fromAsset('lambda/workflow'),
      timeout: cdk.Duration.seconds(30),
    });

    const reserveInventory = new lambda.Function(this, 'ReserveInventory', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'reserve-inventory.handler',
      code: lambda.Code.fromAsset('lambda/workflow'),
      timeout: cdk.Duration.seconds(30),
    });

    const notifyCustomer = new lambda.Function(this, 'NotifyCustomer', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'notify-customer.handler',
      code: lambda.Code.fromAsset('lambda/workflow'),
      timeout: cdk.Duration.seconds(30),
    });

    const refundPayment = new lambda.Function(this, 'RefundPayment', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'refund-payment.handler',
      code: lambda.Code.fromAsset('lambda/workflow'),
      timeout: cdk.Duration.seconds(30),
    });

    // Step Functions tasks
    const validateTask = new tasks.LambdaInvoke(this, 'Valideer Order', {
      lambdaFunction: validateOrder,
      outputPath: '$.Payload',
    });

    const paymentTask = new tasks.LambdaInvoke(this, 'Verwerk Betaling', {
      lambdaFunction: processPayment,
      outputPath: '$.Payload',
    });

    const inventoryTask = new tasks.LambdaInvoke(this, 'Reserveer Voorraad', {
      lambdaFunction: reserveInventory,
      outputPath: '$.Payload',
      resultPath: '$.inventoryResult',
    });

    const notifyTask = new tasks.LambdaInvoke(this, 'Notificeer Klant', {
      lambdaFunction: notifyCustomer,
      outputPath: '$.Payload',
    });

    const refundTask = new tasks.LambdaInvoke(this, 'Terugbetaling', {
      lambdaFunction: refundPayment,
      outputPath: '$.Payload',
    });

    // Succes en falen states
    const orderSuccess = new sfn.Succeed(this, 'Order Succesvol');
    const orderFailed = new sfn.Fail(this, 'Order Mislukt', {
      cause: 'Order verwerking mislukt',
      error: 'OrderProcessingError',
    });

    // Definieer workflow
    const definition = validateTask
      .next(
        new sfn.Choice(this, 'Order Geldig?')
          .when(sfn.Condition.booleanEquals('$.valid', true), paymentTask)
          .otherwise(orderFailed)
      );

    paymentTask.next(
      new sfn.Choice(this, 'Betaling Succesvol?')
        .when(
          sfn.Condition.stringEquals('$.paymentStatus', 'SUCCESS'),
          inventoryTask
        )
        .otherwise(orderFailed)
    );

    inventoryTask.next(
      new sfn.Choice(this, 'Voorraad Gereserveerd?')
        .when(
          sfn.Condition.booleanEquals('$.inventoryResult.Payload.reserved', true),
          notifyTask.next(orderSuccess)
        )
        .otherwise(
          refundTask.next(
            new sfn.Fail(this, 'Onvoldoende Voorraad', {
              cause: 'Kan voorraad niet reserveren',
              error: 'InventoryError',
            })
          )
        )
    );

    // Creëer State Machine
    const stateMachine = new sfn.StateMachine(this, 'OrderWorkflow', {
      stateMachineName: 'order-processing-workflow',
      definition,
      timeout: cdk.Duration.minutes(5),
      tracingEnabled: true,
    });

    // Output
    new cdk.CfnOutput(this, 'StateMachineArn', {
      value: stateMachine.stateMachineArn,
      description: 'Order Workflow State Machine ARN',
    });
  }
}

Monitoring en Observability

CloudWatch Metrics en Alarms

// lib/monitoring-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class MonitoringStack extends cdk.Stack {
  constructor(
    scope: Construct,
    id: string,
    apiFunction: lambda.Function,
    props?: cdk.StackProps
  ) {
    super(scope, id, props);

    // SNS topic voor alarms
    const alarmTopic = new sns.Topic(this, 'AlarmTopic', {
      displayName: 'Lambda Alarms',
    });

    alarmTopic.addSubscription(
      new subscriptions.EmailSubscription('ops-team@example.com')
    );

    // Error rate alarm
    const errorAlarm = new cloudwatch.Alarm(this, 'ErrorAlarm', {
      alarmName: 'Lambda-Hoog-Foutpercentage',
      metric: apiFunction.metricErrors({
        statistic: 'Sum',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 10,
      evaluationPeriods: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    errorAlarm.addAlarmAction(new actions.SnsAction(alarmTopic));

    // Duration alarm (trage responses)
    const durationAlarm = new cloudwatch.Alarm(this, 'DurationAlarm', {
      alarmName: 'Lambda-Trage-Response',
      metric: apiFunction.metricDuration({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 3000, // 3 seconden
      evaluationPeriods: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });

    durationAlarm.addAlarmAction(new actions.SnsAction(alarmTopic));

    // Throttle alarm
    const throttleAlarm = new cloudwatch.Alarm(this, 'ThrottleAlarm', {
      alarmName: 'Lambda-Throttling',
      metric: apiFunction.metricThrottles({
        statistic: 'Sum',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 1,
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });

    throttleAlarm.addAlarmAction(new actions.SnsAction(alarmTopic));

    // Dashboard
    const dashboard = new cloudwatch.Dashboard(this, 'LambdaDashboard', {
      dashboardName: 'Serverless-API-Dashboard',
    });

    dashboard.addWidgets(
      new cloudwatch.GraphWidget({
        title: 'Aanroepen',
        left: [apiFunction.metricInvocations()],
      }),
      new cloudwatch.GraphWidget({
        title: 'Fouten',
        left: [apiFunction.metricErrors()],
      }),
      new cloudwatch.GraphWidget({
        title: 'Duur',
        left: [apiFunction.metricDuration()],
      }),
      new cloudwatch.GraphWidget({
        title: 'Throttles',
        left: [apiFunction.metricThrottles()],
      })
    );
  }
}

Best Practices Samenvatting

1. Kostenoptimalisatie

  • Juiste Lambda geheugen dimensionering op basis van daadwerkelijk gebruik
  • Gebruik Compute Savings Plans voor voorspelbare workloads
  • Implementeer caching (API Gateway, CloudFront)
  • Stel passende timeouts in om onnodige kosten te vermijden
  • Gebruik ARM-gebaseerde Graviton2 processors (20% goedkoper)

2. Beveiliging

  • Volg het least privilege principe voor IAM rollen
  • Gebruik Secrets Manager voor gevoelige gegevens
  • Schakel encryptie in rust en tijdens transport in
  • Implementeer API Gateway authenticatie (Cognito, Lambda authorizers)
  • Regelmatige beveiligingsaudits met AWS Security Hub

3. Prestaties

  • Minimaliseer cold starts (package grootte, runtime keuze)
  • Gebruik provisioned concurrency voor latentie-gevoelige functies
  • Implementeer connection pooling voor databases
  • Schakel X-Ray tracing in voor debugging
  • Gebruik Lambda layers voor gedeelde afhankelijkheden

4. Betrouwbaarheid

  • Implementeer juiste foutafhandeling en retries
  • Gebruik Dead Letter Queues (DLQs)
  • Stel CloudWatch alarms in
  • Schakel X-Ray in voor distributed tracing
  • Test voor fouten (Chaos Engineering)

5. Operationele Excellentie

  • Gebruik Infrastructure as Code (CDK/Terraform)
  • Implementeer CI/CD pipelines
  • Tag alle resources voor kostentoewijzing
  • Documenteer architectuur en runbooks
  • Monitor en log alles

Conclusie

Serverless architectuur met AWS Lambda en CDK stelt u in staat om schaalbare, kosteneffectieve applicaties te bouwen zonder infrastructuur te beheren. Door de patronen en best practices uit deze gids te volgen, kunt u productie-klare serverless applicaties creëren die veilig, performant en onderhoudbaar zijn.

Belangrijkste conclusies:

  • Maak gebruik van AWS Lambda voor event-driven en API workloads
  • Gebruik AWS CDK voor Infrastructure as Code
  • Implementeer juiste foutafhandeling en retries
  • Monitor en optimaliseer continu
  • Volg AWS Well-Architected Framework principes

Klaar om uw serverless applicatie te bouwen? Forrict kan u helpen bij het ontwerpen en implementeren van serverless oplossingen afgestemd op uw bedrijfsbehoeften.

Bronnen

F

Forrict Team

AWS expert en consultant bij Forrict, gespecialiseerd in cloud architectuur en AWS best practices voor Nederlandse bedrijven.

Tags

AWS Lambda Serverless API Gateway EventBridge Step Functions CDK TypeScript Cloud Architectuur

Gerelateerde Artikelen

Klaar om je AWS Infrastructuur te Transformeren?

Laten we bespreken hoe we je cloud journey kunnen optimaliseren