diff --git a/lambda-durable-functions-nodejs-sam/README.md b/lambda-durable-functions-nodejs-sam/README.md new file mode 100644 index 000000000..4d3e394ff --- /dev/null +++ b/lambda-durable-functions-nodejs-sam/README.md @@ -0,0 +1,154 @@ +# AWS Lambda durable functions with Node.js + +This pattern demonstrates AWS Lambda durable functions using Node.js to build resilient, long-running workflows that can execute for up to one year. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/lambda-durable-functions-nodejs-sam](https://serverlessland.com/patterns/lambda-durable-functions-nodejs-sam) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd lambda-durable-functions-nodejs-sam + ``` +1. From the command line, use AWS SAM to build and deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + sam build + sam deploy --guided --capabilities CAPABILITY_NAMED_IAM + ``` +1. During the prompts: + * Enter a stack name + * Enter the desired AWS Region (must support Lambda durable functions) + * Allow SAM CLI to create IAM roles with the required permissions. + + Once you have run `sam deploy --guided` mode once and saved arguments to a configuration file (samconfig.toml), you can use `sam deploy` in future to use these defaults. + +1. Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs which are used for testing. + +## How it works + +This pattern demonstrates AWS Lambda durable functions using Node.js. It implements a simple order processing workflow with automatic checkpointing, durable waits, and fault tolerance. + +The orchestrator function uses the `@aws/durable-execution-sdk-js` to implement: +- Checkpointed steps with `context.step()` +- Durable waits with `context.wait()` +- Automatic recovery from failures +- Structured JSON logging + +The workflow: +1. Validates input (checkpointed) +2. Executes enrichment step (checkpointed) by invoking the OrderEnricher Lambda +3. Waits 5 seconds (durable wait - no compute charges) +4. Executes finalization step (checkpointed) +5. Returns result + +## Architecture + +This demo includes two Lambda functions: + +1. **DurableOrderProcessor** (Orchestrator) — Uses `@aws/durable-execution-sdk-js` for durable execution. Orchestrates the workflow with checkpointed steps and durable waits. + +2. **OrderEnricher** (Worker) — Simple Lambda function (non-durable) that enriches order data with customer information. Called by the orchestrator. + +## Testing + +After deployment, use the test command from the stack outputs to invoke the durable function: + +```bash +# Get the test command from stack outputs +aws cloudformation describe-stacks \ + --stack-name STACK_NAME \ + --query 'Stacks[0].Outputs[?OutputKey==`TestCommand`].OutputValue' \ + --output text +``` + +Run the output command to invoke the function. Alternatively, you can copy the `TestCommand` value directly from the `sam deploy` output. + +### View Logs + +```bash +# View orchestrator logs +aws logs tail /aws/lambda/STACK_NAME-DurableOrderProcessor --follow + +# View enricher logs +aws logs tail /aws/lambda/STACK_NAME-OrderEnricher --follow +``` + +## Expected Output + +Successful execution returns: + +```json +{ + "success": true, + "orderId": "ORDER-123", + "enrichmentResult": { + "statusCode": 200, + "orderId": "ORDER-123", + "enrichedData": { + "customerId": "CUST-XXXX", + "timestamp": "2026-02-08T20:58:24.548Z" + } + }, + "finalResult": { + "orderId": "ORDER-123", + "status": "COMPLETED", + "enrichedData": { ... }, + "finalizedAt": "2026-02-08T20:58:26.859Z", + "message": "Order finalized successfully" + }, + "message": "Order processed successfully with durable execution", + "processedAt": "2026-02-08T20:58:26.954Z" +} +``` + +## Observing Durable Execution + +Check CloudWatch Logs to see the durable execution in action: + +1. **First Invocation**: Executes enrichment step, hits wait, suspends +2. **Second Invocation** (~2 seconds later): Resumes from checkpoint, skips enrichment (uses stored result), completes finalization + +You'll notice: +- Multiple Lambda invocations for a single workflow +- Enrichment step result is reused (not re-executed) +- Total execution time includes the 2-second wait, but you only pay for active compute time + +## Cleanup + +To remove all resources: + +```bash +sam delete --stack-name STACK_NAME +``` + +## Additional Information + +### Supported Runtimes + +AWS Lambda durable functions are available only in selected runtimes. Check the [Supported runtimes for durable functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-supported-runtimes.html) page for the latest availability. + +### Troubleshooting + +**"InvalidParameterValueException: You cannot invoke a durable function using an unqualified ARN"** +- Solution: Always use a qualified ARN (with version or alias). This template automatically creates a `prod` alias. + +**"Cannot find module '@aws/durable-execution-sdk-js'"** +- Solution: Ensure dependencies are installed. SAM CLI automatically runs `npm install` during build. + +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-durable-functions-nodejs-sam/example-pattern.json b/lambda-durable-functions-nodejs-sam/example-pattern.json new file mode 100644 index 000000000..41a462280 --- /dev/null +++ b/lambda-durable-functions-nodejs-sam/example-pattern.json @@ -0,0 +1,63 @@ +{ + "title": "AWS Lambda durable functions with Node.js", + "description": "Demonstrates AWS Lambda durable functions using Node.js with automatic checkpointing, durable waits, and fault tolerance for long-running workflows.", + "language": "Node.js", + "level": "200", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates AWS Lambda durable functions using Node.js to build resilient, long-running workflows that can execute for up to one year.", + "The orchestrator function uses the @aws/durable-execution-sdk-js to implement checkpointed steps with context.step(), durable waits with context.wait(), and automatic recovery from failures.", + "The workflow validates input, invokes an enrichment Lambda function, waits 2 seconds (without compute charges), and finalizes the order processing." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-functions-nodejs-sam", + "templateURL": "serverless-patterns/lambda-durable-functions-nodejs-sam", + "projectFolder": "lambda-durable-functions-nodejs-sam", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda durable functions documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Durable Execution SDK for JavaScript", + "link": "https://github.com/aws/aws-durable-execution-sdk-js" + }, + { + "text": "AWS SAM Documentation", + "link": "https://docs.aws.amazon.com/serverless-application-model/" + } + ] + }, + "deploy": { + "text": [ + "sam build", + "sam deploy --guided --capabilities CAPABILITY_NAMED_IAM" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: sam delete --stack-name durable-functions-demo" + ] + }, + "authors": [ + { + "name": "Your Name", + "image": "", + "bio": "Solutions Architect @ AWS", + "linkedin": "" + } + ] +} diff --git a/lambda-durable-functions-nodejs-sam/src/enrichment/index.js b/lambda-durable-functions-nodejs-sam/src/enrichment/index.js new file mode 100644 index 000000000..8a3e582f7 --- /dev/null +++ b/lambda-durable-functions-nodejs-sam/src/enrichment/index.js @@ -0,0 +1,14 @@ +exports.handler = async (event) => { + console.log('Received order:', event.orderId); + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + await sleep(2000); // Sleep for 2 seconds + // Simple enrichment + return { + statusCode: 200, + orderId: event.orderId, + enrichedData: { + customerId: 'CUST-' + Math.floor(Math.random() * 10000), + timestamp: new Date().toISOString() + } + }; +}; diff --git a/lambda-durable-functions-nodejs-sam/src/enrichment/package.json b/lambda-durable-functions-nodejs-sam/src/enrichment/package.json new file mode 100644 index 000000000..e2c8ffd25 --- /dev/null +++ b/lambda-durable-functions-nodejs-sam/src/enrichment/package.json @@ -0,0 +1,12 @@ +{ + "name": "order-enrichment-lambda", + "version": "1.0.0", + "description": "Simple order enrichment service", + "main": "index.js", + "keywords": [ + "aws", + "lambda" + ], + "author": "", + "license": "MIT" +} diff --git a/lambda-durable-functions-nodejs-sam/src/orchestrator/index.js b/lambda-durable-functions-nodejs-sam/src/orchestrator/index.js new file mode 100644 index 000000000..c8499ae3e --- /dev/null +++ b/lambda-durable-functions-nodejs-sam/src/orchestrator/index.js @@ -0,0 +1,48 @@ +const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda'); +const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); + +const lambda = new LambdaClient(); + +async function handler(event, context) { + const orderId = event.orderId || 'ORDER-001'; + console.log(`[Step 0] Starting durable order processing for ${orderId}`); + + // Step 1: Validate input + const validatedOrder = await context.step('validate-input', async () => { + console.log(`[Step 1] Validating order ${orderId}`); + if (!event.nodejsLambdaArn) throw new Error('nodejsLambdaArn is required'); + return { orderId, nodejsLambdaArn: event.nodejsLambdaArn, receivedAt: new Date().toISOString() }; + }); + + // Step 2: Call the enricher Lambda + // If this Lambda crashes AFTER this step, replay skips it and uses the cached result + const enrichment = await context.step('enrich-order', async () => { + console.log(`[Step 2] Invoking enricher Lambda for ${orderId}`); + const response = await lambda.send(new InvokeCommand({ + FunctionName: validatedOrder.nodejsLambdaArn, + Payload: JSON.stringify({ orderId }) + })); + return JSON.parse(Buffer.from(response.Payload).toString()); + }); + + // Durable wait — survives crashes and restarts + console.log(`[Wait] Pausing for 5 seconds...`); + await context.wait({ seconds: 5 }); + console.log(`[Wait] Resumed after 5 seconds`); + + // Step 3: Finalize + const result = await context.step('finalize-order', async () => { + console.log(`[Step 3] Finalizing order ${orderId}`); + return { + orderId, + status: 'COMPLETED', + enrichedData: enrichment, + finalizedAt: new Date().toISOString() + }; + }); + + console.log(`[Done] Order ${orderId} processed successfully`); + return result; +} + +exports.handler = withDurableExecution(handler); \ No newline at end of file diff --git a/lambda-durable-functions-nodejs-sam/src/orchestrator/package.json b/lambda-durable-functions-nodejs-sam/src/orchestrator/package.json new file mode 100644 index 000000000..3f3225c90 --- /dev/null +++ b/lambda-durable-functions-nodejs-sam/src/orchestrator/package.json @@ -0,0 +1,21 @@ +{ + "name": "nodejs-durable-order-processor", + "version": "1.0.0", + "description": "AWS Lambda durable functions - Node.js orchestrator with structured logging and error handling", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "aws", + "lambda", + "durable", + "orchestrator" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-lambda": "^3.700.0", + "@aws/durable-execution-sdk-js": "^1.0.0" + } +} diff --git a/lambda-durable-functions-nodejs-sam/template.yaml b/lambda-durable-functions-nodejs-sam/template.yaml new file mode 100644 index 000000000..9de400768 --- /dev/null +++ b/lambda-durable-functions-nodejs-sam/template.yaml @@ -0,0 +1,89 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: AWS Lambda Durable Functions Demo Using Nodejs + +Globals: + Function: + Runtime: nodejs24.x + Timeout: 30 + MemorySize: 512 + Architectures: + - arm64 + LoggingConfig: + LogFormat: JSON + ApplicationLogLevel: INFO + SystemLogLevel: INFO + +Resources: + # IAM Role for Lambda Functions + LambdaDurableExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${AWS::StackName}-LambdaDurableExecutionRole' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy + Policies: + - PolicyName: LambdaInvokePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-OrderEnricher' + # Enrichment Lambda Function (Simple, Non-Durable) + OrderEnricherFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub '${AWS::StackName}-OrderEnricher' + CodeUri: src/enrichment/ + Handler: index.handler + Description: Simple order enrichment service + Role: !GetAtt LambdaDurableExecutionRole.Arn + Timeout: 30 + MemorySize: 256 + + # Orchestrator Lambda Function (Durable) + DurableOrderProcessorFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub '${AWS::StackName}-DurableOrderProcessor' + CodeUri: src/orchestrator/ + Handler: index.handler + Description: Node.js Durable Order Processor with structured logging + Role: !GetAtt LambdaDurableExecutionRole.Arn + Environment: + Variables: + ENRICHMENT_FUNCTION_ARN: !GetAtt OrderEnricherFunction.Arn + DurableConfig: + ExecutionTimeout: 120 + RetentionPeriodInDays: 7 + AutoPublishAlias: prod + +Outputs: + DurableOrderProcessorArn: + Description: ARN of the Durable Order Processor Function (use with :prod alias) + Value: !Sub '${DurableOrderProcessorFunction.Arn}:prod' + Export: + Name: !Sub '${AWS::StackName}-DurableOrderProcessorArn' + + OrderEnricherArn: + Description: ARN of the Order Enricher Function + Value: !GetAtt OrderEnricherFunction.Arn + Export: + Name: !Sub '${AWS::StackName}-OrderEnricherArn' + + TestCommand: + Description: Command to test the durable function + Value: !Sub 'aws lambda invoke --function-name ${DurableOrderProcessorFunction.Arn}:prod --payload ''{"orderId":"ORDER-123","nodejsLambdaArn":"${OrderEnricherFunction.Arn}"}'' --cli-binary-format raw-in-base64-out response.json && cat response.json | jq .' + + LogsCommand: + Description: Command to view logs + Value: !Sub 'aws logs tail /aws/lambda/${DurableOrderProcessorFunction} --follow'