diff --git a/lambda-durable-rest-api-sam-js/.gitignore b/lambda-durable-rest-api-sam-js/.gitignore new file mode 100644 index 000000000..4b6c6f876 --- /dev/null +++ b/lambda-durable-rest-api-sam-js/.gitignore @@ -0,0 +1,24 @@ +# SAM +.aws-sam/ +samconfig.toml + +# Node.js +node_modules/ +package-lock.json +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Test outputs +response.json diff --git a/lambda-durable-rest-api-sam-js/README.md b/lambda-durable-rest-api-sam-js/README.md new file mode 100644 index 000000000..e6d006517 --- /dev/null +++ b/lambda-durable-rest-api-sam-js/README.md @@ -0,0 +1,111 @@ +# Lambda Durable Function - REST API Call with Node.js + +This pattern demonstrates a Lambda durable function that calls an external REST API using Node.js. + +Learn more about this pattern at Serverless Land Patterns: << Add the live URL here >> + +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-rest-api-sam-js + ``` + +1. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + sam build + sam deploy --guided + ``` +1. During the prompts: + * Enter a stack name + * Enter the desired AWS Region + * 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 Execution for calling external REST APIs. It uses the AWS Durable Execution SDK to create a durable function that can: + +**AWS Durable Execution Features:** +- **Automatic State Management**: AWS manages execution state across invocations +- **Durable Steps**: The `context.step()` method marks functions that can be retried automatically +- **Durable Waits**: Use `context.wait()` to pause execution without consuming CPU or memory +- **Built-in Retry Logic**: Failed steps are automatically retried by AWS +- **Execution History**: AWS tracks the complete execution history and state + +The function uses the `withDurableExecution` wrapper to mark the Lambda handler as a durable execution workflow. All steps defined with `context.step()` are automatically retryable. + +AWS Lambda Durable Execution automatically handles failures, retries, and state persistence without requiring external services like DynamoDB or Step Functions. + +**Note**: This pattern requires Node.js 24.x runtime which has native support for durable execution. + +## Testing + +1. Get the function name from the stack outputs: +```bash +aws cloudformation describe-stacks --stack-name \ + --query 'Stacks[0].Outputs[?OutputKey==`FunctionName`].OutputValue' --output text +``` + +2. Invoke the function with default URL: +```bash +aws lambda invoke \ + --function-name \ + --payload '{}' \ + response.json && cat response.json +``` + +3. Invoke with a custom URL: +```bash +aws lambda invoke \ + --function-name \ + --payload '{"url": "https://jsonplaceholder.typicode.com/users/1"}' \ + response.json && cat response.json +``` + +Example JSON Lambda test event: +```json +{ + "url": "https://jsonplaceholder.typicode.com/posts/1" +} +``` + +Expected response (success): +```json +{ + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": "{\"message\": \"API call successful\", \"url\": \"https://jsonplaceholder.typicode.com/posts/1\", \"data\": {...}}" +} +``` + +The execution is durable - if the API call fails, AWS Lambda will automatically retry the `callRestApi` step without re-executing the entire function. + +## Cleanup + +1. Delete the stack: + ```bash + aws cloudformation delete-stack --stack-name + ``` +1. Confirm the stack has been deleted: + ```bash + aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'')].StackStatus" + ``` + +---- diff --git a/lambda-durable-rest-api-sam-js/example-pattern.json b/lambda-durable-rest-api-sam-js/example-pattern.json new file mode 100644 index 000000000..73698925f --- /dev/null +++ b/lambda-durable-rest-api-sam-js/example-pattern.json @@ -0,0 +1,64 @@ +{ + "title": "Lambda Durable Function - REST API Call (Node.js)", + "description": "A Lambda function that calls an external REST API using AWS Durable Execution SDK for automatic retries and state management.", + "language": "Node.js", + "level": "200", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates AWS Lambda Durable Execution for calling external REST APIs.", + "Uses the withDurableExecution wrapper to mark the Lambda handler as a durable workflow.", + "Uses context.step() to make the REST API call automatically retryable.", + "AWS automatically handles failures, retries, and state persistence without external services." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-rest-api-sam-js", + "templateURL": "serverless-patterns/lambda-durable-rest-api-sam-js", + "projectFolder": "lambda-durable-rest-api-sam-js", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda Durable Execution", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "AWS Lambda Developer Guide", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" + }, + { + "text": "Node.js Fetch API", + "link": "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" + } + ] + }, + "deploy": { + "text": [ + "sam build", + "sam deploy --guided" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: aws cloudformation delete-stack --stack-name STACK_NAME." + ] + }, + "authors": [ + { + "name": "Theophilus Ajayi", + "image": "https://drive.google.com/file/d/1hUrUxWk2JqDTbPhl0DgUeUVd2uFWnAby/view?usp=drivesdk", + "bio": "Technical Account Manager @ AWS", + "linkedin": "tolutheo" + } + ] +} diff --git a/lambda-durable-rest-api-sam-js/src/app.mjs b/lambda-durable-rest-api-sam-js/src/app.mjs new file mode 100644 index 000000000..4484141a5 --- /dev/null +++ b/lambda-durable-rest-api-sam-js/src/app.mjs @@ -0,0 +1,84 @@ +/** + * Lambda Durable Function - Calls REST API using AWS Durable Execution SDK + * Note: Node.js 22.x has built-in durable execution support + */ + +/** + * Lambda Durable Function - Calls REST API using AWS Durable Execution SDK + */ +import { withDurableExecution } from '@aws/durable-execution-sdk-js'; + +const DEFAULT_API_URL = process.env.API_URL || 'https://jsonplaceholder.typicode.com/posts/1'; + +export const handler = withDurableExecution(async (event, context) => { + context.logger.info('Starting durable REST API call'); + + // Get API URL from event or use default + const apiUrl = event.url || DEFAULT_API_URL; + + context.logger.info(`Using API URL: ${apiUrl}`); + + // Execute the REST API call as a durable step + const result = await context.step('Call REST API', async (stepCtx) => { + stepCtx.logger.info(`Calling REST API: ${apiUrl}`); + + try { + const response = await fetch(apiUrl, { + method: 'GET', + signal: AbortSignal.timeout(30000) // 30 second timeout + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + stepCtx.logger.info(`API call successful: ${response.status}`); + + return { + status: 'success', + statusCode: response.status, + data: data + }; + + } catch (error) { + stepCtx.logger.error(`API call failed: ${error.message}`); + return { + status: 'error', + error: error.message + }; + } + }); + + // Pause for 2 seconds without consuming CPU cycles or incurring usage charges + await context.wait({ seconds: 2 }); + + // Context logger is replay aware and will not log the same message multiple times + context.logger.info('Waited for 2 seconds without consuming CPU.'); + + // Return response based on result + if (result.status === 'success') { + const response = { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message: 'API call successful', + url: apiUrl, + data: result.data + }) + }; + return response; + } else { + const response = { + statusCode: 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + error: 'API call failed', + url: apiUrl, + details: result.error + }) + }; + return response; + } +}); diff --git a/lambda-durable-rest-api-sam-js/src/package.json b/lambda-durable-rest-api-sam-js/src/package.json new file mode 100644 index 000000000..ded2b1bc8 --- /dev/null +++ b/lambda-durable-rest-api-sam-js/src/package.json @@ -0,0 +1,21 @@ +{ + "name": "lambda-durable-rest-api", + "version": "1.0.0", + "description": "Lambda Durable Function - REST API Call with Node.js", + "type": "module", + "main": "app.mjs", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@aws/durable-execution-sdk-js": "^1.0.0" + }, + "keywords": [ + "aws", + "lambda", + "durable", + "rest-api" + ], + "author": "", + "license": "MIT-0" +} diff --git a/lambda-durable-rest-api-sam-js/template.yaml b/lambda-durable-rest-api-sam-js/template.yaml new file mode 100644 index 000000000..20f845374 --- /dev/null +++ b/lambda-durable-rest-api-sam-js/template.yaml @@ -0,0 +1,30 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Lambda Durable Function - Call REST API with Node.js + +Resources: + DurableFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: app.handler + Runtime: nodejs24.x + Timeout: 300 + DurableConfig: + ExecutionTimeout: 900 # 15 minutes - allows durablefunction to be invoked syncronously and asynchronously + RetentionPeriodInDays: 7 + MemorySize: 512 + Architectures: + - x86_64 + Environment: + Variables: + API_URL: https://jsonplaceholder.typicode.com/posts/1 + +Outputs: + FunctionArn: + Description: Lambda Function ARN + Value: !GetAtt DurableFunction.Arn + + FunctionName: + Description: Lambda Function Name + Value: !Ref DurableFunction diff --git a/lambda-durable-rest-api-sam-py/.gitignore b/lambda-durable-rest-api-sam-py/.gitignore new file mode 100644 index 000000000..ac2e7113e --- /dev/null +++ b/lambda-durable-rest-api-sam-py/.gitignore @@ -0,0 +1,48 @@ +# SAM +.aws-sam/ +samconfig.toml + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +ENV/ +env/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Testing +.pytest_cache/ +.coverage +htmlcov/ diff --git a/lambda-durable-rest-api-sam-py/README.md b/lambda-durable-rest-api-sam-py/README.md new file mode 100644 index 000000000..69c6647ff --- /dev/null +++ b/lambda-durable-rest-api-sam-py/README.md @@ -0,0 +1,111 @@ +# Lambda Durable Function - REST API Call with Python + +This pattern demonstrates a Lambda durable function that calls an external REST API using Python. + +Learn more about this pattern at Serverless Land Patterns: << Add the live URL here >> + +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-rest-api-sam-py + ``` + +1. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + sam build + sam deploy --guided + ``` +1. During the prompts: + * Enter a stack name + * Enter the desired AWS Region + * 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 Execution for calling external REST APIs. It uses the AWS Durable Execution SDK to create a durable function that can: + +**AWS Durable Execution Features:** +- **Automatic State Management**: AWS manages execution state across invocations +- **Durable Steps**: The `@durable_step` decorator marks functions that can be retried automatically +- **Durable Waits**: Use `context.wait()` to pause execution without consuming CPU or memory +- **Built-in Retry Logic**: Failed steps are automatically retried by AWS +- **Execution History**: AWS tracks the complete execution history and state + +The function uses two key components: +1. `@durable_step` - Wraps the REST API call, making it automatically retryable +2. `@durable_execution` - Marks the Lambda handler as a durable execution workflow + +AWS Lambda Durable Execution automatically handles failures, retries, and state persistence without requiring external services like DynamoDB or Step Functions. + +## Testing + +1. Get the function name from the stack outputs: +```bash +aws cloudformation describe-stacks --stack-name \ + --query 'Stacks[0].Outputs[?OutputKey==`FunctionName`].OutputValue' --output text +``` + +2. Invoke the function with default URL: +```bash +aws lambda invoke \ + --function-name \ + --payload '{}' \ + response.json && cat response.json +``` + +3. Invoke with a custom URL: +```bash +aws lambda invoke \ + --function-name \ + --payload '{"url": "https://jsonplaceholder.typicode.com/users/1"}' \ + response.json && cat response.json +``` + +Example JSON Lambda test event: +```json +{ + "url": "https://jsonplaceholder.typicode.com/posts/1" +} +``` + +Expected response (success): +```json +{ + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": "{\"message\": \"API call successful\", \"url\": \"https://jsonplaceholder.typicode.com/posts/1\", \"data\": {...}}" +} +``` + +The execution is durable - if the API call fails, AWS Lambda will automatically retry the `call_rest_api` step without re-executing the entire function. + +## Cleanup + +1. Delete the stack: + ```bash + aws cloudformation delete-stack --stack-name + ``` +1. Confirm the stack has been deleted: + ```bash + aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'')].StackStatus" + ``` + +---- diff --git a/lambda-durable-rest-api-sam-py/example-pattern.json b/lambda-durable-rest-api-sam-py/example-pattern.json new file mode 100644 index 000000000..ddb71b346 --- /dev/null +++ b/lambda-durable-rest-api-sam-py/example-pattern.json @@ -0,0 +1,64 @@ +{ + "title": "Lambda Durable Function - REST API Call", + "description": "A Lambda function that calls an external REST API using AWS Durable Execution SDK for automatic retries and state management.", + "language": "Python", + "level": "200", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates AWS Lambda Durable Execution for calling external REST APIs.", + "Uses the @durable_step decorator to make the REST API call automatically retryable.", + "Uses the @durable_execution decorator to mark the Lambda handler as a durable workflow.", + "AWS automatically handles failures, retries, and state persistence without external services." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-rest-api-sam-py", + "templateURL": "serverless-patterns/lambda-durable-rest-api-sam-py", + "projectFolder": "lambda-durable-rest-api-sam-py", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda Durable Execution", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "AWS Lambda Developer Guide", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" + }, + { + "text": "Python Requests Library", + "link": "https://requests.readthedocs.io/" + } + ] + }, + "deploy": { + "text": [ + "sam build", + "sam deploy --guided" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: aws cloudformation delete-stack --stack-name STACK_NAME." + ] + }, + "authors": [ + { + "name": "Theophilus Ajayi", + "image": "https://drive.google.com/file/d/1hUrUxWk2JqDTbPhl0DgUeUVd2uFWnAby/view?usp=drivesdk", + "bio": "Technical Account Manager @ AWS", + "linkedin": "tolutheo" + } + ] +} diff --git a/lambda-durable-rest-api-sam-py/src/app.py b/lambda-durable-rest-api-sam-py/src/app.py new file mode 100644 index 000000000..3e5dd1926 --- /dev/null +++ b/lambda-durable-rest-api-sam-py/src/app.py @@ -0,0 +1,83 @@ +""" +Lambda Durable Function - Calls REST API using AWS Durable Execution SDK +""" +import json +import os +import requests +from aws_durable_execution_sdk_python.config import Duration +from aws_durable_execution_sdk_python.context import StepContext, durable_step +from aws_durable_execution_sdk_python.execution import durable_execution + +DEFAULT_API_URL = os.environ.get('API_URL', 'https://jsonplaceholder.typicode.com/posts/1') + + +@durable_step +def call_rest_api(step_context: StepContext, url: str) -> dict: + """ + Durable step that calls an external REST API + """ + step_context.logger.info(f"Calling REST API: {url}") + + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + + result = { + 'status': 'success', + 'status_code': response.status_code, + 'data': response.json() + } + + step_context.logger.info(f"API call successful: {response.status_code}") + return result + + except requests.exceptions.RequestException as e: + step_context.logger.error(f"API call failed: {str(e)}") + return { + 'status': 'error', + 'error': str(e) + } + + +@durable_execution +def lambda_handler(event, context) -> dict: + """ + Lambda handler using AWS Durable Execution + """ + context.logger.info("Starting durable REST API call") + + # Get API URL from event or use default + api_url = event.get('url', DEFAULT_API_URL) + + context.logger.info(f"Using API URL: {api_url}") + + # Execute the REST API call as a durable step + result = context.step(call_rest_api(api_url)) + + # Optional: Add a wait period (demonstrates durable wait without consuming CPU) + context.logger.info("Waiting 2 seconds before returning response") + context.wait(Duration.from_seconds(2)) + + context.logger.info("Durable execution completed") + + # Return response based on result + if result['status'] == 'success': + return { + 'statusCode': 200, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps({ + 'message': 'API call successful', + 'url': api_url, + 'data': result['data'] + }) + } + else: + return { + 'statusCode': 500, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps({ + 'error': 'API call failed', + 'url': api_url, + 'details': result.get('error') + }) + } diff --git a/lambda-durable-rest-api-sam-py/src/requirements.txt b/lambda-durable-rest-api-sam-py/src/requirements.txt new file mode 100644 index 000000000..88b849d94 --- /dev/null +++ b/lambda-durable-rest-api-sam-py/src/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.31.0 +aws-durable-execution-sdk-python diff --git a/lambda-durable-rest-api-sam-py/template.yaml b/lambda-durable-rest-api-sam-py/template.yaml new file mode 100644 index 000000000..05e75d619 --- /dev/null +++ b/lambda-durable-rest-api-sam-py/template.yaml @@ -0,0 +1,30 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Lambda Durable Function - Call REST API with Python + +Resources: + DurableFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: app.lambda_handler + Runtime: python3.14 + Timeout: 300 + DurableConfig: + ExecutionTimeout: 900 # 15 minutes - allows durablefunction to be invoked syncronously and asynchronously + RetentionPeriodInDays: 7 + MemorySize: 512 + Architectures: + - x86_64 + Environment: + Variables: + API_URL: https://jsonplaceholder.typicode.com/posts/1 + +Outputs: + FunctionArn: + Description: Lambda Function ARN + Value: !GetAtt DurableFunction.Arn + + FunctionName: + Description: Lambda Function Name + Value: !Ref DurableFunction