Skip to content

Commit c806c41

Browse files
author
Sebastien Stormacq
committed
Add support for Lambda Tenants
1 parent 0305cb3 commit c806c41

File tree

14 files changed

+687
-1
lines changed

14 files changed

+687
-1
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: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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`: ISO 8601 timestamp 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: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in
75+
76+
guard let tenantID = context.tenantID else {
77+
return APIGatewayV2Response(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 APIGatewayV2Response(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 in the SAM template:
94+
95+
```yaml
96+
APIGatewayLambda:
97+
Type: AWS::Serverless::Function
98+
Properties:
99+
Runtime: provided.al2023
100+
Architectures:
101+
- arm64
102+
# Enable tenant isolation mode
103+
TenancyConfig:
104+
TenantIsolationMode: PER_TENANT
105+
Events:
106+
HttpApiEvent:
107+
Type: HttpApi
108+
```
109+
110+
### Key Configuration Points
111+
112+
- **TenancyConfig.TenantIsolationMode**: Set to `PER_TENANT` to enable tenant isolation
113+
- **Immutable property**: Tenant isolation can only be enabled when creating a new function
114+
- **Required tenant-id**: All invocations must include a tenant identifier
115+
116+
## Deployment
117+
118+
### Prerequisites
119+
120+
- Swift (>=6.2)
121+
- Docker (for cross-compilation to Amazon Linux)
122+
- AWS SAM CLI (>=1.147.1)
123+
- AWS CLI configured with appropriate credentials
124+
125+
### Build and Deploy
126+
127+
1. **Build the Lambda function**:
128+
```bash
129+
swift package archive --allow-network-connections docker
130+
```
131+
132+
2. **Deploy using SAM**:
133+
```bash
134+
sam deploy --guided
135+
```
136+
137+
3. **Note the API Gateway endpoint** from the CloudFormation outputs
138+
139+
## Testing
140+
141+
### Using API Gateway
142+
143+
The tenant ID is passed as a query parameter:
144+
145+
```bash
146+
# Request from tenant "alice"
147+
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com?tenant-id=alice"
148+
149+
# Request from tenant "bob"
150+
curl "https://your-api-id.execute-api.us-east-1.amazonaws.com?tenant-id=bob"
151+
```
152+
153+
### Expected Response
154+
155+
```json
156+
{
157+
"tenantID": "alice",
158+
"requestCount": 3,
159+
"firstRequest": "2024-01-15T10:30:00Z",
160+
"requests": [
161+
{
162+
"requestNumber": 1,
163+
"timestamp": "2024-01-15T10:30:00Z"
164+
},
165+
{
166+
"requestNumber": 2,
167+
"timestamp": "2024-01-15T10:31:15Z"
168+
},
169+
{
170+
"requestNumber": 3,
171+
"timestamp": "2024-01-15T10:32:30Z"
172+
}
173+
]
174+
}
175+
```
176+
177+
## How Tenant Isolation Works
178+
179+
1. **Request arrives** with a tenant identifier (via query parameter, header, or direct invocation)
180+
2. **Lambda routes the request** to an execution environment dedicated to that tenant
181+
3. **Environment reuse** - Subsequent requests from the same tenant reuse the same environment (warm start)
182+
4. **Isolation guarantee** - Execution environments are never shared between different tenants
183+
5. **Data persistence** - Tenant data persists in memory across invocations within the same execution environment
184+
185+
## Important Considerations
186+
187+
### Concurrency and Scaling
188+
189+
- Lambda imposes a limit of **2,500 tenant-isolated execution environments** (active or idle) for every 1,000 concurrent executions
190+
- Each tenant can scale independently based on their request volume
191+
- Cold starts occur more frequently due to tenant-specific environments
192+
193+
### Pricing
194+
195+
- Standard Lambda pricing applies (compute time and requests)
196+
- **Additional charge** when Lambda creates a new tenant-isolated execution environment
197+
- Price depends on allocated memory and CPU architecture
198+
- See [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing) for details
199+
200+
### Limitations
201+
202+
Tenant isolation mode is **not supported** with:
203+
- Function URLs
204+
- Provisioned concurrency
205+
- SnapStart
206+
207+
### Supported Invocation Methods
208+
209+
- ✅ Synchronous invocations
210+
- ✅ Asynchronous invocations
211+
- ✅ API Gateway event triggers
212+
- ✅ AWS SDK invocations
213+
214+
## Security Best Practices
215+
216+
1. **Execution role applies to all tenants** - Use IAM policies to restrict access to tenant-specific resources
217+
2. **Validate tenant identifiers** - Ensure tenant IDs are properly authenticated and authorized
218+
3. **Implement tenant-aware logging** - Include tenant ID in CloudWatch logs for audit trails
219+
4. **Set appropriate timeouts** - Configure function timeout based on expected workload
220+
5. **Monitor per-tenant metrics** - Use CloudWatch to track invocations, errors, and duration per tenant
221+
222+
## Monitoring
223+
224+
### CloudWatch Metrics
225+
226+
Lambda automatically publishes metrics with tenant dimensions:
227+
228+
- `Invocations` - Number of invocations per tenant
229+
- `Duration` - Execution time per tenant
230+
- `Errors` - Error count per tenant
231+
- `Throttles` - Throttled requests per tenant
232+
233+
### Accessing Metrics
234+
235+
```bash
236+
# Get invocation count for a specific tenant
237+
aws cloudwatch get-metric-statistics \
238+
--namespace AWS/Lambda \
239+
--metric-name Invocations \
240+
--dimensions Name=FunctionName,Value=MultiTenant Name=TenantId,Value=alice \
241+
--start-time 2024-01-15T00:00:00Z \
242+
--end-time 2024-01-15T23:59:59Z \
243+
--period 3600 \
244+
--statistics Sum
245+
```
246+
247+
## Learn More
248+
249+
- [AWS Lambda Tenant Isolation Documentation](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html)
250+
- [Configuring Tenant Isolation](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-configure.html)
251+
- [Invoking Tenant-Isolated Functions](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-invoke.html)
252+
- [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/)
253+
- [Swift AWS Lambda Runtime](https://github.com/swift-server/swift-aws-lambda-runtime)
254+
255+
## License
256+
257+
This example is part of the Swift AWS Lambda Runtime project and is licensed under Apache License 2.0.

0 commit comments

Comments
 (0)