Skip to content

Commit 2abe7eb

Browse files
sebstoSebastien Stormacq0xTim
authored
Add support for Lambda Tenants (#608)
Address #605 NEW Lambda Tenant isolation capability: https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html # Add Support for Lambda Tenant Isolation Mode ## Summary This PR adds support for AWS Lambda's tenant isolation mode to the Swift AWS Lambda Runtime, enabling developers to build multi-tenant applications with strict execution environment isolation per tenant. ## Changes ### Runtime Support - Added `tenantID` property to `LambdaContext` to expose the tenant identifier - Extended `InvocationMetadata` to capture the `Lambda-Runtime-Aws-Tenant-Id` header - Added `AmazonHeaders.tenantID` constant for the tenant ID header - Added trace logging for invocation headers to aid debugging ### New Example: MultiTenant A complete working example demonstrating tenant isolation mode: - **Request tracking system** that maintains separate counters and histories per tenant - **Actor-based storage** (`TenantDataStore`) for thread-safe tenant data management - **Immutable data structures** (`TenantData`) following Swift best practices - **API Gateway integration** with tenant ID passed via query parameter - **SAM template** configured with `TenancyConfig.TenantIsolationMode: PER_TENANT` - **Comprehensive documentation** covering architecture, deployment, testing, and best practices ### Testing - Added unit test for tenant ID extraction from invocation headers - Integrated MultiTenant example into CI/CD pipeline ### Documentation The example includes detailed documentation on: - When to use tenant isolation (user code execution, sensitive data processing) - How tenant isolation works (dedicated environments, no cross-tenant reuse) - Concurrency limits and scaling considerations - Pricing implications - Security best practices - CloudWatch monitoring with tenant dimensions ## Files Changed - `Sources/AWSLambdaRuntime/LambdaContext.swift` - Added tenantID property - `Sources/AWSLambdaRuntime/ControlPlaneRequest.swift` - Capture tenant ID from headers - `Sources/AWSLambdaRuntime/Utils.swift` - Added tenantID header constant - `Sources/AWSLambdaRuntime/Lambda.swift` - Pass tenant ID to context - `Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift` - Added trace logging - `Tests/AWSLambdaRuntimeTests/InvocationTests.swift` - Added tenant ID test - `Examples/MultiTenant/*` - New complete example with SAM template - `.github/workflows/pull_request.yml` - Added MultiTenant to CI pipeline ## Testing Instructions 1. Build and deploy the example: bash cd Examples/MultiTenant swift package archive --allow-network-connections docker sam deploy --guided 2. Test with different tenants: bash curl "https://<api-id>.execute-api.<region>.amazonaws.com/Prod?tenant-id= alice" curl "https://<api-id>.execute-api.<region>.amazonaws.com/Prod?tenant-id= bob" 3. Verify isolation by checking that each tenant maintains separate request counts ## Related Documentation - [AWS Lambda Tenant Isolation](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html) - [AWS Blog: Streamlined Multi-Tenant Application Development](https://aws.amazon.com/blogs/aws/streamlined-multi-tenant-application-development-with-tenant-isolation-mode-in-aws-lambda/) --------- Co-authored-by: Sebastien Stormacq <[email protected]> Co-authored-by: Tim Condon <[email protected]>
1 parent 0305cb3 commit 2abe7eb

File tree

15 files changed

+680
-2
lines changed

15 files changed

+680
-2
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
# We pass the list of examples here, but we can't pass an array as argument
4040
# Instead, we pass a String with a valid JSON array.
4141
# The workaround is mentioned here https://github.com/orgs/community/discussions/11692
42-
examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
42+
examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
4343
archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]"
4444
archive_plugin_enabled: true
4545

Examples/HelloWorldNoTraits/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ response.json
22
samconfig.toml
33
template.yaml
44
Makefile
5+
Dockerfile

Examples/MultiTenant/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
response.json
2+
samconfig.toml
3+
Makefile

