From cb41d6f8904414e393ec4969f0e8b6372d6675b5 Mon Sep 17 00:00:00 2001 From: dsiz Date: Wed, 18 Feb 2026 19:43:12 +0530 Subject: [PATCH 1/5] adding lambda-durable-functions-nodejs-calling-ecs --- .../.gitignore | 26 ++ .../README.md | 220 +++++++++++++ .../example-pattern.json | 68 ++++ .../src/callback-handler.js | 95 ++++++ .../src/package.json | 23 ++ .../src/polling-handler.js | 79 +++++ .../template.yaml | 311 ++++++++++++++++++ 7 files changed, 822 insertions(+) create mode 100644 lambda-durable-functions-nodejs-calling-ecs/.gitignore create mode 100644 lambda-durable-functions-nodejs-calling-ecs/README.md create mode 100644 lambda-durable-functions-nodejs-calling-ecs/example-pattern.json create mode 100644 lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js create mode 100644 lambda-durable-functions-nodejs-calling-ecs/src/package.json create mode 100644 lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js create mode 100644 lambda-durable-functions-nodejs-calling-ecs/template.yaml diff --git a/lambda-durable-functions-nodejs-calling-ecs/.gitignore b/lambda-durable-functions-nodejs-calling-ecs/.gitignore new file mode 100644 index 000000000..dc6e3ed0b --- /dev/null +++ b/lambda-durable-functions-nodejs-calling-ecs/.gitignore @@ -0,0 +1,26 @@ +# Node modules +node_modules/ +package-lock.json + +# SAM build artifacts +.aws-sam/ +samconfig.toml + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Test outputs +response.json +*.log + +# Environment +.env +.env.local diff --git a/lambda-durable-functions-nodejs-calling-ecs/README.md b/lambda-durable-functions-nodejs-calling-ecs/README.md new file mode 100644 index 000000000..25b4c04bf --- /dev/null +++ b/lambda-durable-functions-nodejs-calling-ecs/README.md @@ -0,0 +1,220 @@ +# AWS Lambda durable functions with Amazon ECS Integration + +This pattern demonstrates how to use AWS Lambda durable functions to orchestrate long-running Amazon ECS Fargate tasks. The Lambda function can wait up to 24 hours for ECS task completion without incurring compute charges during the wait period. + +**Important:** Please check the [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) for regions currently supported by AWS Lambda durable functions. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-durable-functions-nodejs-calling-ecs + +## Architecture + +![Architecture Diagram](architecture.png) + +The pattern uses Lambda durable functions with the callback pattern to orchestrate ECS Fargate tasks cost-effectively. + +### Workflow Steps + +1. **Lambda function invoked** with task parameters (message, processing time) +2. **Durable function creates callback ID** using `context.waitForCallback()` +3. **ECS Fargate task started** with callback ID passed as environment variable +4. **Lambda function pauses** (no compute charges during wait) +5. **ECS task processes workload** and logs progress to CloudWatch +6. **ECS task completes** (in production, would call `SendDurableExecutionCallbackSuccess`) +7. **Lambda function resumes** and returns result + +## Key Features + +- ✅ **24-Hour Wait Time** - Can wait up to 24 hours for ECS task completion +- ✅ **No Compute Charges During Wait** - Function suspended during wait period +- ✅ **Callback Pattern** - ECS tasks call Lambda APIs directly to resume execution +- ✅ **CloudWatch Logs** - Full visibility into both Lambda and ECS execution +- ✅ **Generic Container** - Uses public Python image, easily replaceable +- ✅ **Fargate Serverless** - No EC2 instances to manage + +## Prerequisites + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) installed +* Node.js 22.x runtime (automatically provided by Lambda) + +## Deployment + +1. Navigate to the pattern directory: + ```bash + cd lambda-durable-functions-nodejs-calling-ecs + ``` + +2. Build the SAM application: + ```bash + sam build + ``` + +3. Deploy the application: + ```bash + sam deploy --guided + ``` + + During the guided deployment: + - Accept default values or customize as needed + - Allow SAM CLI to create IAM roles when prompted + - Note the function name from the outputs + +4. Note the `CallbackFunctionName` from the CloudFormation outputs + +## Testing + +### Test the Callback Pattern + +Invoke the Lambda function with a test payload: + +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"message":"Test ECS task","processingTime":8}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +**Note:** The `:prod` alias is required for durable functions. + +### Monitor Execution + +Check Lambda logs: +```bash +aws logs tail /aws/lambda/ --since 2m --follow +``` + +Check ECS task logs: +```bash +aws logs tail /ecs/lambda-ecs-durable-demo --since 2m --follow +``` + +### Expected Output + +**Lambda Logs:** +``` +Starting Lambda durable function - Callback Pattern +Callback ID created: +Starting ECS task with callback ID... +ECS task started: arn:aws:ecs:... +``` + +**ECS Logs:** +``` +=== ECS Task Started === +Callback ID: +Message: Test ECS task +Processing Time: 8 seconds +Simulating work... +=== Task Completed Successfully === +Result: {"status":"completed","message":"Processed: Test ECS task"} +Note: In production, call Lambda SendDurableExecutionCallbackSuccess API here +``` + +## How It Works + +### Lambda durable function (Node.js) + +The Lambda function uses the `@aws/durable-execution-sdk-js` package: + +```javascript +const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); + +exports.handler = withDurableExecution(async (event, context) => { + // Create callback and start ECS task + const result = await context.waitForCallback( + 'ecs-task-callback', + async (callbackId) => { + // Start ECS task with callback ID + const response = await ecs.send(new RunTaskCommand({ + // ... pass callbackId as environment variable + })); + }, + { timeout: { hours: 1 } } + ); + + return result; +}); +``` + +### ECS Task (Python) + +The ECS container receives the callback ID and processes the workload. In production, it would call the Lambda API: + +```bash +aws lambda send-durable-execution-callback-success \ + --callback-id $CALLBACK_ID \ + --result '{"status":"completed","data":"..."}' +``` + +### Key Configuration + +**Lambda Function:** +- Runtime: `nodejs22.x` (required for durable functions) +- `AutoPublishAlias: prod` (required) +- `DurableConfig` with execution timeout and retention period + +**ECS Task:** +- Launch type: `FARGATE` +- Public subnet with `assignPublicIp: ENABLED` +- Container image: `public.ecr.aws/docker/library/python:3.12-alpine` +- CloudWatch Logs enabled + +## Customization + +### Replace the ECS Container + +The pattern uses a generic Python container for demonstration. To use your own container: + +1. Update the `Image` in the `ECSTaskDefinition` resource +2. Ensure your container: + - Reads the `CALLBACK_ID` environment variable + - Calls `aws lambda send-durable-execution-callback-success` on completion + - Calls `aws lambda send-durable-execution-callback-failure` on error + +### Adjust Timeouts + +Modify the durable function timeout in `template.yaml`: + +```yaml +DurableConfig: + ExecutionTimeout: 86400 # 24 hours in seconds + RetentionPeriodInDays: 7 +``` + +And the callback timeout in the handler: + +```javascript +context.waitForCallback('ecs-task-callback', async (callbackId) => { + // ... +}, { + timeout: { hours: 24 }, // Maximum wait time + heartbeatTimeout: { minutes: 5 } // Optional heartbeat +}) +``` + +## Cleanup + +Delete the stack: + +```bash +sam delete +``` + +## Cost Considerations + +- **Lambda:** Charged only for active execution time, not during wait periods +- **ECS Fargate:** Charged per vCPU and memory per second while tasks run +- **CloudWatch Logs:** Charged for log ingestion and storage +- **VPC:** NAT Gateway charges if using private subnets (this pattern uses public subnets) + +## Additional Resources + +- [AWS Lambda durable functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) +- [Amazon ECS on AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) +- [AWS SAM Documentation](https://docs.aws.amazon.com/serverless-application-model/) + +--- + +© 2026 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. diff --git a/lambda-durable-functions-nodejs-calling-ecs/example-pattern.json b/lambda-durable-functions-nodejs-calling-ecs/example-pattern.json new file mode 100644 index 000000000..a1bc77d31 --- /dev/null +++ b/lambda-durable-functions-nodejs-calling-ecs/example-pattern.json @@ -0,0 +1,68 @@ +{ + "title": "AWS Lambda durable functions in NodeJS calling ECS", + "description": "Orchestrate long-running ECS Fargate tasks using Lambda durable functions with callback pattern", + "language": "Node.js", + "level": "200", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to use AWS Lambda durable functions to orchestrate Amazon ECS Fargate tasks that can run for up to 24 hours.", + "The Lambda function creates a callback ID, starts an ECS task with that ID, and suspends execution without incurring compute charges during the wait period.", + "When the ECS task completes, it calls the Lambda SendDurableExecutionCallbackSuccess API to resume the durable function and return results.", + "This pattern is ideal for long-running batch jobs, data processing, ML training, or any workload that exceeds Lambda's 15-minute timeout." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-functions-nodejs-calling-ecs", + "templateURL": "serverless-patterns/lambda-durable-functions-nodejs-calling-ecs", + "projectFolder": "lambda-durable-functions-nodejs-calling-ecs", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda durable functions Documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Amazon ECS on AWS Fargate", + "link": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html" + }, + { + "text": "Lambda Durable Execution SDK for JavaScript", + "link": "https://www.npmjs.com/package/@aws/durable-execution-sdk-js" + }, + { + "text": "SendDurableExecutionCallbackSuccess API", + "link": "https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackSuccess.html" + } + ] + }, + "deploy": { + "text": [ + "sam build", + "sam deploy --guided" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "sam delete" + ] + }, + "authors": [ + { + "name": "Surya Sai D", + "image": "", + "bio": "Surya works as a Technical Account Manager in AWS. He is an expert in Serverless frameworks and Event Driven Architectures. Surya is also passionate on technical writing and has contributed to AWS blogs and other Open Source Content.", + "linkedin": "surya-sai-d-64920416a" + } + ] +} diff --git a/lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js b/lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js new file mode 100644 index 000000000..1a8f527fe --- /dev/null +++ b/lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js @@ -0,0 +1,95 @@ +const { ECSClient, RunTaskCommand } = require('@aws-sdk/client-ecs'); +const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); + +const ecs = new ECSClient({ region: process.env.AWS_REGION }); + +/** + * Lambda Durable Function - Callback Pattern + * + * This function demonstrates the callback pattern where: + * 1. Lambda creates a callback and gets a callback ID + * 2. Lambda starts an ECS task and passes the callback ID + * 3. Lambda waits for the callback (suspends without charges) + * 4. ECS task calls Lambda APIs directly to send success/failure + * 5. Lambda resumes and returns the result + */ +exports.handler = withDurableExecution(async (event, context) => { + console.log('Starting Lambda Durable Function - Callback Pattern'); + console.log('Event:', JSON.stringify(event)); + + const message = event.message || 'Hello from Lambda Durable Function'; + const processingTime = event.processingTime || 10; + + try { + // Use waitForCallback to create callback and start ECS task + const result = await context.waitForCallback( + 'ecs-task-callback', + async (callbackId) => { + console.log('Callback ID created:', callbackId); + console.log('Starting ECS task with callback ID...'); + + // Start ECS task with callback ID as environment variable + const runTaskParams = { + cluster: process.env.ECS_CLUSTER, + taskDefinition: process.env.ECS_TASK_DEFINITION, + launchType: 'FARGATE', + networkConfiguration: { + awsvpcConfiguration: { + subnets: process.env.ECS_SUBNETS.split(','), + securityGroups: [process.env.ECS_SECURITY_GROUP], + assignPublicIp: 'ENABLED' + } + }, + overrides: { + containerOverrides: [ + { + name: 'worker', + environment: [ + { name: 'CALLBACK_ID', value: callbackId }, + { name: 'MESSAGE', value: message }, + { name: 'PROCESSING_TIME', value: processingTime.toString() } + ] + } + ] + } + }; + + const response = await ecs.send(new RunTaskCommand(runTaskParams)); + + if (!response.tasks || response.tasks.length === 0) { + throw new Error('Failed to start ECS task'); + } + + const taskArn = response.tasks[0].taskArn; + console.log('ECS task started:', taskArn); + }, + { + timeout: { hours: 1 }, // Wait up to 1 hour for callback + heartbeatTimeout: { minutes: 5 } // Expect heartbeat every 5 minutes + } + ); + + console.log('Callback received with result:', result); + + return { + statusCode: 200, + body: JSON.stringify({ + message: 'ECS task completed successfully', + result: result, + pattern: 'callback' + }) + }; + + } catch (error) { + console.error('Error in durable function:', error); + + return { + statusCode: 500, + body: JSON.stringify({ + message: 'ECS task failed', + error: error.message, + pattern: 'callback' + }) + }; + } +}); diff --git a/lambda-durable-functions-nodejs-calling-ecs/src/package.json b/lambda-durable-functions-nodejs-calling-ecs/src/package.json new file mode 100644 index 000000000..dbca6cf8d --- /dev/null +++ b/lambda-durable-functions-nodejs-calling-ecs/src/package.json @@ -0,0 +1,23 @@ +{ + "name": "lambda-ecs-durable-nodejs", + "version": "1.0.0", + "description": "Lambda Durable Functions calling ECS with Node.js", + "main": "callback-handler.js", + "dependencies": { + "@aws-sdk/client-ecs": "^3.700.0", + "@aws-sdk/client-lambda": "^3.700.0", + "@aws/durable-execution-sdk-js": "^1.0.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "lambda", + "durable", + "ecs", + "nodejs" + ], + "author": "", + "license": "MIT-0" +} diff --git a/lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js b/lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js new file mode 100644 index 000000000..7f22bf984 --- /dev/null +++ b/lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js @@ -0,0 +1,79 @@ +const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); +const { ECSClient, RunTaskCommand, DescribeTasksCommand } = require('@aws-sdk/client-ecs'); + +const ecs = new ECSClient({}); + +exports.handler = withDurableExecution(async (event, context) => { + console.log('Starting Lambda Durable Function - Polling Pattern'); + console.log('Event:', JSON.stringify(event)); + + const { message = 'Default message', processingTime = 10 } = event; + + // Step 1: Start ECS task (checkpointed) + const taskInfo = await context.step('start-ecs-task', async () => { + const params = { + cluster: process.env.ECS_CLUSTER, + taskDefinition: process.env.ECS_TASK_DEFINITION, + launchType: 'FARGATE', + networkConfiguration: { + awsvpcConfiguration: { + subnets: [process.env.ECS_SUBNET_1, process.env.ECS_SUBNET_2], + assignPublicIp: 'ENABLED' + } + }, + overrides: { + containerOverrides: [{ + name: 'worker', + environment: [ + { name: 'MESSAGE', value: message }, + { name: 'PROCESSING_TIME', value: String(processingTime) } + ] + }] + } + }; + + console.log('Starting ECS task...'); + const response = await ecs.send(new RunTaskCommand(params)); + const taskArn = response.tasks[0].taskArn; + console.log('ECS task started:', taskArn); + return { taskArn, cluster: process.env.ECS_CLUSTER }; + }); + + console.log('Task started:', taskInfo.taskArn); + + // Step 2: Poll until complete (checkpointed) + const result = await context.step('poll-until-complete', async () => { + let attempts = 0; + const maxAttempts = 60; + + while (attempts < maxAttempts) { + await new Promise(r => setTimeout(r, 5000)); + + const response = await ecs.send(new DescribeTasksCommand({ + cluster: taskInfo.cluster, + tasks: [taskInfo.taskArn] + })); + + const task = response.tasks[0]; + console.log(`Poll attempt ${attempts + 1}: Status = ${task.lastStatus}`); + + if (task.lastStatus === 'STOPPED') { + const exitCode = task.containers[0].exitCode; + console.log(`Task stopped with exit code: ${exitCode}`); + + if (exitCode === 0) { + return { success: true, message: 'Task completed successfully', taskArn: taskInfo.taskArn }; + } else { + throw new Error(`Task failed with exit code: ${exitCode}`); + } + } + + attempts++; + } + + throw new Error('Polling timeout - task did not complete'); + }); + + console.log('Workflow completed:', JSON.stringify(result)); + return result; +}); diff --git a/lambda-durable-functions-nodejs-calling-ecs/template.yaml b/lambda-durable-functions-nodejs-calling-ecs/template.yaml new file mode 100644 index 000000000..30615f889 --- /dev/null +++ b/lambda-durable-functions-nodejs-calling-ecs/template.yaml @@ -0,0 +1,311 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + Lambda Durable Functions to ECS with Node.js - Callback Pattern + + This pattern demonstrates Lambda Durable Functions invoking ECS tasks using the callback pattern. + The ECS task directly calls Lambda APIs (SendDurableExecutionCallbackSuccess/Failure) instead of using DynamoDB. + +Parameters: + VpcCIDR: + Type: String + Default: 10.0.0.0/16 + Description: CIDR block for VPC + +Globals: + Function: + Timeout: 900 + MemorySize: 512 + Runtime: nodejs22.x + Architectures: + - arm64 + +Resources: + # VPC Configuration + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: !Ref VpcCIDR + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-vpc + + PublicSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: !Select [0, !Cidr [!Ref VpcCIDR, 4, 8]] + AvailabilityZone: !Select [0, !GetAZs ''] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-public-1 + + PublicSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: !Select [1, !Cidr [!Ref VpcCIDR, 4, 8]] + AvailabilityZone: !Select [1, !GetAZs ''] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-public-2 + + InternetGateway: + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-igw + + AttachGateway: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref VPC + InternetGatewayId: !Ref InternetGateway + + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-public-rt + + PublicRoute: + Type: AWS::EC2::Route + DependsOn: AttachGateway + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + SubnetRouteTableAssociation1: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet1 + RouteTableId: !Ref PublicRouteTable + + SubnetRouteTableAssociation2: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet2 + RouteTableId: !Ref PublicRouteTable + + ECSSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for ECS tasks + VpcId: !Ref VPC + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-ecs-sg + + # ECS Cluster + ECSCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Sub ${AWS::StackName}-cluster + ClusterSettings: + - Name: containerInsights + Value: enabled + + # CloudWatch Log Group + ECSLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /ecs/${AWS::StackName} + RetentionInDays: 7 + + # ECS Task Execution Role + ECSTaskExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: CloudWatchLogs + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !GetAtt ECSLogGroup.Arn + + # ECS Task Role (for Lambda callback APIs) + ECSTaskRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: LambdaCallbackPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:SendDurableExecutionCallbackSuccess + - lambda:SendDurableExecutionCallbackFailure + - lambda:SendDurableExecutionCallbackHeartbeat + Resource: '*' + + # ECS Task Definition with Inline Node.js Code + ECSTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub ${AWS::StackName}-task + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + Cpu: '256' + Memory: '512' + ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn + TaskRoleArn: !GetAtt ECSTaskRole.Arn + ContainerDefinitions: + - Name: worker + Image: public.ecr.aws/docker/library/python:3.12-alpine + Essential: true + EntryPoint: ["/bin/sh", "-c"] + Command: + - | + python3 -u -c " + import os, time, sys + print('=== ECS Task Started ===', flush=True) + print(f'Callback ID: {os.environ.get(\"CALLBACK_ID\", \"N/A\")}', flush=True) + print(f'Message: {os.environ.get(\"MESSAGE\", \"N/A\")}', flush=True) + print(f'Processing Time: {os.environ.get(\"PROCESSING_TIME\", \"10\")} seconds', flush=True) + print('', flush=True) + print('Simulating work...', flush=True) + time.sleep(int(os.environ.get('PROCESSING_TIME', '10'))) + print('', flush=True) + print('=== Task Completed Successfully ===', flush=True) + print('Result: {\"status\":\"completed\",\"message\":\"Processed: ' + os.environ.get('MESSAGE', '') + '\"}', flush=True) + print('', flush=True) + print('Note: In production, call Lambda SendDurableExecutionCallbackSuccess API here', flush=True) + print('Command: aws lambda send-durable-execution-callback-success --callback-id $CALLBACK_ID --result {...}', flush=True) + sys.exit(0) + " + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref ECSLogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + + # Lambda Durable Function - Callback Pattern + CallbackLambdaFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./src + Handler: callback-handler.handler + Environment: + Variables: + ECS_CLUSTER: !Ref ECSCluster + ECS_TASK_DEFINITION: !Ref ECSTaskDefinition + ECS_SUBNETS: !Sub ${PublicSubnet1},${PublicSubnet2} + ECS_SECURITY_GROUP: !Ref ECSSecurityGroup + Policies: + - Statement: + - Effect: Allow + Action: + - ecs:RunTask + - ecs:DescribeTasks + Resource: '*' + - Effect: Allow + Action: + - iam:PassRole + Resource: + - !GetAtt ECSTaskExecutionRole.Arn + - !GetAtt ECSTaskRole.Arn + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + AutoPublishAlias: prod + DurableConfig: + ExecutionTimeout: 86400 + RetentionPeriodInDays: 7 + + # Lambda Durable Function - Polling Pattern + PollingLambdaFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./src + Handler: polling-handler.handler + Environment: + Variables: + ECS_CLUSTER: !Ref ECSCluster + ECS_TASK_DEFINITION: !Ref ECSTaskDefinition + ECS_SUBNETS: !Sub ${PublicSubnet1},${PublicSubnet2} + ECS_SECURITY_GROUP: !Ref ECSSecurityGroup + Policies: + - Statement: + - Effect: Allow + Action: + - ecs:RunTask + - ecs:DescribeTasks + Resource: '*' + - Effect: Allow + Action: + - iam:PassRole + Resource: + - !GetAtt ECSTaskExecutionRole.Arn + - !GetAtt ECSTaskRole.Arn + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + AutoPublishAlias: prod + DurableConfig: + ExecutionTimeout: 86400 + RetentionPeriodInDays: 7 + +Outputs: + CallbackLambdaFunctionArn: + Description: ARN of the Callback Lambda Durable Function + Value: !GetAtt CallbackLambdaFunction.Arn + + PollingLambdaFunctionArn: + Description: ARN of the Polling Lambda Durable Function + Value: !GetAtt PollingLambdaFunction.Arn + + ECSClusterName: + Description: Name of the ECS Cluster + Value: !Ref ECSCluster + + ECSTaskDefinitionArn: + Description: ARN of the ECS Task Definition + Value: !Ref ECSTaskDefinition + + LogGroupName: + Description: CloudWatch Log Group for ECS tasks + Value: !Ref ECSLogGroup + + VPCId: + Description: VPC ID + Value: !Ref VPC From 7d2add82a97ef7f8e319f08c141466d9950e265c Mon Sep 17 00:00:00 2001 From: dsiz Date: Wed, 18 Feb 2026 20:32:40 +0530 Subject: [PATCH 2/5] aws-lambda-durable-functions-nodejs-saga-pattern --- .../.gitignore | 8 + .../README.md | 178 ++++++++++++++++++ .../example-pattern.json | 64 +++++++ .../src/orchestrator/index.js | 80 ++++++++ .../src/orchestrator/package.json | 7 + .../template.yaml | 29 +++ 6 files changed, 366 insertions(+) create mode 100644 aws-lambda-durable-functions-nodejs-saga-pattern/.gitignore create mode 100644 aws-lambda-durable-functions-nodejs-saga-pattern/README.md create mode 100644 aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json create mode 100644 aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js create mode 100644 aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/package.json create mode 100644 aws-lambda-durable-functions-nodejs-saga-pattern/template.yaml diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/.gitignore b/aws-lambda-durable-functions-nodejs-saga-pattern/.gitignore new file mode 100644 index 000000000..f90cf609b --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/.gitignore @@ -0,0 +1,8 @@ +.aws-sam/ +*.pyc +__pycache__/ +.DS_Store +samconfig.toml +response.json +node_modules/ +package-lock.json diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/README.md b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md new file mode 100644 index 000000000..3cf3bfb0c --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md @@ -0,0 +1,178 @@ +# Saga Pattern with AWS Lambda Durable Functions (Node.js) + +This pattern demonstrates the Saga pattern for distributed transactions using AWS Lambda durable functions in Node.js. It coordinates a multi-step travel booking process (flight, hotel, car) with automatic compensating transactions on failure. + +## What is the Saga Pattern? + +The Saga pattern manages distributed transactions by breaking them into a sequence of local transactions. Each step can succeed or fail independently, and if any step fails, compensating transactions automatically undo previously completed steps to maintain data consistency. + +## Architecture + +This implementation uses a single Lambda durable function that: +1. Executes reservation steps sequentially (flight → hotel → car) +2. Tracks completed steps automatically via `context.step()` +3. Implements compensating transactions in reverse order on failure +4. Maintains state across retries without external storage + +## Key Features + +- **Automatic Checkpointing**: Each `context.step()` creates a checkpoint +- **Fault Tolerance**: Execution resumes from last checkpoint on failure +- **Compensating Transactions**: Automatic rollback in reverse order +- **No External State Store**: Durable functions handle state management +- **Failure Simulation**: Test different failure scenarios + +## How It Works + +### Success Flow +``` +Reserve Flight → Reserve Hotel → Reserve Car → SUCCESS +``` + +### Failure Flow (e.g., hotel fails) +``` +Reserve Flight → Reserve Hotel (FAILS) → Cancel Flight → ROLLBACK COMPLETE +``` + +### Failure Flow (e.g., car fails) +``` +Reserve Flight → Reserve Hotel → Reserve Car (FAILS) → Cancel Hotel → Cancel Flight → ROLLBACK COMPLETE +``` + +## Deployment + +### Prerequisites +- AWS CLI configured +- SAM CLI installed +- Node.js 22.x + +### Deploy +```bash +sam build +sam deploy --guided +``` + +Follow the prompts: +- Stack Name: `saga-pattern-demo` +- AWS Region: Your preferred region +- Confirm changes: Y +- Allow SAM CLI IAM role creation: Y +- Disable rollback: N +- Save arguments to configuration file: Y + +## Testing + +### Success Case +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-001","userId":"user-123"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +### Simulate Flight Failure +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-002","userId":"user-123","simulateFailure":"flight"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +### Simulate Hotel Failure (triggers flight cancellation) +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-003","userId":"user-123","simulateFailure":"hotel"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +### Simulate Car Failure (triggers hotel and flight cancellation) +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-004","userId":"user-123","simulateFailure":"car"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +## Viewing Logs + +```bash +sam logs --stack-name saga-pattern-demo --tail +``` + +Or view in CloudWatch Logs console. + +## Expected Output + +### Success Case +```json +{ + "status": "SUCCESS", + "message": "Trip booked successfully", + "tripId": "trip-001", + "userId": "user-123", + "reservations": { + "flight": { + "reservationId": "FL-1234567890", + "from": "SFO", + "to": "NYC", + "date": "2026-03-15", + "status": "CONFIRMED" + }, + "hotel": { + "reservationId": "HT-1234567891", + "name": "Grand Hotel NYC", + "checkIn": "2026-03-15", + "checkOut": "2026-03-18", + "status": "CONFIRMED" + }, + "car": { + "reservationId": "CR-1234567892", + "type": "SUV", + "pickupDate": "2026-03-15", + "returnDate": "2026-03-18", + "status": "CONFIRMED" + } + } +} +``` + +### Failure Case (hotel fails) +```json +{ + "status": "FAILED", + "message": "Trip booking failed, all reservations rolled back", + "tripId": "trip-003", + "userId": "user-123", + "error": "Hotel reservation failed - no rooms available", + "compensatedServices": ["flight"] +} +``` + +## How Durable Functions Enable Saga Pattern + +1. **State Management**: `context.step()` automatically checkpoints each operation +2. **Idempotency**: Steps are executed exactly once, even on retries +3. **Compensation Tracking**: `completedSteps` array tracks what needs rollback +4. **Automatic Recovery**: Failed executions resume from last checkpoint +5. **No External Dependencies**: No DynamoDB or Step Functions needed + +## Cleanup + +```bash +sam delete --stack-name saga-pattern-demo +``` + +## Learn More + +- [AWS Lambda Durable Functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) +- [Saga Pattern](https://microservices.io/patterns/data/saga.html) +- [Distributed Transactions](https://aws.amazon.com/blogs/compute/building-a-serverless-distributed-application-using-a-saga-orchestration-pattern/) diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json new file mode 100644 index 000000000..3f1076270 --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json @@ -0,0 +1,64 @@ +{ + "title": "Saga pattern with AWS Lambda durable functions in Node.js", + "description": "Implement the Saga pattern for distributed transactions using AWS Lambda durable functions with automatic compensating transactions", + "language": "Node.js", + "level": "200", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates the Saga pattern using AWS Lambda durable functions to coordinate distributed transactions across multiple services.", + "The orchestrator function executes a sequence of reservation steps (flight, hotel, car). Each step is checkpointed automatically using context.step().", + "If any step fails, compensating transactions execute in reverse order to rollback all completed operations, ensuring data consistency.", + "The durable function maintains execution state without requiring external storage like DynamoDB or Step Functions." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/aws-lambda-durable-functions-nodejs-saga-pattern", + "templateURL": "serverless-patterns/aws-lambda-durable-functions-nodejs-saga-pattern", + "projectFolder": "aws-lambda-durable-functions-nodejs-saga-pattern", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda Durable Functions", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Saga Pattern", + "link": "https://microservices.io/patterns/data/saga.html" + }, + { + "text": "Building Serverless Distributed Applications with Saga", + "link": "https://aws.amazon.com/blogs/compute/building-a-serverless-distributed-application-using-a-saga-orchestration-pattern/" + } + ] + }, + "deploy": { + "text": [ + "sam build", + "sam deploy --guided" + ] + }, + "testing": { + "text": [ + "See the README in the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "sam delete --stack-name saga-pattern-demo" + ] + }, + "authors": [ + { + "name": "Surya Sai D", + "image": "", + "bio": "Technical Account Manager at AWS, specializing in serverless architectures and distributed systems.", + "linkedin": "surya-sai-d-64920416a" + } + ] +} diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js new file mode 100644 index 000000000..adb407dd0 --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js @@ -0,0 +1,80 @@ +const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); + +exports.handler = withDurableExecution(async (event, context) => { + console.log('=== Saga Orchestrator Started ==='); + console.log('Input:', JSON.stringify(event)); + + const { tripId, userId, simulateFailure } = event; + const completedSteps = []; + + try { + // Step 1: Reserve Flight + const flight = await context.step('reserveFlight', async () => { + console.log('Reserving flight...'); + if (simulateFailure === 'flight') { + throw new Error('Flight reservation failed - no availability'); + } + const reservationId = `FL-${Date.now()}`; + console.log(`Flight reserved: ${reservationId}`); + return { reservationId, from: 'SFO', to: 'NYC', date: '2026-03-15', status: 'CONFIRMED' }; + }); + completedSteps.push({ service: 'flight', data: flight }); + + // Step 2: Reserve Hotel + const hotel = await context.step('reserveHotel', async () => { + console.log('Reserving hotel...'); + if (simulateFailure === 'hotel') { + throw new Error('Hotel reservation failed - no rooms available'); + } + const reservationId = `HT-${Date.now()}`; + console.log(`Hotel reserved: ${reservationId}`); + return { reservationId, name: 'Grand Hotel NYC', checkIn: '2026-03-15', checkOut: '2026-03-18', status: 'CONFIRMED' }; + }, { retry: { maxAttempts: 1 } }); + completedSteps.push({ service: 'hotel', data: hotel }); + + // Step 3: Reserve Car + const car = await context.step('reserveCar', async () => { + console.log('Reserving car...'); + if (simulateFailure === 'car') { + throw new Error('Car reservation failed - no vehicles available'); + } + const reservationId = `CR-${Date.now()}`; + console.log(`Car reserved: ${reservationId}`); + return { reservationId, type: 'SUV', pickupDate: '2026-03-15', returnDate: '2026-03-18', status: 'CONFIRMED' }; + }, { retry: { maxAttempts: 1 } }); + completedSteps.push({ service: 'car', data: car }); + + console.log('=== All Reservations Completed Successfully ==='); + return { + status: 'SUCCESS', + message: 'Trip booked successfully', + tripId, + userId, + reservations: { flight, hotel, car } + }; + + } catch (error) { + console.error('=== Saga Failed - Initiating Compensating Transactions ==='); + console.error('Error:', error.message); + + // Execute compensating transactions in REVERSE order + for (let i = completedSteps.length - 1; i >= 0; i--) { + const step = completedSteps[i]; + await context.step(`cancel_${step.service}`, async () => { + console.log(`Cancelling ${step.service}: ${step.data.reservationId}`); + // Simulate cancellation logic + return { reservationId: step.data.reservationId, status: 'CANCELLED' }; + }); + } + + console.log('=== All Compensating Transactions Completed ==='); + return { + status: 'FAILED', + message: 'Trip booking failed, all reservations rolled back', + tripId, + userId, + error: error.message, + compensatedServices: completedSteps.map(s => s.service) + }; + } +}); diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/package.json b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/package.json new file mode 100644 index 000000000..4d8f3db36 --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/package.json @@ -0,0 +1,7 @@ +{ + "name": "saga-orchestrator", + "version": "1.0.0", + "dependencies": { + "@aws/durable-execution-sdk-js": "^1.0.0" + } +} diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/template.yaml b/aws-lambda-durable-functions-nodejs-saga-pattern/template.yaml new file mode 100644 index 000000000..7e6111ade --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/template.yaml @@ -0,0 +1,29 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Saga pattern implementation using AWS Lambda durable functions in Node.js + +Resources: + SagaOrchestratorFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/orchestrator/ + Handler: index.handler + Runtime: nodejs22.x + Timeout: 900 + MemorySize: 512 + AutoPublishAlias: prod + DurableConfig: + ExecutionTimeout: 3600 + RetentionPeriodInDays: 7 + Environment: + Variables: + LOG_LEVEL: INFO + +Outputs: + SagaOrchestratorFunction: + Description: Saga Orchestrator Lambda Function ARN + Value: !GetAtt SagaOrchestratorFunction.Arn + + SagaOrchestratorFunctionAlias: + Description: Saga Orchestrator Function Alias (use this for invocation) + Value: !Sub '${SagaOrchestratorFunction}:prod' From 4f5065544a6362b5d9644c4021925b373fd40387 Mon Sep 17 00:00:00 2001 From: dsiz Date: Wed, 18 Feb 2026 21:07:25 +0530 Subject: [PATCH 3/5] aws-lambda-durable-functions-nodejs-saga-pattern --- .../.gitignore | 26 -- .../README.md | 220 ------------- .../example-pattern.json | 68 ---- .../src/callback-handler.js | 95 ------ .../src/package.json | 23 -- .../src/polling-handler.js | 79 ----- .../template.yaml | 311 ------------------ 7 files changed, 822 deletions(-) delete mode 100644 lambda-durable-functions-nodejs-calling-ecs/.gitignore delete mode 100644 lambda-durable-functions-nodejs-calling-ecs/README.md delete mode 100644 lambda-durable-functions-nodejs-calling-ecs/example-pattern.json delete mode 100644 lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js delete mode 100644 lambda-durable-functions-nodejs-calling-ecs/src/package.json delete mode 100644 lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js delete mode 100644 lambda-durable-functions-nodejs-calling-ecs/template.yaml diff --git a/lambda-durable-functions-nodejs-calling-ecs/.gitignore b/lambda-durable-functions-nodejs-calling-ecs/.gitignore deleted file mode 100644 index dc6e3ed0b..000000000 --- a/lambda-durable-functions-nodejs-calling-ecs/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Node modules -node_modules/ -package-lock.json - -# SAM build artifacts -.aws-sam/ -samconfig.toml - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Test outputs -response.json -*.log - -# Environment -.env -.env.local diff --git a/lambda-durable-functions-nodejs-calling-ecs/README.md b/lambda-durable-functions-nodejs-calling-ecs/README.md deleted file mode 100644 index 25b4c04bf..000000000 --- a/lambda-durable-functions-nodejs-calling-ecs/README.md +++ /dev/null @@ -1,220 +0,0 @@ -# AWS Lambda durable functions with Amazon ECS Integration - -This pattern demonstrates how to use AWS Lambda durable functions to orchestrate long-running Amazon ECS Fargate tasks. The Lambda function can wait up to 24 hours for ECS task completion without incurring compute charges during the wait period. - -**Important:** Please check the [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) for regions currently supported by AWS Lambda durable functions. - -Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-durable-functions-nodejs-calling-ecs - -## Architecture - -![Architecture Diagram](architecture.png) - -The pattern uses Lambda durable functions with the callback pattern to orchestrate ECS Fargate tasks cost-effectively. - -### Workflow Steps - -1. **Lambda function invoked** with task parameters (message, processing time) -2. **Durable function creates callback ID** using `context.waitForCallback()` -3. **ECS Fargate task started** with callback ID passed as environment variable -4. **Lambda function pauses** (no compute charges during wait) -5. **ECS task processes workload** and logs progress to CloudWatch -6. **ECS task completes** (in production, would call `SendDurableExecutionCallbackSuccess`) -7. **Lambda function resumes** and returns result - -## Key Features - -- ✅ **24-Hour Wait Time** - Can wait up to 24 hours for ECS task completion -- ✅ **No Compute Charges During Wait** - Function suspended during wait period -- ✅ **Callback Pattern** - ECS tasks call Lambda APIs directly to resume execution -- ✅ **CloudWatch Logs** - Full visibility into both Lambda and ECS execution -- ✅ **Generic Container** - Uses public Python image, easily replaceable -- ✅ **Fargate Serverless** - No EC2 instances to manage - -## Prerequisites - -* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured -* [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) installed -* Node.js 22.x runtime (automatically provided by Lambda) - -## Deployment - -1. Navigate to the pattern directory: - ```bash - cd lambda-durable-functions-nodejs-calling-ecs - ``` - -2. Build the SAM application: - ```bash - sam build - ``` - -3. Deploy the application: - ```bash - sam deploy --guided - ``` - - During the guided deployment: - - Accept default values or customize as needed - - Allow SAM CLI to create IAM roles when prompted - - Note the function name from the outputs - -4. Note the `CallbackFunctionName` from the CloudFormation outputs - -## Testing - -### Test the Callback Pattern - -Invoke the Lambda function with a test payload: - -```bash -aws lambda invoke \ - --function-name :prod \ - --invocation-type Event \ - --payload '{"message":"Test ECS task","processingTime":8}' \ - --cli-binary-format raw-in-base64-out \ - response.json -``` - -**Note:** The `:prod` alias is required for durable functions. - -### Monitor Execution - -Check Lambda logs: -```bash -aws logs tail /aws/lambda/ --since 2m --follow -``` - -Check ECS task logs: -```bash -aws logs tail /ecs/lambda-ecs-durable-demo --since 2m --follow -``` - -### Expected Output - -**Lambda Logs:** -``` -Starting Lambda durable function - Callback Pattern -Callback ID created: -Starting ECS task with callback ID... -ECS task started: arn:aws:ecs:... -``` - -**ECS Logs:** -``` -=== ECS Task Started === -Callback ID: -Message: Test ECS task -Processing Time: 8 seconds -Simulating work... -=== Task Completed Successfully === -Result: {"status":"completed","message":"Processed: Test ECS task"} -Note: In production, call Lambda SendDurableExecutionCallbackSuccess API here -``` - -## How It Works - -### Lambda durable function (Node.js) - -The Lambda function uses the `@aws/durable-execution-sdk-js` package: - -```javascript -const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); - -exports.handler = withDurableExecution(async (event, context) => { - // Create callback and start ECS task - const result = await context.waitForCallback( - 'ecs-task-callback', - async (callbackId) => { - // Start ECS task with callback ID - const response = await ecs.send(new RunTaskCommand({ - // ... pass callbackId as environment variable - })); - }, - { timeout: { hours: 1 } } - ); - - return result; -}); -``` - -### ECS Task (Python) - -The ECS container receives the callback ID and processes the workload. In production, it would call the Lambda API: - -```bash -aws lambda send-durable-execution-callback-success \ - --callback-id $CALLBACK_ID \ - --result '{"status":"completed","data":"..."}' -``` - -### Key Configuration - -**Lambda Function:** -- Runtime: `nodejs22.x` (required for durable functions) -- `AutoPublishAlias: prod` (required) -- `DurableConfig` with execution timeout and retention period - -**ECS Task:** -- Launch type: `FARGATE` -- Public subnet with `assignPublicIp: ENABLED` -- Container image: `public.ecr.aws/docker/library/python:3.12-alpine` -- CloudWatch Logs enabled - -## Customization - -### Replace the ECS Container - -The pattern uses a generic Python container for demonstration. To use your own container: - -1. Update the `Image` in the `ECSTaskDefinition` resource -2. Ensure your container: - - Reads the `CALLBACK_ID` environment variable - - Calls `aws lambda send-durable-execution-callback-success` on completion - - Calls `aws lambda send-durable-execution-callback-failure` on error - -### Adjust Timeouts - -Modify the durable function timeout in `template.yaml`: - -```yaml -DurableConfig: - ExecutionTimeout: 86400 # 24 hours in seconds - RetentionPeriodInDays: 7 -``` - -And the callback timeout in the handler: - -```javascript -context.waitForCallback('ecs-task-callback', async (callbackId) => { - // ... -}, { - timeout: { hours: 24 }, // Maximum wait time - heartbeatTimeout: { minutes: 5 } // Optional heartbeat -}) -``` - -## Cleanup - -Delete the stack: - -```bash -sam delete -``` - -## Cost Considerations - -- **Lambda:** Charged only for active execution time, not during wait periods -- **ECS Fargate:** Charged per vCPU and memory per second while tasks run -- **CloudWatch Logs:** Charged for log ingestion and storage -- **VPC:** NAT Gateway charges if using private subnets (this pattern uses public subnets) - -## Additional Resources - -- [AWS Lambda durable functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) -- [Amazon ECS on AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) -- [AWS SAM Documentation](https://docs.aws.amazon.com/serverless-application-model/) - ---- - -© 2026 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. diff --git a/lambda-durable-functions-nodejs-calling-ecs/example-pattern.json b/lambda-durable-functions-nodejs-calling-ecs/example-pattern.json deleted file mode 100644 index a1bc77d31..000000000 --- a/lambda-durable-functions-nodejs-calling-ecs/example-pattern.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "title": "AWS Lambda durable functions in NodeJS calling ECS", - "description": "Orchestrate long-running ECS Fargate tasks using Lambda durable functions with callback pattern", - "language": "Node.js", - "level": "200", - "framework": "SAM", - "introBox": { - "headline": "How it works", - "text": [ - "This pattern demonstrates how to use AWS Lambda durable functions to orchestrate Amazon ECS Fargate tasks that can run for up to 24 hours.", - "The Lambda function creates a callback ID, starts an ECS task with that ID, and suspends execution without incurring compute charges during the wait period.", - "When the ECS task completes, it calls the Lambda SendDurableExecutionCallbackSuccess API to resume the durable function and return results.", - "This pattern is ideal for long-running batch jobs, data processing, ML training, or any workload that exceeds Lambda's 15-minute timeout." - ] - }, - "gitHub": { - "template": { - "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-functions-nodejs-calling-ecs", - "templateURL": "serverless-patterns/lambda-durable-functions-nodejs-calling-ecs", - "projectFolder": "lambda-durable-functions-nodejs-calling-ecs", - "templateFile": "template.yaml" - } - }, - "resources": { - "bullets": [ - { - "text": "AWS Lambda durable functions Documentation", - "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" - }, - { - "text": "Amazon ECS on AWS Fargate", - "link": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html" - }, - { - "text": "Lambda Durable Execution SDK for JavaScript", - "link": "https://www.npmjs.com/package/@aws/durable-execution-sdk-js" - }, - { - "text": "SendDurableExecutionCallbackSuccess API", - "link": "https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackSuccess.html" - } - ] - }, - "deploy": { - "text": [ - "sam build", - "sam deploy --guided" - ] - }, - "testing": { - "text": [ - "See the GitHub repo for detailed testing instructions." - ] - }, - "cleanup": { - "text": [ - "sam delete" - ] - }, - "authors": [ - { - "name": "Surya Sai D", - "image": "", - "bio": "Surya works as a Technical Account Manager in AWS. He is an expert in Serverless frameworks and Event Driven Architectures. Surya is also passionate on technical writing and has contributed to AWS blogs and other Open Source Content.", - "linkedin": "surya-sai-d-64920416a" - } - ] -} diff --git a/lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js b/lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js deleted file mode 100644 index 1a8f527fe..000000000 --- a/lambda-durable-functions-nodejs-calling-ecs/src/callback-handler.js +++ /dev/null @@ -1,95 +0,0 @@ -const { ECSClient, RunTaskCommand } = require('@aws-sdk/client-ecs'); -const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); - -const ecs = new ECSClient({ region: process.env.AWS_REGION }); - -/** - * Lambda Durable Function - Callback Pattern - * - * This function demonstrates the callback pattern where: - * 1. Lambda creates a callback and gets a callback ID - * 2. Lambda starts an ECS task and passes the callback ID - * 3. Lambda waits for the callback (suspends without charges) - * 4. ECS task calls Lambda APIs directly to send success/failure - * 5. Lambda resumes and returns the result - */ -exports.handler = withDurableExecution(async (event, context) => { - console.log('Starting Lambda Durable Function - Callback Pattern'); - console.log('Event:', JSON.stringify(event)); - - const message = event.message || 'Hello from Lambda Durable Function'; - const processingTime = event.processingTime || 10; - - try { - // Use waitForCallback to create callback and start ECS task - const result = await context.waitForCallback( - 'ecs-task-callback', - async (callbackId) => { - console.log('Callback ID created:', callbackId); - console.log('Starting ECS task with callback ID...'); - - // Start ECS task with callback ID as environment variable - const runTaskParams = { - cluster: process.env.ECS_CLUSTER, - taskDefinition: process.env.ECS_TASK_DEFINITION, - launchType: 'FARGATE', - networkConfiguration: { - awsvpcConfiguration: { - subnets: process.env.ECS_SUBNETS.split(','), - securityGroups: [process.env.ECS_SECURITY_GROUP], - assignPublicIp: 'ENABLED' - } - }, - overrides: { - containerOverrides: [ - { - name: 'worker', - environment: [ - { name: 'CALLBACK_ID', value: callbackId }, - { name: 'MESSAGE', value: message }, - { name: 'PROCESSING_TIME', value: processingTime.toString() } - ] - } - ] - } - }; - - const response = await ecs.send(new RunTaskCommand(runTaskParams)); - - if (!response.tasks || response.tasks.length === 0) { - throw new Error('Failed to start ECS task'); - } - - const taskArn = response.tasks[0].taskArn; - console.log('ECS task started:', taskArn); - }, - { - timeout: { hours: 1 }, // Wait up to 1 hour for callback - heartbeatTimeout: { minutes: 5 } // Expect heartbeat every 5 minutes - } - ); - - console.log('Callback received with result:', result); - - return { - statusCode: 200, - body: JSON.stringify({ - message: 'ECS task completed successfully', - result: result, - pattern: 'callback' - }) - }; - - } catch (error) { - console.error('Error in durable function:', error); - - return { - statusCode: 500, - body: JSON.stringify({ - message: 'ECS task failed', - error: error.message, - pattern: 'callback' - }) - }; - } -}); diff --git a/lambda-durable-functions-nodejs-calling-ecs/src/package.json b/lambda-durable-functions-nodejs-calling-ecs/src/package.json deleted file mode 100644 index dbca6cf8d..000000000 --- a/lambda-durable-functions-nodejs-calling-ecs/src/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "lambda-ecs-durable-nodejs", - "version": "1.0.0", - "description": "Lambda Durable Functions calling ECS with Node.js", - "main": "callback-handler.js", - "dependencies": { - "@aws-sdk/client-ecs": "^3.700.0", - "@aws-sdk/client-lambda": "^3.700.0", - "@aws/durable-execution-sdk-js": "^1.0.0" - }, - "devDependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "lambda", - "durable", - "ecs", - "nodejs" - ], - "author": "", - "license": "MIT-0" -} diff --git a/lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js b/lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js deleted file mode 100644 index 7f22bf984..000000000 --- a/lambda-durable-functions-nodejs-calling-ecs/src/polling-handler.js +++ /dev/null @@ -1,79 +0,0 @@ -const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); -const { ECSClient, RunTaskCommand, DescribeTasksCommand } = require('@aws-sdk/client-ecs'); - -const ecs = new ECSClient({}); - -exports.handler = withDurableExecution(async (event, context) => { - console.log('Starting Lambda Durable Function - Polling Pattern'); - console.log('Event:', JSON.stringify(event)); - - const { message = 'Default message', processingTime = 10 } = event; - - // Step 1: Start ECS task (checkpointed) - const taskInfo = await context.step('start-ecs-task', async () => { - const params = { - cluster: process.env.ECS_CLUSTER, - taskDefinition: process.env.ECS_TASK_DEFINITION, - launchType: 'FARGATE', - networkConfiguration: { - awsvpcConfiguration: { - subnets: [process.env.ECS_SUBNET_1, process.env.ECS_SUBNET_2], - assignPublicIp: 'ENABLED' - } - }, - overrides: { - containerOverrides: [{ - name: 'worker', - environment: [ - { name: 'MESSAGE', value: message }, - { name: 'PROCESSING_TIME', value: String(processingTime) } - ] - }] - } - }; - - console.log('Starting ECS task...'); - const response = await ecs.send(new RunTaskCommand(params)); - const taskArn = response.tasks[0].taskArn; - console.log('ECS task started:', taskArn); - return { taskArn, cluster: process.env.ECS_CLUSTER }; - }); - - console.log('Task started:', taskInfo.taskArn); - - // Step 2: Poll until complete (checkpointed) - const result = await context.step('poll-until-complete', async () => { - let attempts = 0; - const maxAttempts = 60; - - while (attempts < maxAttempts) { - await new Promise(r => setTimeout(r, 5000)); - - const response = await ecs.send(new DescribeTasksCommand({ - cluster: taskInfo.cluster, - tasks: [taskInfo.taskArn] - })); - - const task = response.tasks[0]; - console.log(`Poll attempt ${attempts + 1}: Status = ${task.lastStatus}`); - - if (task.lastStatus === 'STOPPED') { - const exitCode = task.containers[0].exitCode; - console.log(`Task stopped with exit code: ${exitCode}`); - - if (exitCode === 0) { - return { success: true, message: 'Task completed successfully', taskArn: taskInfo.taskArn }; - } else { - throw new Error(`Task failed with exit code: ${exitCode}`); - } - } - - attempts++; - } - - throw new Error('Polling timeout - task did not complete'); - }); - - console.log('Workflow completed:', JSON.stringify(result)); - return result; -}); diff --git a/lambda-durable-functions-nodejs-calling-ecs/template.yaml b/lambda-durable-functions-nodejs-calling-ecs/template.yaml deleted file mode 100644 index 30615f889..000000000 --- a/lambda-durable-functions-nodejs-calling-ecs/template.yaml +++ /dev/null @@ -1,311 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: > - Lambda Durable Functions to ECS with Node.js - Callback Pattern - - This pattern demonstrates Lambda Durable Functions invoking ECS tasks using the callback pattern. - The ECS task directly calls Lambda APIs (SendDurableExecutionCallbackSuccess/Failure) instead of using DynamoDB. - -Parameters: - VpcCIDR: - Type: String - Default: 10.0.0.0/16 - Description: CIDR block for VPC - -Globals: - Function: - Timeout: 900 - MemorySize: 512 - Runtime: nodejs22.x - Architectures: - - arm64 - -Resources: - # VPC Configuration - VPC: - Type: AWS::EC2::VPC - Properties: - CidrBlock: !Ref VpcCIDR - EnableDnsHostnames: true - EnableDnsSupport: true - Tags: - - Key: Name - Value: !Sub ${AWS::StackName}-vpc - - PublicSubnet1: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - CidrBlock: !Select [0, !Cidr [!Ref VpcCIDR, 4, 8]] - AvailabilityZone: !Select [0, !GetAZs ''] - MapPublicIpOnLaunch: true - Tags: - - Key: Name - Value: !Sub ${AWS::StackName}-public-1 - - PublicSubnet2: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - CidrBlock: !Select [1, !Cidr [!Ref VpcCIDR, 4, 8]] - AvailabilityZone: !Select [1, !GetAZs ''] - MapPublicIpOnLaunch: true - Tags: - - Key: Name - Value: !Sub ${AWS::StackName}-public-2 - - InternetGateway: - Type: AWS::EC2::InternetGateway - Properties: - Tags: - - Key: Name - Value: !Sub ${AWS::StackName}-igw - - AttachGateway: - Type: AWS::EC2::VPCGatewayAttachment - Properties: - VpcId: !Ref VPC - InternetGatewayId: !Ref InternetGateway - - PublicRouteTable: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref VPC - Tags: - - Key: Name - Value: !Sub ${AWS::StackName}-public-rt - - PublicRoute: - Type: AWS::EC2::Route - DependsOn: AttachGateway - Properties: - RouteTableId: !Ref PublicRouteTable - DestinationCidrBlock: 0.0.0.0/0 - GatewayId: !Ref InternetGateway - - SubnetRouteTableAssociation1: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - SubnetId: !Ref PublicSubnet1 - RouteTableId: !Ref PublicRouteTable - - SubnetRouteTableAssociation2: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - SubnetId: !Ref PublicSubnet2 - RouteTableId: !Ref PublicRouteTable - - ECSSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: Security group for ECS tasks - VpcId: !Ref VPC - SecurityGroupEgress: - - IpProtocol: -1 - CidrIp: 0.0.0.0/0 - Tags: - - Key: Name - Value: !Sub ${AWS::StackName}-ecs-sg - - # ECS Cluster - ECSCluster: - Type: AWS::ECS::Cluster - Properties: - ClusterName: !Sub ${AWS::StackName}-cluster - ClusterSettings: - - Name: containerInsights - Value: enabled - - # CloudWatch Log Group - ECSLogGroup: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /ecs/${AWS::StackName} - RetentionInDays: 7 - - # ECS Task Execution Role - ECSTaskExecutionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: ecs-tasks.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - Policies: - - PolicyName: CloudWatchLogs - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:PutLogEvents - Resource: !GetAtt ECSLogGroup.Arn - - # ECS Task Role (for Lambda callback APIs) - ECSTaskRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: ecs-tasks.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: LambdaCallbackPolicy - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - lambda:SendDurableExecutionCallbackSuccess - - lambda:SendDurableExecutionCallbackFailure - - lambda:SendDurableExecutionCallbackHeartbeat - Resource: '*' - - # ECS Task Definition with Inline Node.js Code - ECSTaskDefinition: - Type: AWS::ECS::TaskDefinition - Properties: - Family: !Sub ${AWS::StackName}-task - NetworkMode: awsvpc - RequiresCompatibilities: - - FARGATE - Cpu: '256' - Memory: '512' - ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn - TaskRoleArn: !GetAtt ECSTaskRole.Arn - ContainerDefinitions: - - Name: worker - Image: public.ecr.aws/docker/library/python:3.12-alpine - Essential: true - EntryPoint: ["/bin/sh", "-c"] - Command: - - | - python3 -u -c " - import os, time, sys - print('=== ECS Task Started ===', flush=True) - print(f'Callback ID: {os.environ.get(\"CALLBACK_ID\", \"N/A\")}', flush=True) - print(f'Message: {os.environ.get(\"MESSAGE\", \"N/A\")}', flush=True) - print(f'Processing Time: {os.environ.get(\"PROCESSING_TIME\", \"10\")} seconds', flush=True) - print('', flush=True) - print('Simulating work...', flush=True) - time.sleep(int(os.environ.get('PROCESSING_TIME', '10'))) - print('', flush=True) - print('=== Task Completed Successfully ===', flush=True) - print('Result: {\"status\":\"completed\",\"message\":\"Processed: ' + os.environ.get('MESSAGE', '') + '\"}', flush=True) - print('', flush=True) - print('Note: In production, call Lambda SendDurableExecutionCallbackSuccess API here', flush=True) - print('Command: aws lambda send-durable-execution-callback-success --callback-id $CALLBACK_ID --result {...}', flush=True) - sys.exit(0) - " - LogConfiguration: - LogDriver: awslogs - Options: - awslogs-group: !Ref ECSLogGroup - awslogs-region: !Ref AWS::Region - awslogs-stream-prefix: ecs - - # Lambda Durable Function - Callback Pattern - CallbackLambdaFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: ./src - Handler: callback-handler.handler - Environment: - Variables: - ECS_CLUSTER: !Ref ECSCluster - ECS_TASK_DEFINITION: !Ref ECSTaskDefinition - ECS_SUBNETS: !Sub ${PublicSubnet1},${PublicSubnet2} - ECS_SECURITY_GROUP: !Ref ECSSecurityGroup - Policies: - - Statement: - - Effect: Allow - Action: - - ecs:RunTask - - ecs:DescribeTasks - Resource: '*' - - Effect: Allow - Action: - - iam:PassRole - Resource: - - !GetAtt ECSTaskExecutionRole.Arn - - !GetAtt ECSTaskRole.Arn - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: '*' - AutoPublishAlias: prod - DurableConfig: - ExecutionTimeout: 86400 - RetentionPeriodInDays: 7 - - # Lambda Durable Function - Polling Pattern - PollingLambdaFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: ./src - Handler: polling-handler.handler - Environment: - Variables: - ECS_CLUSTER: !Ref ECSCluster - ECS_TASK_DEFINITION: !Ref ECSTaskDefinition - ECS_SUBNETS: !Sub ${PublicSubnet1},${PublicSubnet2} - ECS_SECURITY_GROUP: !Ref ECSSecurityGroup - Policies: - - Statement: - - Effect: Allow - Action: - - ecs:RunTask - - ecs:DescribeTasks - Resource: '*' - - Effect: Allow - Action: - - iam:PassRole - Resource: - - !GetAtt ECSTaskExecutionRole.Arn - - !GetAtt ECSTaskRole.Arn - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: '*' - AutoPublishAlias: prod - DurableConfig: - ExecutionTimeout: 86400 - RetentionPeriodInDays: 7 - -Outputs: - CallbackLambdaFunctionArn: - Description: ARN of the Callback Lambda Durable Function - Value: !GetAtt CallbackLambdaFunction.Arn - - PollingLambdaFunctionArn: - Description: ARN of the Polling Lambda Durable Function - Value: !GetAtt PollingLambdaFunction.Arn - - ECSClusterName: - Description: Name of the ECS Cluster - Value: !Ref ECSCluster - - ECSTaskDefinitionArn: - Description: ARN of the ECS Task Definition - Value: !Ref ECSTaskDefinition - - LogGroupName: - Description: CloudWatch Log Group for ECS tasks - Value: !Ref ECSLogGroup - - VPCId: - Description: VPC ID - Value: !Ref VPC From f537853c9c1f49acad82212bab5b4b09c334deeb Mon Sep 17 00:00:00 2001 From: dsiz Date: Fri, 20 Feb 2026 16:23:07 +0530 Subject: [PATCH 4/5] Fix capitalization and apply PR feedback: use 'durable functions', AWS SAM, code tags, and update bio --- .../README.md | 6 +++--- .../example-pattern.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/README.md b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md index 3cf3bfb0c..f4c7af854 100644 --- a/aws-lambda-durable-functions-nodejs-saga-pattern/README.md +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md @@ -1,4 +1,4 @@ -# Saga Pattern with AWS Lambda Durable Functions (Node.js) +# Saga Pattern with AWS Lambda durable functions (Node.js) This pattern demonstrates the Saga pattern for distributed transactions using AWS Lambda durable functions in Node.js. It coordinates a multi-step travel booking process (flight, hotel, car) with automatic compensating transactions on failure. @@ -157,7 +157,7 @@ Or view in CloudWatch Logs console. } ``` -## How Durable Functions Enable Saga Pattern +## How durable functions Enable Saga Pattern 1. **State Management**: `context.step()` automatically checkpoints each operation 2. **Idempotency**: Steps are executed exactly once, even on retries @@ -173,6 +173,6 @@ sam delete --stack-name saga-pattern-demo ## Learn More -- [AWS Lambda Durable Functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) +- [AWS Lambda durable functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) - [Saga Pattern](https://microservices.io/patterns/data/saga.html) - [Distributed Transactions](https://aws.amazon.com/blogs/compute/building-a-serverless-distributed-application-using-a-saga-orchestration-pattern/) diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json index 3f1076270..a9dc2596f 100644 --- a/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json @@ -3,7 +3,7 @@ "description": "Implement the Saga pattern for distributed transactions using AWS Lambda durable functions with automatic compensating transactions", "language": "Node.js", "level": "200", - "framework": "SAM", + "framework": "AWS SAM", "introBox": { "headline": "How it works", "text": [ @@ -24,7 +24,7 @@ "resources": { "bullets": [ { - "text": "AWS Lambda Durable Functions", + "text": "AWS Lambda durable functions", "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" }, { @@ -50,14 +50,14 @@ }, "cleanup": { "text": [ - "sam delete --stack-name saga-pattern-demo" + "sam delete --stack-name saga-pattern-demo" ] }, "authors": [ { "name": "Surya Sai D", "image": "", - "bio": "Technical Account Manager at AWS, specializing in serverless architectures and distributed systems.", + "bio": "Surya works as a Technical Account Manager at AWS. He is an expert in Serverless frameworks and Event Driven Architectures. Surya is also passionate on technical writing and has contributed to AWS blogs and other Open Source Content.", "linkedin": "surya-sai-d-64920416a" } ] From 6eb4ea7f65ae089fd916fbd204ad65deba6ba7b5 Mon Sep 17 00:00:00 2001 From: dsiz Date: Mon, 23 Feb 2026 10:14:24 +0530 Subject: [PATCH 5/5] Applied PR feedback: replace console.log with context.logger, add SDK link, update README text, and add copyright footer --- .../README.md | 10 +++++-- .../example-pattern.json | 4 +++ .../src/orchestrator/index.js | 26 +++++++++---------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/README.md b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md index f4c7af854..1c62ab85d 100644 --- a/aws-lambda-durable-functions-nodejs-saga-pattern/README.md +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md @@ -159,8 +159,8 @@ Or view in CloudWatch Logs console. ## How durable functions Enable Saga Pattern -1. **State Management**: `context.step()` automatically checkpoints each operation -2. **Idempotency**: Steps are executed exactly once, even on retries +1. **State Management**: `context.step()` automatically creates checkpoints when completing the operation +2. **Idempotency**: Completed steps are executed exactly once, even on retries 3. **Compensation Tracking**: `completedSteps` array tracks what needs rollback 4. **Automatic Recovery**: Failed executions resume from last checkpoint 5. **No External Dependencies**: No DynamoDB or Step Functions needed @@ -176,3 +176,9 @@ sam delete --stack-name saga-pattern-demo - [AWS Lambda durable functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) - [Saga Pattern](https://microservices.io/patterns/data/saga.html) - [Distributed Transactions](https://aws.amazon.com/blogs/compute/building-a-serverless-distributed-application-using-a-saga-orchestration-pattern/) + +--- + +© 2026 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json index a9dc2596f..4199eaecd 100644 --- a/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json @@ -27,6 +27,10 @@ "text": "AWS Lambda durable functions", "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" }, + { + "text": "Lambda Durable Execution SDK", + "link": "https://github.com/aws/aws-durable-execution-sdk-js" + }, { "text": "Saga Pattern", "link": "https://microservices.io/patterns/data/saga.html" diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js index adb407dd0..129545178 100644 --- a/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js @@ -1,8 +1,8 @@ const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); exports.handler = withDurableExecution(async (event, context) => { - console.log('=== Saga Orchestrator Started ==='); - console.log('Input:', JSON.stringify(event)); + context.logger.info('=== Saga Orchestrator Started ==='); + context.logger.info('Input:', JSON.stringify(event)); const { tripId, userId, simulateFailure } = event; const completedSteps = []; @@ -10,41 +10,41 @@ exports.handler = withDurableExecution(async (event, context) => { try { // Step 1: Reserve Flight const flight = await context.step('reserveFlight', async () => { - console.log('Reserving flight...'); + context.logger.info('Reserving flight...'); if (simulateFailure === 'flight') { throw new Error('Flight reservation failed - no availability'); } const reservationId = `FL-${Date.now()}`; - console.log(`Flight reserved: ${reservationId}`); + context.logger.info(`Flight reserved: ${reservationId}`); return { reservationId, from: 'SFO', to: 'NYC', date: '2026-03-15', status: 'CONFIRMED' }; }); completedSteps.push({ service: 'flight', data: flight }); // Step 2: Reserve Hotel const hotel = await context.step('reserveHotel', async () => { - console.log('Reserving hotel...'); + context.logger.info('Reserving hotel...'); if (simulateFailure === 'hotel') { throw new Error('Hotel reservation failed - no rooms available'); } const reservationId = `HT-${Date.now()}`; - console.log(`Hotel reserved: ${reservationId}`); + context.logger.info(`Hotel reserved: ${reservationId}`); return { reservationId, name: 'Grand Hotel NYC', checkIn: '2026-03-15', checkOut: '2026-03-18', status: 'CONFIRMED' }; }, { retry: { maxAttempts: 1 } }); completedSteps.push({ service: 'hotel', data: hotel }); // Step 3: Reserve Car const car = await context.step('reserveCar', async () => { - console.log('Reserving car...'); + context.logger.info('Reserving car...'); if (simulateFailure === 'car') { throw new Error('Car reservation failed - no vehicles available'); } const reservationId = `CR-${Date.now()}`; - console.log(`Car reserved: ${reservationId}`); + context.logger.info(`Car reserved: ${reservationId}`); return { reservationId, type: 'SUV', pickupDate: '2026-03-15', returnDate: '2026-03-18', status: 'CONFIRMED' }; }, { retry: { maxAttempts: 1 } }); completedSteps.push({ service: 'car', data: car }); - console.log('=== All Reservations Completed Successfully ==='); + context.logger.info('=== All Reservations Completed Successfully ==='); return { status: 'SUCCESS', message: 'Trip booked successfully', @@ -54,20 +54,20 @@ exports.handler = withDurableExecution(async (event, context) => { }; } catch (error) { - console.error('=== Saga Failed - Initiating Compensating Transactions ==='); - console.error('Error:', error.message); + context.logger.error('=== Saga Failed - Initiating Compensating Transactions ==='); + context.logger.error('Error:', error.message); // Execute compensating transactions in REVERSE order for (let i = completedSteps.length - 1; i >= 0; i--) { const step = completedSteps[i]; await context.step(`cancel_${step.service}`, async () => { - console.log(`Cancelling ${step.service}: ${step.data.reservationId}`); + context.logger.info(`Cancelling ${step.service}: ${step.data.reservationId}`); // Simulate cancellation logic return { reservationId: step.data.reservationId, status: 'CANCELLED' }; }); } - console.log('=== All Compensating Transactions Completed ==='); + context.logger.info('=== All Compensating Transactions Completed ==='); return { status: 'FAILED', message: 'Trip booking failed, all reservations rolled back',