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
- AWS Lambda Documentatie
- AWS CDK Documentatie
- Serverless Land
- AWS Well-Architected Serverless Lens
- Lambda Power Tuning
Forrict Team
AWS expert en consultant bij Forrict, gespecialiseerd in cloud architectuur en AWS best practices voor Nederlandse bedrijven.