Examples/MultiTenant/Package.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// swift-tools-version:6.2
2+
3+
import PackageDescription
4+
5+
// needed for CI to test the local version of the library
6+
import struct Foundation.URL
7+
8+
let package = Package(
9+
name: "swift-aws-lambda-runtime-example",
10+
platforms: [.macOS(.v15)],
11+
products: [
12+
.executable(name: "MultiTenant", targets: ["MultiTenant"])
13+
],
14+
dependencies: [
15+
// during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below
16+
.package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"),
17+
.package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"),
18+
],
19+
targets: [
20+
.executableTarget(
21+
name: "MultiTenant",
22+
dependencies: [
23+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
24+
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
25+
]
26+
)
27+
]
28+
)
29+
30+
if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"],
31+
localDepsPath != "",
32+
let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]),
33+
v.isDirectory == true
34+
{
35+
// when we use the local runtime as deps, let's remove the dependency added above
36+
let indexToRemove = package.dependencies.firstIndex { dependency in
37+
if case .sourceControl(
38+
name: _,
39+
location: "https://github.com/awslabs/swift-aws-lambda-runtime.git",
40+
requirement: _
41+
) = dependency.kind {
42+
return true
43+
}
44+
return false
45+
}
46+
if let indexToRemove {
47+
package.dependencies.remove(at: indexToRemove)
48+
}
49+
50+
// then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..)
51+
print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)")
52+
package.dependencies += [
53+
.package(name: "swift-aws-lambda-runtime", path: localDepsPath)
54+
]
55+
}

