mirror of
https://github.com/swift-server/swift-aws-lambda-runtime.git
synced 2026-06-02 07:27:33 +00:00
Add support for Lambda Tenants (#608)
Address https://github.com/awslabs/swift-aws-lambda-runtime/issues/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 <stormacq@amazon.lu> Co-authored-by: Tim Condon <0xTim@users.noreply.github.com>
This commit is contained in:
co-authored by
Sebastien Stormacq
Tim Condon
parent
0305cb31b8
commit
2abe7eb7de
@@ -39,7 +39,7 @@ jobs:
|
||||
# We pass the list of examples here, but we can't pass an array as argument
|
||||
# Instead, we pass a String with a valid JSON array.
|
||||
# The workaround is mentioned here https://github.com/orgs/community/discussions/11692
|
||||
examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
|
||||
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' ]"
|
||||
archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]"
|
||||
archive_plugin_enabled: true
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ response.json
|
||||
samconfig.toml
|
||||
template.yaml
|
||||
Makefile
|
||||
Dockerfile
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
response.json
|
||||
samconfig.toml
|
||||
Makefile
|
||||
@@ -0,0 +1,55 @@
|
||||
// swift-tools-version:6.2
|
||||
|
||||
import PackageDescription
|
||||
|
||||
// needed for CI to test the local version of the library
|
||||
import struct Foundation.URL
|
||||
|
||||
let package = Package(
|
||||
name: "swift-aws-lambda-runtime-example",
|
||||
platforms: [.macOS(.v15)],
|
||||
products: [
|
||||
.executable(name: "MultiTenant", targets: ["MultiTenant"])
|
||||
],
|
||||
dependencies: [
|
||||
// during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below
|
||||
.package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "MultiTenant",
|
||||
dependencies: [
|
||||
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
|
||||
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"],
|
||||
localDepsPath != "",
|
||||
let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]),
|
||||
v.isDirectory == true
|
||||
{
|
||||
// when we use the local runtime as deps, let's remove the dependency added above
|
||||
let indexToRemove = package.dependencies.firstIndex { dependency in
|
||||
if case .sourceControl(
|
||||
name: _,
|
||||
location: "https://github.com/awslabs/swift-aws-lambda-runtime.git",
|
||||
requirement: _
|
||||
) = dependency.kind {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if let indexToRemove {
|
||||
package.dependencies.remove(at: indexToRemove)
|
||||
}
|
||||
|
||||
// then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..)
|
||||
print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)")
|
||||
package.dependencies += [
|
||||
.package(name: "swift-aws-lambda-runtime", path: localDepsPath)
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
# 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
|
||||
- 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:
|
||||
|
||||
1. **TenantData** - Immutable struct tracking tenant information:
|
||||
- `tenantID`: Unique identifier for the tenant
|
||||
- `requestCount`: Total number of requests from this tenant
|
||||
- `firstRequest`: Unix timestamp (seconds since epoch) of the first request
|
||||
- `requests`: Array of individual request records
|
||||
|
||||
2. **TenantDataStore** - Actor-based storage providing thread-safe access to tenant data across invocations
|
||||
|
||||
3. **Lambda Handler** - Processes API Gateway requests and manages tenant data
|
||||
|
||||
## Code Structure
|
||||
|
||||
```swift
|
||||
// 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:
|
||||
|
||||
```yaml
|
||||
# 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_TENANT` to enable tenant isolation
|
||||
- **Parameter Mapping**: API Gateway maps the `tenant-id` query parameter to the `X-Amz-Tenant-Id` header 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:
|
||||
|
||||
1. **Client sends request** with `tenant-id` as a query parameter
|
||||
2. **API Gateway transforms** the query parameter into the `X-Amz-Tenant-Id` header
|
||||
3. **Lambda receives** the header and routes to the appropriate tenant-isolated environment
|
||||
|
||||
This mapping is configured in the `x-amazon-apigateway-integration` section using:
|
||||
```yaml
|
||||
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
|
||||
|
||||
1. **Build the Lambda function**:
|
||||
```bash
|
||||
swift package archive --allow-network-connections docker
|
||||
```
|
||||
|
||||
2. **Deploy using SAM**:
|
||||
```bash
|
||||
sam deploy --guided
|
||||
```
|
||||
|
||||
3. **Note the API Gateway endpoint** from the CloudFormation outputs
|
||||
|
||||
## Testing
|
||||
|
||||
### Using API Gateway
|
||||
|
||||
The tenant ID is passed as a query parameter. API Gateway automatically maps it to the `X-Amz-Tenant-Id` header:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# Synchronous invocation
|
||||
aws lambda invoke \
|
||||
--function-name MultiTenantLambda \
|
||||
--tenant-id alice \
|
||||
response.json
|
||||
|
||||
# View the response
|
||||
cat response.json
|
||||
```
|
||||
|
||||
### Expected Response
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
1. **Request arrives** with a tenant identifier (via query parameter, header, or direct invocation)
|
||||
2. **Lambda routes the request** to an execution environment dedicated to that tenant
|
||||
3. **Environment reuse** - Subsequent requests from the same tenant reuse the same environment (warm start)
|
||||
4. **Isolation guarantee** - Execution environments are never shared between different tenants
|
||||
5. **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](https://aws.amazon.com/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
|
||||
|
||||
1. **Execution role applies to all tenants** - Use IAM policies to restrict access to tenant-specific resources
|
||||
2. **Validate tenant identifiers** - Ensure tenant IDs are properly authenticated and authorized
|
||||
3. **Implement tenant-aware logging** - Include tenant ID in CloudWatch logs for audit trails
|
||||
4. **Set appropriate timeouts** - Configure function timeout based on expected workload
|
||||
5. **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 tenant
|
||||
- `Duration` - Execution time per tenant
|
||||
- `Errors` - Error count per tenant
|
||||
- `Throttles` - Throttled requests per tenant
|
||||
|
||||
### Accessing Metrics
|
||||
|
||||
```bash
|
||||
# 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](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html)
|
||||
- [Configuring Tenant Isolation](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-configure.html)
|
||||
- [Invoking Tenant-Isolated Functions](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-invoke.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/)
|
||||
- [Swift AWS Lambda Runtime](https://github.com/swift-server/swift-aws-lambda-runtime)
|
||||
|
||||
## License
|
||||
|
||||
This example is part of the Swift AWS Lambda Runtime project and is licensed under Apache License 2.0.
|
||||
@@ -0,0 +1,107 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftAWSLambdaRuntime open source project
|
||||
//
|
||||
// Copyright SwiftAWSLambdaRuntime project authors
|
||||
// Copyright (c) Amazon.com, Inc. or its affiliates.
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import AWSLambdaEvents
|
||||
import AWSLambdaRuntime
|
||||
|
||||
#if canImport(FoundationEssentials)
|
||||
import FoundationEssentials
|
||||
#else
|
||||
import Foundation
|
||||
#endif
|
||||
|
||||
let tenants = TenantDataStore()
|
||||
|
||||
let runtime = LambdaRuntime {
|
||||
(event: APIGatewayRequest, context: LambdaContext) -> APIGatewayResponse in
|
||||
|
||||
// Extract tenant ID from context
|
||||
guard let tenantID = context.tenantID else {
|
||||
return APIGatewayResponse(statusCode: .badRequest, body: "No Tenant ID provided")
|
||||
}
|
||||
|
||||
// Get or create tenant data
|
||||
let currentData = await tenants[tenantID] ?? TenantData(tenantID: tenantID)
|
||||
|
||||
// Add new request
|
||||
let updatedData = currentData.addingRequest()
|
||||
|
||||
// Store updated data
|
||||
await tenants.update(id: tenantID, data: updatedData)
|
||||
|
||||
return try APIGatewayResponse(statusCode: .ok, encodableBody: updatedData)
|
||||
}
|
||||
|
||||
try await runtime.run()
|
||||
|
||||
actor TenantDataStore {
|
||||
private var tenants: [String: TenantData] = [:]
|
||||
|
||||
subscript(id: String) -> TenantData? {
|
||||
tenants[id]
|
||||
}
|
||||
|
||||
// subscript setters can't be called from outside of the actor
|
||||
func update(id: String, data: TenantData) {
|
||||
tenants[id] = data
|
||||
}
|
||||
}
|
||||
|
||||
struct TenantData: Codable {
|
||||
struct TenantRequest: Codable {
|
||||
let requestNumber: Int
|
||||
let timestamp: String
|
||||
}
|
||||
|
||||
let tenantID: String
|
||||
let requestCount: Int
|
||||
let firstRequest: String
|
||||
let requests: [TenantRequest]
|
||||
|
||||
init(tenantID: String) {
|
||||
self.init(
|
||||
tenantID: tenantID,
|
||||
requestCount: 0,
|
||||
firstRequest: "\(Date().timeIntervalSince1970)",
|
||||
requests: []
|
||||
)
|
||||
}
|
||||
|
||||
func addingRequest() -> TenantData {
|
||||
let newCount = requestCount + 1
|
||||
let newRequest = TenantRequest(
|
||||
requestNumber: newCount,
|
||||
timestamp: "\(Date().timeIntervalSince1970)"
|
||||
)
|
||||
return TenantData(
|
||||
tenantID: tenantID,
|
||||
requestCount: newCount,
|
||||
firstRequest: firstRequest,
|
||||
requests: requests + [newRequest]
|
||||
)
|
||||
}
|
||||
|
||||
private init(
|
||||
tenantID: String,
|
||||
requestCount: Int,
|
||||
firstRequest: String,
|
||||
requests: [TenantRequest]
|
||||
) {
|
||||
self.tenantID = tenantID
|
||||
self.requestCount = requestCount
|
||||
self.firstRequest = firstRequest
|
||||
self.requests = requests
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
Description: SAM Template for Multi Tenant Lambda Example
|
||||
|
||||
# This is an example SAM template for the purpose of this project.
|
||||
# When deploying such infrastructure in production environment,
|
||||
# we strongly encourage you to follow these best practices for improved security and resiliency
|
||||
# - Enable access logging on API Gateway
|
||||
# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)
|
||||
# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit
|
||||
# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html
|
||||
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html
|
||||
# - Check encryption settings for Lambda environment variable
|
||||
# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html
|
||||
# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ)
|
||||
# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq
|
||||
# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources
|
||||
# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html
|
||||
# Code Example: https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres
|
||||
|
||||
Resources:
|
||||
# API Gateway REST API
|
||||
MultiTenantApi:
|
||||
Type: AWS::Serverless::Api
|
||||
Properties:
|
||||
StageName: Prod
|
||||
DefinitionBody:
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: MultiTenant API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/{proxy+}:
|
||||
x-amazon-apigateway-any-method:
|
||||
parameters:
|
||||
- name: tenant-id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: proxy
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
x-amazon-apigateway-request-validator: params-only
|
||||
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
|
||||
requestParameters:
|
||||
integration.request.header.X-Amz-Tenant-Id: method.request.querystring.tenant-id
|
||||
/:
|
||||
x-amazon-apigateway-any-method:
|
||||
parameters:
|
||||
- name: tenant-id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
x-amazon-apigateway-request-validator: params-only
|
||||
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
|
||||
requestParameters:
|
||||
integration.request.header.X-Amz-Tenant-Id: method.request.querystring.tenant-id
|
||||
x-amazon-apigateway-request-validators:
|
||||
params-only:
|
||||
validateRequestParameters: true
|
||||
validateRequestBody: false
|
||||
|
||||
# Lambda function
|
||||
MultiTenantLambda:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MultiTenant/MultiTenant.zip
|
||||
Timeout: 60
|
||||
Handler: swift.bootstrap # ignored by the Swift runtime
|
||||
Runtime: provided.al2023
|
||||
MemorySize: 128
|
||||
Architectures:
|
||||
- arm64
|
||||
# https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-configure.html#tenant-isolation-cfn
|
||||
TenancyConfig:
|
||||
TenantIsolationMode: PER_TENANT
|
||||
Environment:
|
||||
Variables:
|
||||
# by default, AWS Lambda runtime produces no log
|
||||
# use `LOG_LEVEL: debug` for lifecycle and event handling information
|
||||
# use `LOG_LEVEL: trace` for detailed input event information
|
||||
LOG_LEVEL: trace
|
||||
Events:
|
||||
RootPath:
|
||||
Type: Api
|
||||
Properties:
|
||||
RestApiId: !Ref MultiTenantApi
|
||||
Path: /
|
||||
Method: ANY
|
||||
ProxyPath:
|
||||
Type: Api
|
||||
Properties:
|
||||
RestApiId: !Ref MultiTenantApi
|
||||
Path: /{proxy+}
|
||||
Method: ANY
|
||||
|
||||
# Permission for API Gateway to invoke Lambda
|
||||
MultiTenantLambdaPermission:
|
||||
Type: AWS::Lambda::Permission
|
||||
Properties:
|
||||
FunctionName: !Ref MultiTenantLambda
|
||||
Action: lambda:InvokeFunction
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MultiTenantApi}/*/*
|
||||
|
||||
Outputs:
|
||||
# print API Gateway endpoint
|
||||
APIGatewayEndpoint:
|
||||
Description: API Gateway endpoint URL
|
||||
# https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-invoke.html#tenant-isolation-invoke-apigateway
|
||||
Value: !Sub "https://${MultiTenantApi}.execute-api.${AWS::Region}.amazonaws.com/Prod?tenant-id=seb"
|
||||
@@ -46,6 +46,8 @@ package struct InvocationMetadata: Hashable, Sendable {
|
||||
package let clientContext: String?
|
||||
@usableFromInline
|
||||
package let cognitoIdentity: String?
|
||||
@usableFromInline
|
||||
package let tenantID: String?
|
||||
|
||||
package init(headers: HTTPHeaders) throws(LambdaRuntimeError) {
|
||||
guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else {
|
||||
@@ -69,6 +71,7 @@ package struct InvocationMetadata: Hashable, Sendable {
|
||||
headers.first(name: AmazonHeaders.traceID) ?? "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=0"
|
||||
self.clientContext = headers["Lambda-Runtime-Client-Context"].first
|
||||
self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first
|
||||
self.tenantID = headers[AmazonHeaders.tenantID].first
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ public enum Lambda {
|
||||
context: LambdaContext(
|
||||
requestID: invocation.metadata.requestID,
|
||||
traceID: invocation.metadata.traceID,
|
||||
tenantID: invocation.metadata.tenantID,
|
||||
invokedFunctionARN: invocation.metadata.invokedFunctionARN,
|
||||
deadline: LambdaClock.Instant(
|
||||
millisecondsSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch
|
||||
|
||||
@@ -86,9 +86,14 @@ public struct ClientContext: Codable, Sendable {
|
||||
/// The Lambda runtime generates and passes the `LambdaContext` to the Lambda handler as an argument.
|
||||
@available(LambdaSwift 2.0, *)
|
||||
public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
|
||||
// use a final class as storage to have value type semantic with
|
||||
// low overhead of class for copy on write operations
|
||||
// https://www.youtube.com/watch?v=iLDldae64xE
|
||||
final class _Storage: Sendable {
|
||||
let requestID: String
|
||||
let traceID: String
|
||||
let tenantID: String?
|
||||
let invokedFunctionARN: String
|
||||
let deadline: LambdaClock.Instant
|
||||
let cognitoIdentity: String?
|
||||
@@ -98,6 +103,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
init(
|
||||
requestID: String,
|
||||
traceID: String,
|
||||
tenantID: String?,
|
||||
invokedFunctionARN: String,
|
||||
deadline: LambdaClock.Instant,
|
||||
cognitoIdentity: String?,
|
||||
@@ -106,6 +112,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
) {
|
||||
self.requestID = requestID
|
||||
self.traceID = traceID
|
||||
self.tenantID = tenantID
|
||||
self.invokedFunctionARN = invokedFunctionARN
|
||||
self.deadline = deadline
|
||||
self.cognitoIdentity = cognitoIdentity
|
||||
@@ -126,6 +133,11 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
self.storage.traceID
|
||||
}
|
||||
|
||||
/// The Tenant ID.
|
||||
public var tenantID: String? {
|
||||
self.storage.tenantID
|
||||
}
|
||||
|
||||
/// The ARN of the Lambda function, version, or alias that's specified in the invocation.
|
||||
public var invokedFunctionARN: String {
|
||||
self.storage.invokedFunctionARN
|
||||
@@ -153,6 +165,12 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
self.storage.logger
|
||||
}
|
||||
|
||||
@available(
|
||||
*,
|
||||
deprecated,
|
||||
message:
|
||||
"This method will be removed in a future major version update. Use init(requestID:traceID:tenantID:invokedFunctionARN:deadline:cognitoIdentity:clientContext:logger) instead."
|
||||
)
|
||||
public init(
|
||||
requestID: String,
|
||||
traceID: String,
|
||||
@@ -161,10 +179,32 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
cognitoIdentity: String? = nil,
|
||||
clientContext: ClientContext? = nil,
|
||||
logger: Logger
|
||||
) {
|
||||
self.init(
|
||||
requestID: requestID,
|
||||
traceID: traceID,
|
||||
tenantID: nil,
|
||||
invokedFunctionARN: invokedFunctionARN,
|
||||
deadline: deadline,
|
||||
cognitoIdentity: cognitoIdentity,
|
||||
clientContext: clientContext,
|
||||
logger: logger
|
||||
)
|
||||
}
|
||||
public init(
|
||||
requestID: String,
|
||||
traceID: String,
|
||||
tenantID: String?,
|
||||
invokedFunctionARN: String,
|
||||
deadline: LambdaClock.Instant,
|
||||
cognitoIdentity: String? = nil,
|
||||
clientContext: ClientContext? = nil,
|
||||
logger: Logger
|
||||
) {
|
||||
self.storage = _Storage(
|
||||
requestID: requestID,
|
||||
traceID: traceID,
|
||||
tenantID: tenantID,
|
||||
invokedFunctionARN: invokedFunctionARN,
|
||||
deadline: deadline,
|
||||
cognitoIdentity: cognitoIdentity,
|
||||
@@ -187,6 +227,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
package static func __forTestsOnly(
|
||||
requestID: String,
|
||||
traceID: String,
|
||||
tenantID: String?,
|
||||
invokedFunctionARN: String,
|
||||
timeout: Duration,
|
||||
logger: Logger
|
||||
@@ -194,6 +235,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
LambdaContext(
|
||||
requestID: requestID,
|
||||
traceID: traceID,
|
||||
tenantID: tenantID,
|
||||
invokedFunctionARN: invokedFunctionARN,
|
||||
deadline: LambdaClock().now.advanced(by: timeout),
|
||||
logger: logger
|
||||
|
||||
@@ -392,6 +392,7 @@ extension LambdaChannelHandler: ChannelInboundHandler {
|
||||
switch self.state {
|
||||
case .connected(let context, .waitingForNextInvocation(let continuation)):
|
||||
do {
|
||||
self.logger.trace("Lambda Invocation Headers", metadata: ["headers": "\(response.head.headers)"])
|
||||
let metadata = try InvocationMetadata(headers: response.head.headers)
|
||||
self.state = .connected(context, .waitingForResponse)
|
||||
continuation.resume(returning: Invocation(metadata: metadata, event: response.body ?? ByteBuffer()))
|
||||
|
||||
@@ -28,7 +28,13 @@ enum Consts {
|
||||
static let initializationError = "InitializationError"
|
||||
}
|
||||
|
||||
/// AWS Lambda HTTP Headers, used to populate the `LambdaContext` object.
|
||||
/// AWS Lambda HTTP Headers, used to populate the `LambdaContext` object. E.g.
|
||||
/// Content-Type: application/json;
|
||||
/// Lambda-Runtime-Aws-Request-Id: bfcc9017-7f34-4154-9699-ff0229e9ad2b;
|
||||
/// Lambda-Runtime-Aws-Tenant-Id: seb;
|
||||
/// Lambda-Runtime-Deadline-Ms: 1763672952393;
|
||||
/// Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:us-west-2:486652066693:function:MultiTenant-MultiTenantLambda-1E9mgLUtIQ9N;
|
||||
/// Lambda-Runtime-Trace-Id: Root=1-691f833c-79cf6a2b23942f8925881714;Parent=76ab2f41125eef94;Sampled=0;Lineage=1:9581a8d4:0; Date: Thu, 20 Nov 2025 21:08:12 GMT; Transfer-Encoding: chunked
|
||||
enum AmazonHeaders {
|
||||
static let requestID = "Lambda-Runtime-Aws-Request-Id"
|
||||
static let traceID = "Lambda-Runtime-Trace-Id"
|
||||
@@ -36,6 +42,7 @@ enum AmazonHeaders {
|
||||
static let cognitoIdentity = "X-Amz-Cognito-Identity"
|
||||
static let deadline = "Lambda-Runtime-Deadline-Ms"
|
||||
static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn"
|
||||
static let tenantID = "Lambda-Runtime-Aws-Tenant-Id"
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
@@ -41,4 +41,23 @@ struct InvocationTest {
|
||||
let invocation = try #require(maybeInvocation)
|
||||
#expect(!invocation.traceID.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(LambdaSwift 2.0, *)
|
||||
func testInvocationTenantID() throws {
|
||||
let tenantID = "123"
|
||||
let headers = HTTPHeaders([
|
||||
(AmazonHeaders.requestID, "test"),
|
||||
(AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).millisSinceEpoch)),
|
||||
(AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"),
|
||||
(AmazonHeaders.tenantID, tenantID),
|
||||
])
|
||||
|
||||
var maybeInvocation: InvocationMetadata?
|
||||
|
||||
#expect(throws: Never.self) { maybeInvocation = try InvocationMetadata(headers: headers) }
|
||||
let invocation = try #require(maybeInvocation)
|
||||
#expect(invocation.tenantID == tenantID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ struct JSONTests {
|
||||
let context = LambdaContext.__forTestsOnly(
|
||||
requestID: UUID().uuidString,
|
||||
traceID: UUID().uuidString,
|
||||
tenantID: nil,
|
||||
invokedFunctionARN: "arn:",
|
||||
timeout: .milliseconds(6000),
|
||||
logger: self.logger
|
||||
|
||||
@@ -122,6 +122,7 @@ struct LambdaContextTests {
|
||||
let context = LambdaContext.__forTestsOnly(
|
||||
requestID: "test-request",
|
||||
traceID: "test-trace",
|
||||
tenantID: nil,
|
||||
invokedFunctionARN: "test-arn",
|
||||
timeout: .seconds(30),
|
||||
logger: Logger(label: "test")
|
||||
|
||||
Reference in New Issue
Block a user