Skip to content

Commit 97583a7

Browse files
sebstoSebastien Stormacqjbelkins
authored
Add Multi-Source API Example (#598)
This PR adds a new example demonstrating how to build a Lambda function that handles requests from multiple sources (Application Load Balancer and API Gateway V2) using a single handler. ### What's New **New Example: `Examples/MultiSourceAPI`** A Lambda function that: - Implements `StreamingLambdaHandler` to accept raw `ByteBuffer` events - Dynamically decodes events as either `ALBTargetGroupRequest` or `APIGatewayV2Request` - Returns appropriate responses based on the detected event source - Demonstrates handling multiple AWS service integrations with a single function ### Key Features - **Type-safe event detection**: Uses Swift's `Decodable` to identify the event source at runtime - **Streaming response**: Implements `StreamingLambdaHandler` for efficient response handling - **Complete deployment**: Includes SAM template with both ALB and API Gateway V2 infrastructure - **Production-ready**: Full VPC setup with subnets, security groups, and load balancer configuration ### Files Added - `Examples/MultiSourceAPI/Sources/main.swift` - Lambda handler implementation - `Examples/MultiSourceAPI/Package.swift` - Swift package configuration - `Examples/MultiSourceAPI/template.yaml` - SAM deployment template with ALB and API Gateway V2 - `Examples/MultiSourceAPI/README.md` - Documentation with build, deploy, and test instructions - Updated CI to include the new example ### Use Case This pattern is useful when you want to: - Expose the same Lambda function through multiple AWS services - Reduce code duplication by handling similar requests from different sources - Maintain a single codebase for multi-channel APIs --------- Co-authored-by: Sebastien Stormacq <[email protected]> Co-authored-by: Josh Elkins <[email protected]>
1 parent 5c7a555 commit 97583a7

File tree

6 files changed

+350
-1
lines changed

6 files changed

+350
-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', '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', '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/MultiSourceAPI/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
9+
Package.resolved
10+
aws-sam
11+
samconfig.toml
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: "MultiSourceAPI",
10+
platforms: [.macOS(.v15)],
11+
products: [
12+
.executable(name: "MultiSourceAPI", targets: ["MultiSourceAPI"])
13+
],
14+
dependencies: [
15+
.package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"),
16+
.package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"),
17+
],
18+
targets: [
19+
.executableTarget(
20+
name: "MultiSourceAPI",
21+
dependencies: [
22+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
23+
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
24+
],
25+
path: "Sources"
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+
let indexToRemove = package.dependencies.firstIndex { dependency in
36+
switch dependency.kind {
37+
case .sourceControl(
38+
name: _,
39+
location: "https://github.com/awslabs/swift-aws-lambda-runtime.git",
40+
requirement: _
41+
):
42+
return true
43+
default:
44+
return false
45+
}
46+
}
47+
if let indexToRemove {
48+
package.dependencies.remove(at: indexToRemove)
49+
}
50+
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/MultiSourceAPI/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Multi-Source API Example
2+
3+
This example demonstrates a Lambda function that handles requests from both Application Load Balancer (ALB) and API Gateway V2 by accepting a raw `ByteBuffer` and decoding the appropriate event type.
4+
5+
## Overview
6+
7+
The Lambda handler receives events as `ByteBuffer` and attempts to decode them as either:
8+
- `ALBTargetGroupRequest` - for requests from Application Load Balancer
9+
- `APIGatewayV2Request` - for requests from API Gateway V2
10+
11+
Based on the successfully decoded type, it returns an appropriate response.
12+
13+
## Building
14+
15+
```bash
16+
swift package archive --allow-network-connections docker
17+
```
18+
19+
## Deploying
20+
21+
Deploy using SAM:
22+
23+
```bash
24+
sam deploy \
25+
--resolve-s3 \
26+
--template-file template.yaml \
27+
--stack-name MultiSourceAPI \
28+
--capabilities CAPABILITY_IAM
29+
```
30+
31+
## Testing
32+
33+
After deployment, SAM will output two URLs:
34+
35+
### Test API Gateway V2:
36+
```bash
37+
curl https://<api-id>.execute-api.<region>.amazonaws.com/apigw/test
38+
```
39+
40+
Expected response:
41+
```json
42+
{"source":"APIGatewayV2","path":"/apigw/test"}
43+
```
44+
45+
### Test ALB:
46+
```bash
47+
curl http://<alb-dns-name>/alb/test
48+
```
49+
50+
Expected response:
51+
```json
52+
{"source":"ALB","path":"/alb/test"}
53+
```
54+
55+
## How It Works
56+
57+
The handler uses Swift's type-safe decoding to determine the event source:
58+
59+
1. Receives raw `ByteBuffer` event
60+
2. Attempts to decode as `ALBTargetGroupRequest`
61+
3. If that fails, attempts to decode as `APIGatewayV2Request`
62+
4. Returns appropriate response based on the decoded type
63+
5. Throws error if neither decoding succeeds
64+
65+
This pattern is useful when a single Lambda function needs to handle requests from multiple sources.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright SwiftAWSLambdaRuntime project authors
6+
// Copyright (c) Amazon.com, Inc. or its affiliates.
7+
// Licensed under Apache License v2.0
8+
//
9+
// See LICENSE.txt for license information
10+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
11+
//
12+
// SPDX-License-Identifier: Apache-2.0
13+
//
14+
//===----------------------------------------------------------------------===//
15+
16+
import AWSLambdaEvents
17+
import AWSLambdaRuntime
18+
import NIOCore
19+
20+
#if canImport(FoundationEssentials)
21+
import FoundationEssentials
22+
#else
23+
import Foundation
24+
#endif
25+
26+
struct MultiSourceHandler: StreamingLambdaHandler {
27+
func handle(
28+
_ event: ByteBuffer,
29+
responseWriter: some LambdaResponseStreamWriter,
30+
context: LambdaContext
31+
) async throws {
32+
let decoder = JSONDecoder()
33+
let data = Data(event.readableBytesView)
34+
35+
// Try to decode as ALBTargetGroupRequest first
36+
if let albRequest = try? decoder.decode(ALBTargetGroupRequest.self, from: data) {
37+
context.logger.info("Received ALB request to path: \(albRequest.path)")
38+
39+
let response = ALBTargetGroupResponse(
40+
statusCode: .ok,
41+
headers: ["Content-Type": "application/json"],
42+
body: "{\"source\":\"ALB\",\"path\":\"\(albRequest.path)\"}"
43+
)
44+
45+
let encoder = JSONEncoder()
46+
let responseData = try encoder.encode(response)
47+
try await responseWriter.write(ByteBuffer(bytes: responseData))
48+
try await responseWriter.finish()
49+
return
50+
}
51+
52+
// Try to decode as APIGatewayV2Request
53+
if let apiGwRequest = try? decoder.decode(APIGatewayV2Request.self, from: data) {
54+
context.logger.info("Received API Gateway V2 request to path: \(apiGwRequest.rawPath)")
55+
56+
let response = APIGatewayV2Response(
57+
statusCode: .ok,
58+
headers: ["Content-Type": "application/json"],
59+
body: "{\"source\":\"APIGatewayV2\",\"path\":\"\(apiGwRequest.rawPath)\"}"
60+
)
61+
62+
let encoder = JSONEncoder()
63+
let responseData = try encoder.encode(response)
64+
try await responseWriter.write(ByteBuffer(bytes: responseData))
65+
try await responseWriter.finish()
66+
return
67+
}
68+
69+
// Unknown event type
70+
context.logger.error("Unable to decode event as ALB or API Gateway V2 request")
71+
throw LambdaError.invalidEvent
72+
}
73+
}
74+
75+
enum LambdaError: Error {
76+
case invalidEvent
77+
}
78+
79+
let runtime = LambdaRuntime(handler: MultiSourceHandler())
80+
try await runtime.run()
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: Multi-source API Lambda function with ALB and API Gateway V2
4+
5+
Resources:
6+
MultiSourceAPIFunction:
7+
Type: AWS::Serverless::Function
8+
Properties:
9+
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MultiSourceAPI/MultiSourceAPI.zip
10+
Handler: provided
11+
Runtime: provided.al2
12+
Architectures:
13+
- arm64
14+
MemorySize: 256
15+
Timeout: 30
16+
Environment:
17+
Variables:
18+
LOG_LEVEL: trace
19+
Events:
20+
ApiGatewayEvent:
21+
Type: HttpApi
22+
Properties:
23+
Path: /{proxy+}
24+
Method: ANY
25+
26+
# VPC for ALB
27+
VPC:
28+
Type: AWS::EC2::VPC
29+
Properties:
30+
CidrBlock: 10.0.0.0/16
31+
EnableDnsHostnames: true
32+
EnableDnsSupport: true
33+
34+
PublicSubnet1:
35+
Type: AWS::EC2::Subnet
36+
Properties:
37+
VpcId: !Ref VPC
38+
CidrBlock: 10.0.1.0/24
39+
AvailabilityZone: !Select [0, !GetAZs '']
40+
MapPublicIpOnLaunch: true
41+
42+
PublicSubnet2:
43+
Type: AWS::EC2::Subnet
44+
Properties:
45+
VpcId: !Ref VPC
46+
CidrBlock: 10.0.2.0/24
47+
AvailabilityZone: !Select [1, !GetAZs '']
48+
MapPublicIpOnLaunch: true
49+
50+
InternetGateway:
51+
Type: AWS::EC2::InternetGateway
52+
53+
AttachGateway:
54+
Type: AWS::EC2::VPCGatewayAttachment
55+
Properties:
56+
VpcId: !Ref VPC
57+
InternetGatewayId: !Ref InternetGateway
58+
59+
RouteTable:
60+
Type: AWS::EC2::RouteTable
61+
Properties:
62+
VpcId: !Ref VPC
63+
64+
Route:
65+
Type: AWS::EC2::Route
66+
DependsOn: AttachGateway
67+
Properties:
68+
RouteTableId: !Ref RouteTable
69+
DestinationCidrBlock: 0.0.0.0/0
70+
GatewayId: !Ref InternetGateway
71+
72+
SubnetRouteTableAssociation1:
73+
Type: AWS::EC2::SubnetRouteTableAssociation
74+
Properties:
75+
SubnetId: !Ref PublicSubnet1
76+
RouteTableId: !Ref RouteTable
77+
78+
SubnetRouteTableAssociation2:
79+
Type: AWS::EC2::SubnetRouteTableAssociation
80+
Properties:
81+
SubnetId: !Ref PublicSubnet2
82+
RouteTableId: !Ref RouteTable
83+
84+
# Application Load Balancer
85+
ALBSecurityGroup:
86+
Type: AWS::EC2::SecurityGroup
87+
Properties:
88+
GroupDescription: Security group for ALB
89+
VpcId: !Ref VPC
90+
SecurityGroupIngress:
91+
- IpProtocol: tcp
92+
FromPort: 80
93+
ToPort: 80
94+
CidrIp: 0.0.0.0/0
95+
96+
ApplicationLoadBalancer:
97+
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
98+
Properties:
99+
Scheme: internet-facing
100+
Subnets:
101+
- !Ref PublicSubnet1
102+
- !Ref PublicSubnet2
103+
SecurityGroups:
104+
- !Ref ALBSecurityGroup
105+
106+
ALBTargetGroup:
107+
Type: AWS::ElasticLoadBalancingV2::TargetGroup
108+
DependsOn: ALBLambdaInvokePermission
109+
Properties:
110+
TargetType: lambda
111+
Targets:
112+
- Id: !GetAtt MultiSourceAPIFunction.Arn
113+
114+
ALBListener:
115+
Type: AWS::ElasticLoadBalancingV2::Listener
116+
Properties:
117+
LoadBalancerArn: !Ref ApplicationLoadBalancer
118+
Port: 80
119+
Protocol: HTTP
120+
DefaultActions:
121+
- Type: forward
122+
TargetGroupArn: !Ref ALBTargetGroup
123+
124+
ALBLambdaInvokePermission:
125+
Type: AWS::Lambda::Permission
126+
Properties:
127+
FunctionName: !GetAtt MultiSourceAPIFunction.Arn
128+
Action: lambda:InvokeFunction
129+
Principal: elasticloadbalancing.amazonaws.com
130+
131+
Outputs:
132+
ApiGatewayUrl:
133+
Description: API Gateway endpoint URL
134+
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
135+
136+
ALBUrl:
137+
Description: Application Load Balancer URL
138+
Value: !Sub "http://${ApplicationLoadBalancer.DNSName}"

0 commit comments

Comments
 (0)