Examples/MultiTenant/README.md

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
# Multi-Tenant Lambda Function Example
2+
3+
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.
4+
5+
## Overview
6+
7+
This example implements a request tracking system that maintains separate counters and request histories for each tenant. The Lambda function:
8+
9+
- Accepts requests from multiple tenants via API Gateway
10+
- Maintains isolated execution environments per tenant
11+
- Tracks request counts and timestamps for each tenant
12+
- Returns tenant-specific data in JSON format
13+
14+
## What is Tenant Isolation Mode?
15+
16+
AWS Lambda's tenant isolation mode routes requests to execution environments based on a customer-specified tenant identifier. This ensures that:
17+
18+
- **Execution environments are never reused across different tenants** - Each tenant gets dedicated execution environments
19+
- **Data isolation** - Tenant-specific data remains isolated from other tenants
20+
- **Firecracker virtualization** - Provides workload isolation at the infrastructure level
21+
22+
### When to Use Tenant Isolation
23+
24+
Use tenant isolation mode when building multi-tenant applications that:
25+
26+
- **Execute end-user supplied code** - Limits the impact of potentially incorrect or malicious user code
27+
- **Process tenant-specific data** - Prevents exposure of sensitive data to other tenants
28+
- **Require strict isolation guarantees** - Such as SaaS platforms for workflow automation or code execution
29+
30+
## Architecture
31+
32+
The example consists of:
33+
34+
1. **TenantData** - Immutable struct tracking tenant information:
35+
- `tenantID`: Unique identifier for the tenant
36+
- `requestCount`: Total number of requests from this tenant
37+
- `firstRequest`: Unix timestamp (seconds since epoch) of the first request
38+
- `requests`: Array of individual request records
39+
40+
2. **TenantDataStore** - Actor-based storage providing thread-safe access to tenant data across invocations
41+
42+
3. **Lambda Handler** - Processes API Gateway requests and manages tenant data
43+
44+
## Code Structure
45+
46+
```swift
47+
// Immutable tenant data structure
48+
struct TenantData: Codable {
49+
let tenantID: String
50+
let requestCount: Int
51+
let firstRequest: String
52+
let requests: [TenantRequest]
53+
54+
func addingRequest() -> TenantData {
55+
// Returns new instance with incremented count
56+
}
57+
}
58+
59+
// Thread-safe tenant storage using Swift actors
60+
actor TenantDataStore {
61+
private var tenants: [String: TenantData] = [:]
62+
63+
subscript(id: String) -> TenantData? {
64+
tenants[id]
65+
}
66+
67+
func update(id: String, data: TenantData) {
68+
tenants[id] = data
69+
}
70+
}
71+
72+
// Lambda handler extracts tenant ID from context
73+
let runtime = LambdaRuntime {
74+
(event: APIGatewayRequest, context: LambdaContext) -> APIGatewayResponse in
75+
76+
guard let tenantID = context.tenantID else {
77+
return APIGatewayResponse(statusCode: .badRequest, body: "No Tenant ID provided")
78+
}
79+
80+
// Process request for this tenant
81+
let currentData = await tenants[tenantID] ?? TenantData(tenantID: tenantID)
82+
let updatedData = currentData.addingRequest()
83+
await tenants.update(id: tenantID, data: updatedData)
84+
85+
return try APIGatewayResponse(statusCode: .ok, encodableBody: updatedData)
86+
}
87+
```
88+
89+
## Configuration
90+
91+
### SAM Template (template.yaml)
92+
93+
The function is configured with tenant isolation mode and API Gateway parameter mapping in the SAM template:
94+
95+
```yaml
96+
# API Gateway REST API with parameter mapping
97+
MultiTenantApi:
98+
Type: AWS::Serverless::Api
99+
Properties:
100+
StageName: Prod
101+
DefinitionBody:
102+
openapi: 3.0.1
103+
paths:
104+
/:
105+
get:
106+
parameters:
107+
- name: tenant-id
108+
in: query
109+
required: true
110+
x-amazon-apigateway-integration:
111+
type: aws_proxy
112+
httpMethod: POST
113+
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MultiTenantLambda.Arn}/invocations
114+
# Map query parameter to Lambda tenant header
115+
requestParameters:
116+
integration.request.header.X-Amz-Tenant-Id: method.request.querystring.tenant-id
117+
118+
# Lambda function with tenant isolation
119+
MultiTenantLambda:
120+
Type: AWS::Serverless::Function
121+
Properties:
122+
Runtime: provided.al2023
123+
Architectures:
124+
- arm64
125+
# Enable tenant isolation mode
126+
TenancyConfig:
127+
TenantIsolationMode: PER_TENANT
128+
```
129+
130+
### Key Configuration Points
131+
132+
- **TenancyConfig.TenantIsolationMode**: Set to `PER_TENANT` to enable tenant isolation
133+
- **Parameter Mapping**: API Gateway maps the `tenant-id` query parameter to the `X-Amz-Tenant-Id` header required by Lambda
134+
- **REST API**: Uses REST API (not HTTP API) to support request parameter mapping
135+
- **OpenAPI Definition**: Defines the integration using OpenAPI 3.0 specification for fine-grained control
136+
- **Immutable property**: Tenant isolation can only be enabled when creating a new function
137+
- **Required tenant-id**: All invocations must include a tenant identifier
138+
139+
### Why Parameter Mapping is Required
140+
141+
Lambda's tenant isolation feature requires the tenant ID to be passed via the `X-Amz-Tenant-Id` header. When using API Gateway:
142+
143+
1. **Client sends request** with `tenant-id` as a query parameter
144+
2. **API Gateway transforms** the query parameter into the `X-Amz-Tenant-Id` header
145+
3. **Lambda receives** the header and routes to the appropriate tenant-isolated environment
146+
147+
This mapping is configured in the `x-amazon-apigateway-integration` section using:
148+
```yaml
149+
requestParameters:
150+
integration.request.header.X-Amz-Tenant-Id: method.request.querystring.tenant-id
151+
```
152+
153+
## Deployment
154+
155+
### Prerequisites
156+
157+
- Swift (>=6.2)
158+
- Docker (for cross-compilation to Amazon Linux)
159+
- AWS SAM CLI (>=1.147.1)
160+
- AWS CLI configured with appropriate credentials
161+
162+
### Build and Deploy
163+
164+
1. **Build the Lambda function**:
165+
```bash
166+
swift package archive --allow-network-connections docker
167+
```
168+
169+
2. **Deploy using SAM**:
170+
```bash
171+
sam deploy --guided
172+
```
173+
174+
3. **Note the API Gateway endpoint** from the CloudFormation outputs
175+
176+
## Testing
177+
178+
### Using API Gateway
179+
180+
The tenant ID is passed as a query parameter. API Gateway automatically maps it to the `X-Amz-Tenant-Id` header:
181+
182+
```bash
183+
# Request from tenant "alice"
184+
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com/Prod?tenant-id=alice"
185+
186+
# Request from tenant "bob"
187+
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com/Prod?tenant-id=bob"
188+
189+
# Multiple requests from the same tenant will reuse the execution environment
190+
for i in {1..5}; do
191+
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com/Prod?tenant-id=alice"
192+
done
193+
```
194+
195+
### Using AWS CLI (Direct Lambda Invocation)
196+
197+
For direct Lambda invocation without API Gateway:
198+
199+
```bash
200+
# Synchronous invocation
201+
aws lambda invoke \
202+
--function-name MultiTenantLambda \
203+
--tenant-id alice \
204+
response.json
205+
206+
# View the response
207+
cat response.json
208+
```
209+
210+
### Expected Response
211+
212+
```json
213+
{
214+
"tenantID": "alice",
215+
"requestCount": 3,
216+
"firstRequest": "1705320000.123456",
217+
"requests": [
218+
{
219+
"requestNumber": 1,
220+
"timestamp": "1705320000.123456"
221+
},
222+
{
223+
"requestNumber": 2,
224+
"timestamp": "1705320075.789012"
225+
},
226+
{
227+
"requestNumber": 3,
228+
"timestamp": "1705320150.345678"
229+
}
230+
]
231+
}
232+
```
233+
234+
**Note**: Timestamps are Unix epoch times (seconds since January 1, 1970) for cross-platform compatibility.
235+
236+
## How Tenant Isolation Works
237+
238+
1. **Request arrives** with a tenant identifier (via query parameter, header, or direct invocation)
239+
2. **Lambda routes the request** to an execution environment dedicated to that tenant
240+
3. **Environment reuse** - Subsequent requests from the same tenant reuse the same environment (warm start)
241+
4. **Isolation guarantee** - Execution environments are never shared between different tenants
242+
5. **Data persistence** - Tenant data persists in memory across invocations within the same execution environment
243+
244+
## Important Considerations
245+
246+
### Concurrency and Scaling
247+
248+
- Lambda imposes a limit of **2,500 tenant-isolated execution environments** (active or idle) for every 1,000 concurrent executions
249+
- Each tenant can scale independently based on their request volume
250+
- Cold starts occur more frequently due to tenant-specific environments
251+
252+
### Pricing
253+
254+
- Standard Lambda pricing applies (compute time and requests)
255+
- **Additional charge** when Lambda creates a new tenant-isolated execution environment
256+
- Price depends on allocated memory and CPU architecture
257+
- See [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing) for details
258+
259+
### Limitations
260+
261+
Tenant isolation mode is **not supported** with:
262+
- Function URLs
263+
- Provisioned concurrency
264+
- SnapStart
265+
266+
### Supported Invocation Methods
267+
268+
- ✅ Synchronous invocations
269+
- ✅ Asynchronous invocations
270+
- ✅ API Gateway event triggers
271+
- ✅ AWS SDK invocations
272+
273+
## Security Best Practices
274+
275+
1. **Execution role applies to all tenants** - Use IAM policies to restrict access to tenant-specific resources
276+
2. **Validate tenant identifiers** - Ensure tenant IDs are properly authenticated and authorized
277+
3. **Implement tenant-aware logging** - Include tenant ID in CloudWatch logs for audit trails
278+
4. **Set appropriate timeouts** - Configure function timeout based on expected workload
279+
5. **Monitor per-tenant metrics** - Use CloudWatch to track invocations, errors, and duration per tenant
280+
281+
## Monitoring
282+
283+
### CloudWatch Metrics
284+
285+
Lambda automatically publishes metrics with tenant dimensions:
286+
287+
- `Invocations` - Number of invocations per tenant
288+
- `Duration` - Execution time per tenant
289+
- `Errors` - Error count per tenant
290+
- `Throttles` - Throttled requests per tenant
291+
292+
### Accessing Metrics
293+
294+
```bash
295+
# Get invocation count for a specific tenant
296+
aws cloudwatch get-metric-statistics \
297+
--namespace AWS/Lambda \
298+
--metric-name Invocations \
299+
--dimensions Name=FunctionName,Value=MultiTenant Name=TenantId,Value=alice \
300+
--start-time 2024-01-15T00:00:00Z \
301+
--end-time 2024-01-15T23:59:59Z \
302+
--period 3600 \
303+
--statistics Sum
304+
```
305+
306+
## Learn More
307+
308+
- [AWS Lambda Tenant Isolation Documentation](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html)
309+
- [Configuring Tenant Isolation](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-configure.html)
310+
- [Invoking Tenant-Isolated Functions](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-invoke.html)
311+
- [AWS Blog: Streamlined Multi-Tenant Application Development](https://aws.amazon.com/blogs/aws/streamlined-multi-tenant-application-development-with-tenant-isolation-mode-in-aws-lambda/)
312+
- [Swift AWS Lambda Runtime](https://github.com/swift-server/swift-aws-lambda-runtime)
313+
314+
## License
315+
316+
This example is part of the Swift AWS Lambda Runtime project and is licensed under Apache License 2.0.

0 commit comments

Comments
 (0)