<!--- Provide a general summary of your changes in the Title above --> ## Issue \# <!--- If it fixes an issue, please link to the issue here --> https://github.com/awslabs/swift-aws-lambda-runtime/issues/607 ## Description of changes <!--- Why is this change required? What problem does it solve? --> The local HTTP server was not forwarding user‑provided headers to the runtime’s response. It passes all headers through to the runtime. This it makes local behavior match the Lambda runtime API contract and allows developers to opt into metadata by sending the appropriate runtime headers. ## New/existing dependencies impact assessment, if applicable <!--- No new dependencies were added to this change. --> <!--- If any dependency was added / modified / removed, THIRD-PARTY-LICENSES must be updated accordingly. --> N/A ## Conventional Commits <!--- Please use conventional commits to let us know what kind of change this is.--> <!--- More info can be found here: https://www.conventionalcommits.org/en/v1.0.0/--> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Sébastien Stormacq <sebastien.stormacq@gmail.com>
Multi-Tenant Lambda Function Example
This example demonstrates how to build a multi-tenant Lambda function using Swift and AWS Lambda's tenant isolation mode. Tenant isolation ensures that execution environments are dedicated to specific tenants, providing strict isolation for processing tenant-specific code or data.
Overview
This example implements a request tracking system that maintains separate counters and request histories for each tenant. The Lambda function:
- Accepts requests from multiple tenants via API Gateway (MultiTenant)
- Accepts direct JSON payloads without API Gateway (MultiTenantLocal)
- Maintains isolated execution environments per tenant
- Tracks request counts and timestamps for each tenant
- Returns tenant-specific data in JSON format
What is Tenant Isolation Mode?
AWS Lambda's tenant isolation mode routes requests to execution environments based on a customer-specified tenant identifier. This ensures that:
- Execution environments are never reused across different tenants - Each tenant gets dedicated execution environments
- Data isolation - Tenant-specific data remains isolated from other tenants
- Firecracker virtualization - Provides workload isolation at the infrastructure level
When to Use Tenant Isolation
Use tenant isolation mode when building multi-tenant applications that:
- Execute end-user supplied code - Limits the impact of potentially incorrect or malicious user code
- Process tenant-specific data - Prevents exposure of sensitive data to other tenants
- Require strict isolation guarantees - Such as SaaS platforms for workflow automation or code execution
Architecture
The example consists of:
-
TenantData - Immutable struct tracking tenant information:
tenantID: Unique identifier for the tenantrequestCount: Total number of requests from this tenantfirstRequest: Unix timestamp (seconds since epoch) of the first requestrequests: Array of individual request records
-
TenantDataStore - Actor-based storage providing thread-safe access to tenant data across invocations
-
Lambda Handler - Processes API Gateway requests and manages tenant data
-
MultiTenantLocal Handler - Processes plain JSON requests without API Gateway
Code Structure
// Immutable tenant data structure
struct TenantData: Codable {
let tenantID: String
let requestCount: Int
let firstRequest: String
let requests: [TenantRequest]
func addingRequest() -> TenantData {
// Returns new instance with incremented count
}
}
// Thread-safe tenant storage using Swift actors
actor TenantDataStore {
private var tenants: [String: TenantData] = [:]
subscript(id: String) -> TenantData? {
tenants[id]
}
func update(id: String, data: TenantData) {
tenants[id] = data
}
}
// Lambda handler extracts tenant ID from context
let runtime = LambdaRuntime {
(event: APIGatewayRequest, context: LambdaContext) -> APIGatewayResponse in
guard let tenantID = context.tenantID else {
return APIGatewayResponse(statusCode: .badRequest, body: "No Tenant ID provided")
}
// Process request for this tenant
let currentData = await tenants[tenantID] ?? TenantData(tenantID: tenantID)
let updatedData = currentData.addingRequest()
await tenants.update(id: tenantID, data: updatedData)
return try APIGatewayResponse(statusCode: .ok, encodableBody: updatedData)
}
Configuration
SAM Template (template.yaml)
The function is configured with tenant isolation mode and API Gateway parameter mapping in the SAM template:
# API Gateway REST API with parameter mapping
MultiTenantApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
DefinitionBody:
openapi: 3.0.1
paths:
/:
get:
parameters:
- name: tenant-id
in: query
required: true
x-amazon-apigateway-integration:
type: aws_proxy
httpMethod: POST
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MultiTenantLambda.Arn}/invocations
# Map query parameter to Lambda tenant header
requestParameters:
integration.request.header.X-Amz-Tenant-Id: method.request.querystring.tenant-id
# Lambda function with tenant isolation
MultiTenantLambda:
Type: AWS::Serverless::Function
Properties:
Runtime: provided.al2023
Architectures:
- arm64
# Enable tenant isolation mode
TenancyConfig:
TenantIsolationMode: PER_TENANT
Key Configuration Points
- TenancyConfig.TenantIsolationMode: Set to
PER_TENANTto enable tenant isolation - Parameter Mapping: API Gateway maps the
tenant-idquery parameter to theX-Amz-Tenant-Idheader required by Lambda - REST API: Uses REST API (not HTTP API) to support request parameter mapping
- OpenAPI Definition: Defines the integration using OpenAPI 3.0 specification for fine-grained control
- Immutable property: Tenant isolation can only be enabled when creating a new function
- Required tenant-id: All invocations must include a tenant identifier
Why Parameter Mapping is Required
Lambda's tenant isolation feature requires the tenant ID to be passed via the X-Amz-Tenant-Id header. When using API Gateway:
- Client sends request with
tenant-idas a query parameter - API Gateway transforms the query parameter into the
X-Amz-Tenant-Idheader - Lambda receives the header and routes to the appropriate tenant-isolated environment
This mapping is configured in the x-amazon-apigateway-integration section using:
requestParameters:
integration.request.header.X-Amz-Tenant-Id: method.request.querystring.tenant-id
Deployment
Prerequisites
- Swift (>=6.2)
- Docker (for cross-compilation to Amazon Linux)
- AWS SAM CLI (>=1.147.1)
- AWS CLI configured with appropriate credentials
Build and Deploy
-
Build the Lambda function:
swift package archive --allow-network-connections docker -
Deploy using SAM:
sam deploy --guided -
Note the API Gateway endpoint from the CloudFormation outputs
Testing
Local Testing (No API Gateway)
Run the simple handler locally and invoke it directly using the local server. This example does not depend on API Gateway or its request/response types.
-
Start the local server:
swift run MultiTenantLocal -
Invoke with a tenant header and a JSON body:
# Test multi-tenant function locally curl -X POST http://127.0.0.1:7000/invoke \ -H "Content-Type: application/json" \ -H "Lambda-Runtime-Aws-Tenant-Id: tenant-123" \ -d '{"message" : "hello"}'
Using API Gateway
The tenant ID is passed as a query parameter. API Gateway automatically maps it to the X-Amz-Tenant-Id header:
# Request from tenant "alice"
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com/Prod?tenant-id=alice"
# Request from tenant "bob"
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com/Prod?tenant-id=bob"
# Multiple requests from the same tenant will reuse the execution environment
for i in {1..5}; do
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com/Prod?tenant-id=alice"
done
Using AWS CLI (Direct Lambda Invocation)
For direct Lambda invocation without API Gateway:
# Synchronous invocation
aws lambda invoke \
--function-name MultiTenantLambda \
--tenant-id alice \
response.json
# View the response
cat response.json
Expected Response
{
"tenantID": "alice",
"requestCount": 3,
"firstRequest": "1705320000.123456",
"requests": [
{
"requestNumber": 1,
"timestamp": "1705320000.123456"
},
{
"requestNumber": 2,
"timestamp": "1705320075.789012"
},
{
"requestNumber": 3,
"timestamp": "1705320150.345678"
}
]
}
Note: Timestamps are Unix epoch times (seconds since January 1, 1970) for cross-platform compatibility.
How Tenant Isolation Works
- Request arrives with a tenant identifier (via query parameter, header, or direct invocation)
- Lambda routes the request to an execution environment dedicated to that tenant
- Environment reuse - Subsequent requests from the same tenant reuse the same environment (warm start)
- Isolation guarantee - Execution environments are never shared between different tenants
- Data persistence - Tenant data persists in memory across invocations within the same execution environment
Important Considerations
Concurrency and Scaling
- Lambda imposes a limit of 2,500 tenant-isolated execution environments (active or idle) for every 1,000 concurrent executions
- Each tenant can scale independently based on their request volume
- Cold starts occur more frequently due to tenant-specific environments
Pricing
- Standard Lambda pricing applies (compute time and requests)
- Additional charge when Lambda creates a new tenant-isolated execution environment
- Price depends on allocated memory and CPU architecture
- See AWS Lambda Pricing for details
Limitations
Tenant isolation mode is not supported with:
- Function URLs
- Provisioned concurrency
- SnapStart
Supported Invocation Methods
- ✅ Synchronous invocations
- ✅ Asynchronous invocations
- ✅ API Gateway event triggers
- ✅ AWS SDK invocations
Security Best Practices
- Execution role applies to all tenants - Use IAM policies to restrict access to tenant-specific resources
- Validate tenant identifiers - Ensure tenant IDs are properly authenticated and authorized
- Implement tenant-aware logging - Include tenant ID in CloudWatch logs for audit trails
- Set appropriate timeouts - Configure function timeout based on expected workload
- Monitor per-tenant metrics - Use CloudWatch to track invocations, errors, and duration per tenant
Monitoring
CloudWatch Metrics
Lambda automatically publishes metrics with tenant dimensions:
Invocations- Number of invocations per tenantDuration- Execution time per tenantErrors- Error count per tenantThrottles- Throttled requests per tenant
Accessing Metrics
# Get invocation count for a specific tenant
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value=MultiTenant Name=TenantId,Value=alice \
--start-time 2024-01-15T00:00:00Z \
--end-time 2024-01-15T23:59:59Z \
--period 3600 \
--statistics Sum
Learn More
- AWS Lambda Tenant Isolation Documentation
- Configuring Tenant Isolation
- Invoking Tenant-Isolated Functions
- AWS Blog: Streamlined Multi-Tenant Application Development
- Swift AWS Lambda Runtime
License
This example is part of the Swift AWS Lambda Runtime project and is licensed under Apache License 2.0.