From c806c41f5d799ec6f1300d5998d035524a8d0281 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Thu, 20 Nov 2025 22:13:36 +0100 Subject: [PATCH 01/12] Add support for Lambda Tenants --- .github/workflows/pull_request.yml | 2 +- Examples/HelloWorldNoTraits/.gitignore | 1 + Examples/MultiTenant/.gitignore | 3 + Examples/MultiTenant/Package.swift | 55 ++++ Examples/MultiTenant/README.md | 257 ++++++++++++++++++ Examples/MultiTenant/Sources/main.swift | 109 ++++++++ Examples/MultiTenant/event.json | 123 +++++++++ Examples/MultiTenant/template.yaml | 95 +++++++ .../ControlPlaneRequest.swift | 3 + Sources/AWSLambdaRuntime/Lambda.swift | 1 + Sources/AWSLambdaRuntime/LambdaContext.swift | 12 + .../LambdaRuntimeClient+ChannelHandler.swift | 1 + Sources/AWSLambdaRuntime/Utils.swift | 7 + .../InvocationTests.swift | 19 ++ 14 files changed, 687 insertions(+), 1 deletion(-) create mode 100644 Examples/MultiTenant/.gitignore create mode 100644 Examples/MultiTenant/Package.swift create mode 100644 Examples/MultiTenant/README.md create mode 100644 Examples/MultiTenant/Sources/main.swift create mode 100644 Examples/MultiTenant/event.json create mode 100644 Examples/MultiTenant/template.yaml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6245e4af..6c3dbde8 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -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 diff --git a/Examples/HelloWorldNoTraits/.gitignore b/Examples/HelloWorldNoTraits/.gitignore index e41d0be5..610d9dad 100644 --- a/Examples/HelloWorldNoTraits/.gitignore +++ b/Examples/HelloWorldNoTraits/.gitignore @@ -2,3 +2,4 @@ response.json samconfig.toml template.yaml Makefile +Dockerfile diff --git a/Examples/MultiTenant/.gitignore b/Examples/MultiTenant/.gitignore new file mode 100644 index 00000000..a03a102d --- /dev/null +++ b/Examples/MultiTenant/.gitignore @@ -0,0 +1,3 @@ +response.json +samconfig.toml +Makefile diff --git a/Examples/MultiTenant/Package.swift b/Examples/MultiTenant/Package.swift new file mode 100644 index 00000000..c22dd399 --- /dev/null +++ b/Examples/MultiTenant/Package.swift @@ -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) + ] +} diff --git a/Examples/MultiTenant/README.md b/Examples/MultiTenant/README.md new file mode 100644 index 00000000..fd80d569 --- /dev/null +++ b/Examples/MultiTenant/README.md @@ -0,0 +1,257 @@ +# 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`: ISO 8601 timestamp 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: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + guard let tenantID = context.tenantID else { + return APIGatewayV2Response(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 APIGatewayV2Response(statusCode: .ok, encodableBody: updatedData) +} +``` + +## Configuration + +### SAM Template (template.yaml) + +The function is configured with tenant isolation mode in the SAM template: + +```yaml +APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + Runtime: provided.al2023 + Architectures: + - arm64 + # Enable tenant isolation mode + TenancyConfig: + TenantIsolationMode: PER_TENANT + Events: + HttpApiEvent: + Type: HttpApi +``` + +### Key Configuration Points + +- **TenancyConfig.TenantIsolationMode**: Set to `PER_TENANT` to enable tenant isolation +- **Immutable property**: Tenant isolation can only be enabled when creating a new function +- **Required tenant-id**: All invocations must include a tenant identifier + +## 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: + +```bash +# Request from tenant "alice" +curl "https://your-api-id.execute-api.us-east-1.amazonaws.com?tenant-id=alice" + +# Request from tenant "bob" +curl "https://your-api-id.execute-api.us-east-1.amazonaws.com?tenant-id=bob" +``` + +### Expected Response + +```json +{ + "tenantID": "alice", + "requestCount": 3, + "firstRequest": "2024-01-15T10:30:00Z", + "requests": [ + { + "requestNumber": 1, + "timestamp": "2024-01-15T10:30:00Z" + }, + { + "requestNumber": 2, + "timestamp": "2024-01-15T10:31:15Z" + }, + { + "requestNumber": 3, + "timestamp": "2024-01-15T10:32:30Z" + } + ] +} +``` + +## 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. diff --git a/Examples/MultiTenant/Sources/main.swift b/Examples/MultiTenant/Sources/main.swift new file mode 100644 index 00000000..a70cc3fc --- /dev/null +++ b/Examples/MultiTenant/Sources/main.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// +// 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: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in +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 + } +} diff --git a/Examples/MultiTenant/event.json b/Examples/MultiTenant/event.json new file mode 100644 index 00000000..5c9540ea --- /dev/null +++ b/Examples/MultiTenant/event.json @@ -0,0 +1,123 @@ +{ + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": true, + "queryStringParameters": { + "foo": "bar" + }, + "multiValueQueryStringParameters": { + "foo": [ + "bar" + ] + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ], + "Accept-Encoding": [ + "gzip, deflate, sdch" + ], + "Accept-Language": [ + "en-US,en;q=0.8" + ], + "Cache-Control": [ + "max-age=0" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Host": [ + "0123456789.execute-api.us-east-1.amazonaws.com" + ], + "Upgrade-Insecure-Requests": [ + "1" + ], + "User-Agent": [ + "Custom User Agent String" + ], + "Via": [ + "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" + ], + "X-Forwarded-For": [ + "127.0.0.1, 127.0.0.2" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/Examples/MultiTenant/template.yaml b/Examples/MultiTenant/template.yaml new file mode 100644 index 00000000..bff34b2d --- /dev/null +++ b/Examples/MultiTenant/template.yaml @@ -0,0 +1,95 @@ +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 loggin 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: + /: + get: + 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 for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: trace + Events: + GetRoot: + Type: Api + Properties: + RestApiId: !Ref MultiTenantApi + Path: / + Method: GET + + # 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" diff --git a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift index 4d82d76b..677a134e 100644 --- a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift @@ -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 } } diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift index 7f748457..d559bda2 100644 --- a/Sources/AWSLambdaRuntime/Lambda.swift +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -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 diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 111c97ec..2bd12c92 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -89,6 +89,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { 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 +99,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { init( requestID: String, traceID: String, + tenantID: String?, invokedFunctionARN: String, deadline: LambdaClock.Instant, cognitoIdentity: String?, @@ -106,6 +108,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 +129,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 @@ -156,6 +164,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { public init( requestID: String, traceID: String, + tenantID: String? = nil, invokedFunctionARN: String, deadline: LambdaClock.Instant, cognitoIdentity: String? = nil, @@ -165,6 +174,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { self.storage = _Storage( requestID: requestID, traceID: traceID, + tenantID: tenantID, invokedFunctionARN: invokedFunctionARN, deadline: deadline, cognitoIdentity: cognitoIdentity, @@ -187,6 +197,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { package static func __forTestsOnly( requestID: String, traceID: String, + tenantID: String? = nil, invokedFunctionARN: String, timeout: Duration, logger: Logger @@ -194,6 +205,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { LambdaContext( requestID: requestID, traceID: traceID, + tenantID: tenantID, invokedFunctionARN: invokedFunctionARN, deadline: LambdaClock().now.advanced(by: timeout), logger: logger diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift index 6238fac4..e4fa6e16 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift @@ -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())) diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index 6a80e7f6..9aa287d9 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -29,6 +29,12 @@ enum Consts { } /// AWS Lambda HTTP Headers, used to populate the `LambdaContext` object. +/// 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 { diff --git a/Tests/AWSLambdaRuntimeTests/InvocationTests.swift b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift index 13e5956e..e30cbda1 100644 --- a/Tests/AWSLambdaRuntimeTests/InvocationTests.swift +++ b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift @@ -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) + } + } From 8dc16ab99cd7cdf8d53a858718d2a743d6b72fbb Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Thu, 20 Nov 2025 22:18:32 +0100 Subject: [PATCH 02/12] fix yaml lint --- Examples/MultiTenant/template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/MultiTenant/template.yaml b/Examples/MultiTenant/template.yaml index bff34b2d..ad51ee11 100644 --- a/Examples/MultiTenant/template.yaml +++ b/Examples/MultiTenant/template.yaml @@ -61,7 +61,7 @@ Resources: MemorySize: 128 Architectures: - arm64 - # https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-configure.html#tenant-isolation-cfn + # https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation-configure.html#tenant-isolation-cfn TenancyConfig: TenantIsolationMode: PER_TENANT Environment: @@ -92,4 +92,4 @@ Outputs: 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" + Value: !Sub "https://${MultiTenantApi}.execute-api.${AWS::Region}.amazonaws.com/Prod?tenant-id=seb" From 23cfa83b92ab61d63941194e5f6562d4fcb3b685 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Thu, 20 Nov 2025 22:30:52 +0100 Subject: [PATCH 03/12] cleanup code --- Examples/MultiTenant/Sources/main.swift | 2 - Examples/MultiTenant/event.json | 123 ------------------------ 2 files changed, 125 deletions(-) delete mode 100644 Examples/MultiTenant/event.json diff --git a/Examples/MultiTenant/Sources/main.swift b/Examples/MultiTenant/Sources/main.swift index a70cc3fc..8229c908 100644 --- a/Examples/MultiTenant/Sources/main.swift +++ b/Examples/MultiTenant/Sources/main.swift @@ -24,8 +24,6 @@ import Foundation let tenants = TenantDataStore() -// let runtime = LambdaRuntime { -// (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in let runtime = LambdaRuntime { (event: APIGatewayRequest, context: LambdaContext) -> APIGatewayResponse in diff --git a/Examples/MultiTenant/event.json b/Examples/MultiTenant/event.json deleted file mode 100644 index 5c9540ea..00000000 --- a/Examples/MultiTenant/event.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "body": "eyJ0ZXN0IjoiYm9keSJ9", - "resource": "/{proxy+}", - "path": "/path/to/resource", - "httpMethod": "POST", - "isBase64Encoded": true, - "queryStringParameters": { - "foo": "bar" - }, - "multiValueQueryStringParameters": { - "foo": [ - "bar" - ] - }, - "pathParameters": { - "proxy": "/path/to/resource" - }, - "stageVariables": { - "baz": "qux" - }, - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, sdch", - "Accept-Language": "en-US,en;q=0.8", - "Cache-Control": "max-age=0", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "1234567890.execute-api.us-east-1.amazonaws.com", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Custom User Agent String", - "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", - "X-Forwarded-For": "127.0.0.1, 127.0.0.2", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" - ], - "Accept-Encoding": [ - "gzip, deflate, sdch" - ], - "Accept-Language": [ - "en-US,en;q=0.8" - ], - "Cache-Control": [ - "max-age=0" - ], - "CloudFront-Forwarded-Proto": [ - "https" - ], - "CloudFront-Is-Desktop-Viewer": [ - "true" - ], - "CloudFront-Is-Mobile-Viewer": [ - "false" - ], - "CloudFront-Is-SmartTV-Viewer": [ - "false" - ], - "CloudFront-Is-Tablet-Viewer": [ - "false" - ], - "CloudFront-Viewer-Country": [ - "US" - ], - "Host": [ - "0123456789.execute-api.us-east-1.amazonaws.com" - ], - "Upgrade-Insecure-Requests": [ - "1" - ], - "User-Agent": [ - "Custom User Agent String" - ], - "Via": [ - "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" - ], - "X-Amz-Cf-Id": [ - "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" - ], - "X-Forwarded-For": [ - "127.0.0.1, 127.0.0.2" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "requestContext": { - "accountId": "123456789012", - "resourceId": "123456", - "stage": "prod", - "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", - "requestTime": "09/Apr/2015:12:34:56 +0000", - "requestTimeEpoch": 1428582896000, - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "accessKey": null, - "sourceIp": "127.0.0.1", - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Custom User Agent String", - "user": null - }, - "path": "/prod/path/to/resource", - "resourcePath": "/{proxy+}", - "httpMethod": "POST", - "apiId": "1234567890", - "protocol": "HTTP/1.1" - } -} \ No newline at end of file From f13a376cf0c3122709ffd69d6deb02852fe922f0 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Thu, 20 Nov 2025 22:34:49 +0100 Subject: [PATCH 04/12] address co-pilot suggestions --- Examples/MultiTenant/README.md | 93 ++++++++++++++++++++++++------ Examples/MultiTenant/template.yaml | 2 +- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/Examples/MultiTenant/README.md b/Examples/MultiTenant/README.md index fd80d569..0e8bba07 100644 --- a/Examples/MultiTenant/README.md +++ b/Examples/MultiTenant/README.md @@ -34,7 +34,7 @@ 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`: ISO 8601 timestamp of the first request + - `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 @@ -71,10 +71,10 @@ actor TenantDataStore { // Lambda handler extracts tenant ID from context let runtime = LambdaRuntime { - (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + (event: APIGatewayRequest, context: LambdaContext) -> APIGatewayResponse in guard let tenantID = context.tenantID else { - return APIGatewayV2Response(statusCode: .badRequest, body: "No Tenant ID provided") + return APIGatewayResponse(statusCode: .badRequest, body: "No Tenant ID provided") } // Process request for this tenant @@ -82,7 +82,7 @@ let runtime = LambdaRuntime { let updatedData = currentData.addingRequest() await tenants.update(id: tenantID, data: updatedData) - return try APIGatewayV2Response(statusCode: .ok, encodableBody: updatedData) + return try APIGatewayResponse(statusCode: .ok, encodableBody: updatedData) } ``` @@ -90,10 +90,33 @@ let runtime = LambdaRuntime { ### SAM Template (template.yaml) -The function is configured with tenant isolation mode in the SAM template: +The function is configured with tenant isolation mode and API Gateway parameter mapping in the SAM template: ```yaml -APIGatewayLambda: +# 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 @@ -102,17 +125,31 @@ APIGatewayLambda: # Enable tenant isolation mode TenancyConfig: TenantIsolationMode: PER_TENANT - Events: - HttpApiEvent: - Type: HttpApi ``` ### 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 @@ -140,14 +177,34 @@ APIGatewayLambda: ### Using API Gateway -The tenant ID is passed as a query parameter: +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?tenant-id=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?tenant-id=bob" +# 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 @@ -156,24 +213,26 @@ curl "https://your-api-id.execute-api.us-east-1.amazonaws.com?tenant-id=bob" { "tenantID": "alice", "requestCount": 3, - "firstRequest": "2024-01-15T10:30:00Z", + "firstRequest": "1705320000.123456", "requests": [ { "requestNumber": 1, - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "1705320000.123456" }, { "requestNumber": 2, - "timestamp": "2024-01-15T10:31:15Z" + "timestamp": "1705320075.789012" }, { "requestNumber": 3, - "timestamp": "2024-01-15T10:32:30Z" + "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) diff --git a/Examples/MultiTenant/template.yaml b/Examples/MultiTenant/template.yaml index ad51ee11..53aeb2c0 100644 --- a/Examples/MultiTenant/template.yaml +++ b/Examples/MultiTenant/template.yaml @@ -67,7 +67,7 @@ Resources: Environment: Variables: # by default, AWS Lambda runtime produces no log - # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: debug` for lifecycle and event handling information # use `LOG_LEVEL: trace` for detailed input event information LOG_LEVEL: trace Events: From f16dd2edce49afb177a95a3b0dc56c9a9dadb1eb Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Thu, 20 Nov 2025 22:36:49 +0100 Subject: [PATCH 05/12] fix typo --- Examples/MultiTenant/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/MultiTenant/template.yaml b/Examples/MultiTenant/template.yaml index 53aeb2c0..6cce7412 100644 --- a/Examples/MultiTenant/template.yaml +++ b/Examples/MultiTenant/template.yaml @@ -5,7 +5,7 @@ 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 loggin on API Gateway +# - 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 From 2193c16ff85dc0eab8696d90ec83dfe41ec5dd7f Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 21 Nov 2025 10:22:00 +0100 Subject: [PATCH 06/12] add proxy configuration to the API Gateway --- Examples/MultiTenant/template.yaml | 32 +++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/Examples/MultiTenant/template.yaml b/Examples/MultiTenant/template.yaml index 6cce7412..78ab635c 100644 --- a/Examples/MultiTenant/template.yaml +++ b/Examples/MultiTenant/template.yaml @@ -30,8 +30,28 @@ Resources: 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 /: - get: + x-amazon-apigateway-any-method: parameters: - name: tenant-id in: query @@ -71,12 +91,18 @@ Resources: # use `LOG_LEVEL: trace` for detailed input event information LOG_LEVEL: trace Events: - GetRoot: + RootPath: Type: Api Properties: RestApiId: !Ref MultiTenantApi Path: / - Method: GET + Method: ANY + ProxyPath: + Type: Api + Properties: + RestApiId: !Ref MultiTenantApi + Path: /{proxy+} + Method: ANY # Permission for API Gateway to invoke Lambda MultiTenantLambdaPermission: From 131d174cb43dd1dfd2284eebd3c92d3ed552fae2 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 21 Nov 2025 14:00:26 +0100 Subject: [PATCH 07/12] add comments --- Sources/AWSLambdaRuntime/LambdaContext.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 2bd12c92..19617049 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -86,6 +86,10 @@ 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 a 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 From 4ac9ab915dc17e048943c09a669efb35e484dcb0 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 21 Nov 2025 17:07:42 +0100 Subject: [PATCH 08/12] fix swift format --- Sources/AWSLambdaRuntime/LambdaContext.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 19617049..d4602209 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -87,8 +87,8 @@ public struct ClientContext: Codable, Sendable { @available(LambdaSwift 2.0, *) public struct LambdaContext: CustomDebugStringConvertible, Sendable { - // use a final class a storage to have value type semantic with - // low overhead of class for copy on write operations + // use a final class a 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 From cf88ecf3d29b8887848522894625f94d9200b730 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 21 Nov 2025 17:35:45 +0100 Subject: [PATCH 09/12] remove api breakage on LambdaContext.init() --- Sources/AWSLambdaRuntime/LambdaContext.swift | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index d4602209..ef686fe5 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -165,6 +165,32 @@ 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, + invokedFunctionARN: String, + deadline: LambdaClock.Instant, + 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, From 98ef22bb349300c9c15570f1bee76a2ff9d498c1 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 21 Nov 2025 19:24:55 +0100 Subject: [PATCH 10/12] remove default value in LambdaContext.init() --- Sources/AWSLambdaRuntime/LambdaContext.swift | 4 ++-- Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift | 1 + Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index ef686fe5..0510b7fe 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -194,7 +194,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { public init( requestID: String, traceID: String, - tenantID: String? = nil, + tenantID: String?, invokedFunctionARN: String, deadline: LambdaClock.Instant, cognitoIdentity: String? = nil, @@ -227,7 +227,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { package static func __forTestsOnly( requestID: String, traceID: String, - tenantID: String? = nil, + tenantID: String?, invokedFunctionARN: String, timeout: Duration, logger: Logger diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift index 4e7e0219..5d24cb63 100644 --- a/Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift +++ b/Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift @@ -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 diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index e0f6a7b4..bac85e5e 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -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") From 99d18cfe43e93d470b89c59591604caf25b35c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Nov 2025 21:07:01 +0100 Subject: [PATCH 11/12] Update Sources/AWSLambdaRuntime/LambdaContext.swift Co-authored-by: Tim Condon <0xTim@users.noreply.github.com> --- Sources/AWSLambdaRuntime/LambdaContext.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 0510b7fe..d14e16c6 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -87,7 +87,7 @@ public struct ClientContext: Codable, Sendable { @available(LambdaSwift 2.0, *) public struct LambdaContext: CustomDebugStringConvertible, Sendable { - // use a final class a storage to have value type semantic with + // 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 { From ccd93f17689365bf535462e1ceb6dfdd17990216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Nov 2025 21:07:17 +0100 Subject: [PATCH 12/12] Update Sources/AWSLambdaRuntime/Utils.swift Co-authored-by: Tim Condon <0xTim@users.noreply.github.com> --- Sources/AWSLambdaRuntime/Utils.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index 9aa287d9..8a8d0442 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -28,7 +28,7 @@ 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;