diff --git a/apigw-lambda-tenant-isolation/README.md b/apigw-lambda-tenant-isolation/README.md new file mode 100644 index 000000000..2833b22f9 --- /dev/null +++ b/apigw-lambda-tenant-isolation/README.md @@ -0,0 +1,208 @@ +# Multi-tenant API with AWS Lambda functions 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 +- [Maven 3.8.6 or above](https://maven.apache.org/download.cgi) 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 + ``` + +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 tenant isolation mode enabled and another disabled. + +![End to End Architecture](diagram/architecture.png) + +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, it impacts the other tenant (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 "x-tenant-id: BlueTenant" "$STANDARD_URL" +``` + +Response: + +```bash +{ + "counter": 1, + "tenant_id_received": "BlueTenant", + "tenant_id": null, + "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_received": "RedTenant", + "tenant_id": null, + "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_received": "GreenTenant", + "tenant_id": null, + "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/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..73a7f66d0 --- /dev/null +++ b/apigw-lambda-tenant-isolation/src/standard/lambda_function.py @@ -0,0 +1,132 @@ +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('x-tenant-id') or + headers.get('X-Tenant-Id') or + headers.get('X-TENANT-ID') + ) + + # 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_received': tenant_id_from_header, # Show what tenant was requested + 'tenant_id': None, # But no actual tenant isolation + '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..f59f15e2f --- /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. + +# 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.x-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.x-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"