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