diff --git a/apigw-lambda-tenant-isolation/README.md b/apigw-lambda-tenant-isolation/README.md
new file mode 100644
index 000000000..c2c034e90
--- /dev/null
+++ b/apigw-lambda-tenant-isolation/README.md
@@ -0,0 +1,205 @@
+# Multi-tenant API with AWS Lambda tenant isolation
+
+This sample project demonstrates tenant isolation mode of AWS Lambda functions by comparing two Lambda functions - one with tenant isolation enabled and one without. The demonstration uses in-memory counters to visually show how tenant isolation provides separate execution environments for different tenants.
+
+## 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
+- [Python 3.14 or above](https://www.python.org/downloads/) installed
+
+
+## Deployment Instructions
+
+1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
+
+ ```bash
+ git clone https://github.com/aws-samples/serverless-patterns
+ ```
+
+2. Change directory to the pattern directory:
+
+ ```bash
+ cd serverless-patterns/apigw-lambda-tenant-isolation
+ ```
+
+3. From the command line, run the following commands:
+
+ ```bash
+ sam build
+ sam deploy --guided --capabilities CAPABILITY_NAMED_IAM
+ ```
+
+4. During the prompts:
+
+ - Enter a stack name
+ - Enter the desired AWS Region e.g. `us-east-1`.
+ - Allow SAM CLI to create IAM roles with the required permissions.
+ - Keep default values to the rest of the parameters.
+
+ 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.
+
+5. Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs which are used for next step as well as testing.
+
+## How it works
+
+The SAM template deploys two Lambda functions - one with tenant isolation mode enabled, and another with tenant mode disabled.
+
+
+
+Here's a breakdown of the steps:
+
+1. **Standard AWS Lambda Function**: Receives tenant headers (`x-tenant-id`) but shares execution environment across all tenants. The counter variable, when increased for one tenant, impacts the other tenants (demonstrates the limitation)
+
+2. **Tenant-Isolated AWS Lambda Function**: Maintains separate execution environments per tenant using AWS Lambda tenant isolation mode (demonstrates the solution)
+
+3. **Amazon API Gateway**: Provides REST endpoints for both functions with header mapping
+
+## Testing
+
+Use [curl](https://curl.se/) to send a HTTP POST request to the API. Make sure to replace `api-id` with the one from your `sam deploy --guided` output:
+
+### Standard Function (The limitation)
+
+The standard function receives tenant headers but cannot isolate tenants - all requests share the same counter:
+
+Replace with `StandardMultiTenantAPIEndpointUrl`:
+
+```bash
+STANDARD_URL="https://your-api-id.execute-api.region.amazonaws.com/dev/standard"
+```
+
+BlueTenant request:
+
+```bash
+curl -H "tenant-id: BlueTenant" "$STANDARD_URL"
+```
+
+Response:
+
+```bash
+{
+ "counter": 1,
+ "tenant_id": "BlueTenant",
+ "isolation_enabled": false,
+ "message": "Counter incremented successfully - SHARED across all tenants! (Received tenant: BlueTenant)",
+ "warning": "This function does NOT provide tenant isolation - all tenants share the same counter!"
+}
+```
+
+RedTenant request:
+
+```bash
+curl -H "x-tenant-id: RedTenant" "$STANDARD_URL"
+```
+
+Response:
+
+```bash
+{
+ "counter": 2,
+ "tenant_id": "RedTenant",
+ "isolation_enabled": false,
+ "message": "Counter incremented successfully - SHARED across all tenants! (Received tenant: RedTenant)",
+ "warning": "This function does NOT provide tenant isolation - all tenants share the same counter!"
+}
+```
+
+GreenTenant request:
+
+```bash
+curl -H "x-tenant-id: GreenTenant" "$STANDARD_URL"
+```
+
+Response:
+
+```bash
+{
+ "counter": 3,
+ "tenant_id": "GreenTenant",
+ "isolation_enabled": false,
+ "message": "Counter incremented successfully - SHARED across all tenants! (Received tenant: GreenTenant)",
+ "warning": "This function does NOT provide tenant isolation - all tenants share the same counter!"
+}
+```
+
+Continue to invoke the API for different tenants. Note the `counter` values. As all the three tenants are reusing the same Lambda execution environment, the counter variable is also shared and continuously increasing across tenants.
+
+### Isolated Function (The solution)
+
+The isolated function provides true tenant isolation - each tenant gets separate Lambda execution environments:
+
+Replace with `IsolatedTenantAPIEndpointUrl`:
+
+```bash
+
+ISOLATED_URL="https://your-api-id.execute-api.region.amazonaws.com/dev/isolated"
+```
+
+BlueTenant requests (independent counter):
+
+```bash
+curl -H "x-tenant-id: BlueTenant" "$ISOLATED_URL"
+```
+
+Response:
+
+```bash
+{
+ "counter": 1,
+ "tenant_id": "BlueTenant",
+ "isolation_enabled": true,
+ "message": "Counter incremented successfully for tenant BlueTenant"
+}
+```
+
+GreenTenant requests (separate independent counter):
+
+```bash
+curl -H "x-tenant-id: GreenTenant" "$ISOLATED_URL"
+```
+
+Response:
+
+```bash
+{
+ "counter": 1,
+ "tenant_id": "GreenTenant",
+ "isolation_enabled": true,
+ "message": "Counter incremented successfully for tenant GreenTenant"
+}
+```
+
+Continue to invoke the API for different tenants. Note the `counter` values. Each tenant maintains independent counters (BlueTenant: 1→2→3, GreenTenant: 1→2), showing true isolation.
+
+### Monitoring
+
+Check CloudWatch logs to see tenant isolation in action:
+
+```bash
+# View logs for standard function
+aws logs filter-log-events \
+ --log-group-name "/aws/lambda/your-stack-name-counter-standard" \
+ --start-time $(date -d '10 minutes ago' +%s)000
+
+# View logs for isolated function (notice tenantId in platform events)
+aws logs filter-log-events \
+ --log-group-name "/aws/lambda/your-stack-name-counter-isolated" \
+ --start-time $(date -d '10 minutes ago' +%s)000
+```
+
+## Cleanup
+
+1. To delete the resources deployed to your AWS account via AWS SAM, run the following command:
+
+```bash
+sam delete
+```
+
+---
+
+Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+SPDX-License-Identifier: MIT-0
\ No newline at end of file
diff --git a/apigw-lambda-tenant-isolation/apigw-lambda-tenant-isolation.yaml b/apigw-lambda-tenant-isolation/apigw-lambda-tenant-isolation.yaml
new file mode 100644
index 000000000..4b99d61fb
--- /dev/null
+++ b/apigw-lambda-tenant-isolation/apigw-lambda-tenant-isolation.yaml
@@ -0,0 +1,78 @@
+{
+ "title": "Multi-tenant API with AWS Lambda tenant isolation",
+ "description": "This sample project demonstrates the tenant isolation mode of AWS Lambda functions.",
+ "language": "Python",
+ "level": "200",
+ "framework": "AWS SAM",
+ "introBox": {
+ "headline": "How it works",
+ "text": [
+ "Amazon API Gateway receives the HTTP GET request with tenant id in the header x-tenant-id.",
+ "The API Gateway triggers either the standard or the tenant isolated Lambda functions depending on the URI.",
+ "Observe the counter variable value between standard and tenant isolation mode enabled Lambda function as you invoke the APIs for different tenant."
+ ]
+ },
+ "gitHub": {
+ "template": {
+ "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-lambda-tenant-isolation",
+ "templateURL": "serverless-patterns/apigw-lambda-tenant-isolation",
+ "projectFolder": "apigw-lambda-tenant-isolation",
+ "templateFile": "template.yaml"
+ }
+ },
+ "resources": {
+ "bullets": [
+ {
+ "text": "AWS Lambda tenant isolation",
+ "link": "https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html"
+ },
+ {
+ "text": "AWS Blog - Building multi-tenant SaaS applications with AWS Lambda’s new tenant isolation mode",
+ "link": "https://aws.amazon.com/blogs/compute/building-multi-tenant-saas-applications-with-aws-lambdas-new-tenant-isolation-mode/"
+ }
+ ]
+ },
+ "deploy": {
+ "text": [
+ "sam build",
+ "sam deploy --guided"
+ ]
+ },
+ "testing": {
+ "text": [
+ "See the GitHub repo for detailed testing instructions."
+ ]
+ },
+ "cleanup": {
+ "text": [
+ "Delete the stack: sam delete."
+ ]
+ },
+ "authors": [
+ {
+ "name": "Biswanath Mukherjee",
+ "image": "https://serverlessland.com/assets/images/resources/contributors/biswanath-mukherjee.jpg",
+ "bio": "I am a Sr. Solutions Architect working at AWS India. I help strategic global enterprise customer to architect their workload to run on AWS.",
+ "linkedin": "biswanathmukherjee"
+ }
+ ],
+ "patternArch": {
+ "icon1": {
+ "x": 20,
+ "y": 50,
+ "service": "apigw",
+ "label": "API Gateway REST API"
+ },
+ "icon2": {
+ "x": 80,
+ "y": 50,
+ "service": "lambda",
+ "label": "AWS Lambda"
+ },
+ "line1": {
+ "from": "icon1",
+ "to": "icon2",
+ "label": "Request by tenant"
+ }
+ }
+}
diff --git a/apigw-lambda-tenant-isolation/diagram/architecture.png b/apigw-lambda-tenant-isolation/diagram/architecture.png
new file mode 100644
index 000000000..d99cf8f2a
Binary files /dev/null and b/apigw-lambda-tenant-isolation/diagram/architecture.png differ
diff --git a/apigw-lambda-tenant-isolation/example-pattern.json b/apigw-lambda-tenant-isolation/example-pattern.json
new file mode 100644
index 000000000..9fde4580f
--- /dev/null
+++ b/apigw-lambda-tenant-isolation/example-pattern.json
@@ -0,0 +1,59 @@
+{
+ "title": "Multi-tenant API with AWS Lambda functions tenant isolation",
+ "description": "This sample project demonstrates tenant isolation mode of AWS Lambda functions.",
+ "language": "Python",
+ "level": "200",
+ "framework": "AWS SAM",
+ "introBox": {
+ "headline": "How it works",
+ "text": [
+ "Amazon API Gateway receives the HTTP GET request with tenant id in the header x-tenant-id.",
+ "The API Gateway triggers either the standard or the tenant isolated Lambda functions depending on the URI.",
+ "Observe the counter variable value between standard and tenant isolation mode enabled Lambda functions are you invoke the APIs for different tenant."
+ ]
+ },
+ "gitHub": {
+ "template": {
+ "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-lambda-tenant-isolation",
+ "templateURL": "serverless-patterns/apigw-lambda-tenant-isolation",
+ "projectFolder": "apigw-lambda-tenant-isolation",
+ "templateFile": "template.yaml"
+ }
+ },
+ "resources": {
+ "bullets": [
+ {
+ "text": "AWS Lambda tenant isolation",
+ "link": "https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html"
+ },
+ {
+ "text": "AWS Blog - Building multi-tenant SaaS applications with AWS Lambda’s new tenant isolation mode",
+ "link": "https://aws.amazon.com/blogs/compute/building-multi-tenant-saas-applications-with-aws-lambdas-new-tenant-isolation-mode/"
+ }
+ ]
+ },
+ "deploy": {
+ "text": [
+ "sam build",
+ "sam deploy --guided"
+ ]
+ },
+ "testing": {
+ "text": [
+ "See the GitHub repo for detailed testing instructions."
+ ]
+ },
+ "cleanup": {
+ "text": [
+ "Delete the stack: sam delete."
+ ]
+ },
+ "authors": [
+ {
+ "name": "Biswanath Mukherjee",
+ "image": "https://serverlessland.com/assets/images/resources/contributors/biswanath-mukherjee.jpg",
+ "bio": "I am a Sr. Solutions Architect working at AWS India. I help strategic global enterprise customer to architect their workload to run on AWS.",
+ "linkedin": "biswanathmukherjee"
+ }
+ ]
+}
diff --git a/apigw-lambda-tenant-isolation/src/isolated/lambda_function.py b/apigw-lambda-tenant-isolation/src/isolated/lambda_function.py
new file mode 100644
index 000000000..42ecba1ff
--- /dev/null
+++ b/apigw-lambda-tenant-isolation/src/isolated/lambda_function.py
@@ -0,0 +1,143 @@
+import json
+import logging
+
+# Configure logging
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+
+# In-memory counter that persists across invocations within the same execution environment
+# With tenant isolation, each tenant gets their own separate instance of this counter
+counter = 0
+
+def lambda_handler(event, context):
+ """
+ Tenant-isolated Lambda function handler with tenant isolation enabled.
+ Maintains separate counters for each tenant using context.tenant_id.
+
+ Args:
+ event: API Gateway event containing request information
+ context: Lambda context object with tenant_id attribute
+
+ Returns:
+ dict: API Gateway response with tenant-specific counter value and tenant ID
+ """
+ global counter
+
+ try:
+ # Validate event structure first
+ if not isinstance(event, dict):
+ logger.error("Invalid event structure: event is not a dictionary")
+ return create_error_response(400, 'Bad Request', 'Invalid request format')
+
+ # Log the incoming request (sanitized for security)
+ logger.info(f"Processing isolated request - Method: {event.get('httpMethod', 'UNKNOWN')}, Path: {event.get('path', 'UNKNOWN')}")
+
+ # Check if this is a GET request
+ http_method = event.get('httpMethod', '')
+ if not http_method:
+ logger.error("Missing httpMethod in event")
+ return create_error_response(400, 'Bad Request', 'Missing HTTP method in request')
+
+ if http_method != 'GET':
+ logger.warning(f"Unsupported HTTP method: {http_method}")
+ return create_error_response(405, 'Method Not Allowed', f'HTTP method {http_method} is not supported. Only GET requests are allowed.')
+
+ # Validate path (optional but good practice)
+ path = event.get('path', '')
+ if path and not path.endswith('/isolated'):
+ logger.warning(f"Unexpected path: {path}")
+
+ # Get tenant ID from Lambda context
+ # AWS Lambda provides tenant_id in the context when tenant isolation is enabled
+ tenant_id = getattr(context, 'tenant_id', None)
+
+ # Enhanced tenant ID validation
+ if not tenant_id:
+ logger.error("Missing tenant ID in Lambda context")
+ return create_error_response(400, 'Missing Tenant ID', 'Tenant ID is required for isolated function calls. Ensure x-tenant-id header is provided.')
+
+ # Validate tenant ID format (basic validation)
+ if not isinstance(tenant_id, str) or len(tenant_id.strip()) == 0:
+ logger.error(f"Invalid tenant ID format: {tenant_id}")
+ return create_error_response(400, 'Invalid Tenant ID', 'Tenant ID must be a non-empty string')
+
+ # Sanitize tenant ID for logging
+ tenant_id = tenant_id.strip()
+
+ # Validate tenant ID length (reasonable limit)
+ if len(tenant_id) > 100:
+ logger.error(f"Tenant ID too long: {len(tenant_id)} characters")
+ return create_error_response(400, 'Invalid Tenant ID', 'Tenant ID must be 100 characters or less')
+
+ # With tenant isolation, each tenant gets their own execution environment
+ # So this simple counter is automatically isolated per tenant by AWS Lambda
+ counter += 1
+
+ # Log the isolated behavior
+ logger.info(f"Tenant '{tenant_id}' using isolated counter value: {counter}")
+
+ # Prepare response body
+ response_body = {
+ 'counter': counter,
+ 'tenant_id': tenant_id,
+ 'isolation_enabled': True,
+ 'message': f'Counter incremented successfully for tenant {tenant_id}'
+ }
+
+ # Log the response
+ logger.info(f"Returning isolated counter value {counter} for tenant {tenant_id}")
+
+ # Return successful response
+ return {
+ 'statusCode': 200,
+ 'headers': {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ },
+ 'body': json.dumps(response_body)
+ }
+
+ except json.JSONDecodeError as e:
+ logger.error(f"JSON decode error: {str(e)}")
+ return create_error_response(400, 'Bad Request', 'Invalid JSON in request')
+ except KeyError as e:
+ logger.error(f"Missing required field: {str(e)}")
+ return create_error_response(400, 'Bad Request', f'Missing required field: {str(e)}')
+ except ValueError as e:
+ logger.error(f"Value error: {str(e)}")
+ return create_error_response(400, 'Bad Request', f'Invalid value: {str(e)}')
+ except AttributeError as e:
+ logger.error(f"Context attribute error: {str(e)}")
+ return create_error_response(500, 'Configuration Error', 'Lambda function configuration error')
+ except Exception as e:
+ # Log the error with more context
+ logger.error(f"Unexpected error processing isolated request: {str(e)}", exc_info=True)
+
+ # Return generic error response for security
+ return create_error_response(500, 'Internal Server Error', 'An unexpected error occurred while processing the request')
+
+
+def create_error_response(status_code, error_type, message):
+ """
+ Create a standardized error response.
+
+ Args:
+ status_code (int): HTTP status code
+ error_type (str): Type of error
+ message (str): Error message
+
+ Returns:
+ dict: Standardized error response
+ """
+ return {
+ 'statusCode': status_code,
+ 'headers': {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ },
+ 'body': json.dumps({
+ 'error': error_type,
+ 'message': message,
+ 'statusCode': status_code
+ })
+ }
\ No newline at end of file
diff --git a/apigw-lambda-tenant-isolation/src/standard/lambda_function.py b/apigw-lambda-tenant-isolation/src/standard/lambda_function.py
new file mode 100644
index 000000000..4a0ef4424
--- /dev/null
+++ b/apigw-lambda-tenant-isolation/src/standard/lambda_function.py
@@ -0,0 +1,133 @@
+import json
+import logging
+
+# Configure logging
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+
+# In-memory counter that persists across invocations within the same execution environment
+counter = 0
+
+def lambda_handler(event, context):
+ """
+ Standard Lambda function handler without tenant isolation.
+ Maintains a shared counter across all invocations within the same execution environment.
+ This function receives tenant headers but CANNOT isolate tenants - demonstrating the problem.
+
+ Args:
+ event: API Gateway event containing request information
+ context: Lambda context object
+
+ Returns:
+ dict: API Gateway response with counter value and isolation status
+ """
+ global counter
+
+ try:
+ # Validate event structure first
+ if not isinstance(event, dict):
+ logger.error("Invalid event structure: event is not a dictionary")
+ return create_error_response(400, 'Bad Request', 'Invalid request format')
+
+ # Extract tenant ID from headers (for demonstration purposes)
+ headers = event.get('headers', {}) or {}
+ # API Gateway may pass headers in different cases, so check both
+ tenant_id_from_header = (
+ headers.get('tenant-id') or
+ headers.get('Tenant-Id') or
+ headers.get('TENANT-ID')
+ )
+
+ logger.info(f"Received header: {headers}")
+
+ # Log the incoming request with tenant information
+ logger.info(f"Processing standard request - Method: {event.get('httpMethod', 'UNKNOWN')}, Path: {event.get('path', 'UNKNOWN')}, Tenant Header: {tenant_id_from_header}")
+
+ # Check if this is a GET request
+ http_method = event.get('httpMethod', '')
+ if not http_method:
+ logger.error("Missing httpMethod in event")
+ return create_error_response(400, 'Bad Request', 'Missing HTTP method in request')
+
+ if http_method != 'GET':
+ logger.warning(f"Unsupported HTTP method: {http_method}")
+ return create_error_response(405, 'Method Not Allowed', f'HTTP method {http_method} is not supported. Only GET requests are allowed.')
+
+ # Validate path (optional but good practice)
+ path = event.get('path', '')
+ if path and not path.endswith('/standard'):
+ logger.warning(f"Unexpected path: {path}")
+
+ # CRITICAL DEMONSTRATION: Without tenant isolation, all tenants share the same counter!
+ # This is the problem that tenant isolation solves
+ counter += 1
+
+ # Log the problematic behavior
+ if tenant_id_from_header:
+ logger.warning(f"PROBLEM: Tenant '{tenant_id_from_header}' is using shared counter value {counter}! This demonstrates data leakage between tenants.")
+ else:
+ logger.info(f"Request without tenant header using shared counter value: {counter}")
+
+ # Prepare response body - showing the received tenant header but no isolation
+ response_body = {
+ 'counter': counter,
+ 'tenant_id': tenant_id_from_header, # Show what tenant was requested
+ 'isolation_enabled': False,
+ 'message': f'Counter incremented successfully - SHARED across all tenants! (Received tenant: {tenant_id_from_header or "none"})',
+ 'warning': 'This function does NOT provide tenant isolation - all tenants share the same counter!'
+ }
+
+ # Log the response with warning
+ logger.info(f"Returning SHARED counter value {counter} for requested tenant '{tenant_id_from_header}' - NO ISOLATION!")
+
+ # Return successful response
+ return {
+ 'statusCode': 200,
+ 'headers': {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ },
+ 'body': json.dumps(response_body)
+ }
+
+ except json.JSONDecodeError as e:
+ logger.error(f"JSON decode error: {str(e)}")
+ return create_error_response(400, 'Bad Request', 'Invalid JSON in request')
+ except KeyError as e:
+ logger.error(f"Missing required field: {str(e)}")
+ return create_error_response(400, 'Bad Request', f'Missing required field: {str(e)}')
+ except ValueError as e:
+ logger.error(f"Value error: {str(e)}")
+ return create_error_response(400, 'Bad Request', f'Invalid value: {str(e)}')
+ except Exception as e:
+ # Log the error with more context
+ logger.error(f"Unexpected error processing request: {str(e)}", exc_info=True)
+
+ # Return generic error response for security
+ return create_error_response(500, 'Internal Server Error', 'An unexpected error occurred while processing the request')
+
+
+def create_error_response(status_code, error_type, message):
+ """
+ Create a standardized error response.
+
+ Args:
+ status_code (int): HTTP status code
+ error_type (str): Type of error
+ message (str): Error message
+
+ Returns:
+ dict: Standardized error response
+ """
+ return {
+ 'statusCode': status_code,
+ 'headers': {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ },
+ 'body': json.dumps({
+ 'error': error_type,
+ 'message': message,
+ 'statusCode': status_code
+ })
+ }
\ No newline at end of file
diff --git a/apigw-lambda-tenant-isolation/template.yaml b/apigw-lambda-tenant-isolation/template.yaml
new file mode 100644
index 000000000..c12ce792d
--- /dev/null
+++ b/apigw-lambda-tenant-isolation/template.yaml
@@ -0,0 +1,323 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: >
+ Lambda Tenant Isolation Demo
+
+ Demonstration project showcasing AWS Lambda's tenant isolation feature
+ with two Lambda functions - one with tenant isolation enabled and one without. (uksb-1tthgi812) (tag:apigw-lambda-tenant-isolation)
+
+# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
+Globals:
+ Function:
+ Timeout: 30
+ MemorySize: 128
+ Runtime: python3.14
+
+Parameters:
+ Environment:
+ Type: String
+ Default: dev
+ Description: Environment name for resource naming
+ AllowedValues:
+ - dev
+ - staging
+ - prod
+
+Resources:
+ # IAM Execution Role for Lambda Functions
+ LambdaExecutionRole:
+ Type: AWS::IAM::Role
+ Properties:
+ RoleName: !Sub "${AWS::StackName}-lambda-execution-role"
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service:
+ - lambda.amazonaws.com
+ Action:
+ - sts:AssumeRole
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ Policies:
+ - PolicyName: CloudWatchLogsPolicy
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - logs:CreateLogGroup
+ - logs:CreateLogStream
+ - logs:PutLogEvents
+ Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"
+
+ # Standard Lambda Function (without tenant isolation)
+ CounterStandardFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ FunctionName: !Sub "${AWS::StackName}-counter-standard"
+ CodeUri: src/standard/
+ Handler: lambda_function.lambda_handler
+ Role: !GetAtt LambdaExecutionRole.Arn
+ Description: Lambda function without tenant isolation for counter demonstration
+ Environment:
+ Variables:
+ LOG_LEVEL: INFO
+ Events:
+ Api:
+ Type: Api
+ Properties:
+ RestApiId: !Ref ApiGateway
+ Path: /standard
+ Method: get
+
+ # CloudWatch Log Group for Standard Function
+ CounterStandardLogGroup:
+ Type: AWS::Logs::LogGroup
+ Properties:
+ LogGroupName: !Sub "/aws/lambda/${AWS::StackName}-counter-standard"
+ RetentionInDays: 14
+
+ # Tenant-Isolated Lambda Function (with tenant isolation enabled)
+ CounterIsolatedFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ FunctionName: !Sub "${AWS::StackName}-counter-isolated"
+ CodeUri: src/isolated/
+ Handler: lambda_function.lambda_handler
+ Role: !GetAtt LambdaExecutionRole.Arn
+ Description: Lambda function with tenant isolation enabled for counter demonstration
+ TenancyConfig:
+ TenantIsolationMode: PER_TENANT
+ Environment:
+ Variables:
+ LOG_LEVEL: INFO
+
+ # CloudWatch Log Group for Isolated Function
+ CounterIsolatedLogGroup:
+ Type: AWS::Logs::LogGroup
+ Properties:
+ LogGroupName: !Sub "/aws/lambda/${AWS::StackName}-counter-isolated"
+ RetentionInDays: 14
+
+ # API Gateway REST API
+ ApiGateway:
+ Type: AWS::ApiGateway::RestApi
+ Properties:
+ Name: !Sub "${AWS::StackName}-api"
+ Description: API Gateway for Lambda tenant isolation demonstration
+ EndpointConfiguration:
+ Types:
+ - REGIONAL
+ SecurityPolicy: SecurityPolicy_TLS13_1_3_2025_09
+ EndpointAccessMode: BASIC
+
+ # Request Validator for API Gateway
+ RequestValidator:
+ Type: AWS::ApiGateway::RequestValidator
+ Properties:
+ RestApiId: !Ref ApiGateway
+ Name: !Sub "${AWS::StackName}-request-validator"
+ ValidateRequestParameters: true
+ ValidateRequestBody: false
+
+ # Error Model for API Gateway responses
+ ErrorModel:
+ Type: AWS::ApiGateway::Model
+ Properties:
+ RestApiId: !Ref ApiGateway
+ Name: ErrorModel
+ ContentType: application/json
+ Schema:
+ $schema: http://json-schema.org/draft-04/schema#
+ title: Error Schema
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: integer
+
+ # Standard endpoint resource
+ StandardResource:
+ Type: AWS::ApiGateway::Resource
+ Properties:
+ RestApiId: !Ref ApiGateway
+ ParentId: !GetAtt ApiGateway.RootResourceId
+ PathPart: standard
+
+ # Standard endpoint method (with optional header mapping for demonstration)
+ StandardMethod:
+ Type: AWS::ApiGateway::Method
+ Properties:
+ RestApiId: !Ref ApiGateway
+ ResourceId: !Ref StandardResource
+ HttpMethod: GET
+ AuthorizationType: NONE
+ RequestParameters:
+ method.request.header.tenant-id: false # Optional parameter
+ Integration:
+ Type: AWS_PROXY
+ IntegrationHttpMethod: POST
+ Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CounterStandardFunction.Arn}/invocations"
+ RequestParameters:
+ integration.request.header.X-Amz-Tenant-Id: method.request.header.tenant-id
+ MethodResponses:
+ - StatusCode: 200
+ ResponseModels:
+ application/json: Empty
+ - StatusCode: 405
+ ResponseModels:
+ application/json: !Ref ErrorModel
+ - StatusCode: 500
+ ResponseModels:
+ application/json: !Ref ErrorModel
+
+ # OPTIONS method for Standard endpoint (CORS support)
+ StandardOptionsMethod:
+ Type: AWS::ApiGateway::Method
+ Properties:
+ RestApiId: !Ref ApiGateway
+ ResourceId: !Ref StandardResource
+ HttpMethod: OPTIONS
+ AuthorizationType: NONE
+ Integration:
+ Type: MOCK
+ IntegrationResponses:
+ - StatusCode: 200
+ ResponseParameters:
+ method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
+ method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
+ method.response.header.Access-Control-Allow-Origin: "'*'"
+ ResponseTemplates:
+ application/json: ''
+ PassthroughBehavior: WHEN_NO_MATCH
+ RequestTemplates:
+ application/json: '{"statusCode": 200}'
+ MethodResponses:
+ - StatusCode: 200
+ ResponseParameters:
+ method.response.header.Access-Control-Allow-Headers: false
+ method.response.header.Access-Control-Allow-Methods: false
+ method.response.header.Access-Control-Allow-Origin: false
+
+ # Isolated endpoint resource
+ IsolatedResource:
+ Type: AWS::ApiGateway::Resource
+ Properties:
+ RestApiId: !Ref ApiGateway
+ ParentId: !GetAtt ApiGateway.RootResourceId
+ PathPart: isolated
+
+ # Isolated endpoint method with header mapping
+ IsolatedMethod:
+ Type: AWS::ApiGateway::Method
+ Properties:
+ RestApiId: !Ref ApiGateway
+ ResourceId: !Ref IsolatedResource
+ HttpMethod: GET
+ AuthorizationType: NONE
+ RequestParameters:
+ method.request.header.x-tenant-id: true # Required parameter
+ RequestValidatorId: !Ref RequestValidator
+ Integration:
+ Type: AWS_PROXY
+ IntegrationHttpMethod: POST
+ Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CounterIsolatedFunction.Arn}/invocations"
+ RequestParameters:
+ integration.request.header.X-Amz-Tenant-Id: method.request.header.x-tenant-id
+ MethodResponses:
+ - StatusCode: 200
+ ResponseModels:
+ application/json: Empty
+ - StatusCode: 400
+ ResponseModels:
+ application/json: !Ref ErrorModel
+ - StatusCode: 405
+ ResponseModels:
+ application/json: !Ref ErrorModel
+ - StatusCode: 500
+ ResponseModels:
+ application/json: !Ref ErrorModel
+
+ # OPTIONS method for Isolated endpoint (CORS support)
+ IsolatedOptionsMethod:
+ Type: AWS::ApiGateway::Method
+ Properties:
+ RestApiId: !Ref ApiGateway
+ ResourceId: !Ref IsolatedResource
+ HttpMethod: OPTIONS
+ AuthorizationType: NONE
+ Integration:
+ Type: MOCK
+ IntegrationResponses:
+ - StatusCode: 200
+ ResponseParameters:
+ method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,x-tenant-id'"
+ method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
+ method.response.header.Access-Control-Allow-Origin: "'*'"
+ ResponseTemplates:
+ application/json: ''
+ PassthroughBehavior: WHEN_NO_MATCH
+ RequestTemplates:
+ application/json: '{"statusCode": 200}'
+ MethodResponses:
+ - StatusCode: 200
+ ResponseParameters:
+ method.response.header.Access-Control-Allow-Headers: false
+ method.response.header.Access-Control-Allow-Methods: false
+ method.response.header.Access-Control-Allow-Origin: false
+
+ # Lambda permissions for API Gateway to invoke functions
+ StandardFunctionPermission:
+ Type: AWS::Lambda::Permission
+ Properties:
+ FunctionName: !Ref CounterStandardFunction
+ Action: lambda:InvokeFunction
+ Principal: apigateway.amazonaws.com
+ SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/*"
+
+ IsolatedFunctionPermission:
+ Type: AWS::Lambda::Permission
+ Properties:
+ FunctionName: !Ref CounterIsolatedFunction
+ Action: lambda:InvokeFunction
+ Principal: apigateway.amazonaws.com
+ SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/*"
+
+ # API Gateway Deployment
+ ApiGatewayDeployment:
+ Type: AWS::ApiGateway::Deployment
+ DependsOn:
+ - StandardMethod
+ - StandardOptionsMethod
+ - IsolatedMethod
+ - IsolatedOptionsMethod
+ Properties:
+ RestApiId: !Ref ApiGateway
+
+ # API Gateway Stage
+ ApiGatewayStage:
+ Type: AWS::ApiGateway::Stage
+ Properties:
+ RestApiId: !Ref ApiGateway
+ DeploymentId: !Ref ApiGatewayDeployment
+ StageName: !Ref Environment
+ Description: !Sub "Stage for ${Environment} environment"
+
+Outputs:
+ # API Gateway endpoint URLs
+ StandardMultiTenantAPIEndpointUrl:
+ Description: "URL for the standard Lambda function endpoint"
+ Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStage}/standard"
+ Export:
+ Name: !Sub "${AWS::StackName}-standard-url"
+
+ IsolatedTenantAPIEndpointUrl:
+ Description: "URL for the tenant-isolated Lambda function endpoint"
+ Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStage}/isolated"
+ Export:
+ Name: !Sub "${AWS::StackName}-isolated-url"