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 <stormacq@amazon.lu>
Co-authored-by: Josh Elkins <jbelkins@users.noreply.github.com>
This commit is contained in:
Sébastien Stormacq
2025-11-02 21:58:02 +01:00
committed by GitHub
parent 5c7a55500c
commit 97583a78c2
6 changed files with 350 additions and 1 deletions
+11
View File
@@ -0,0 +1,11 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved
aws-sam
samconfig.toml
+55
View File
@@ -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: "MultiSourceAPI",
platforms: [.macOS(.v15)],
products: [
.executable(name: "MultiSourceAPI", targets: ["MultiSourceAPI"])
],
dependencies: [
.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: "MultiSourceAPI",
dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
],
path: "Sources"
)
]
)
if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"],
localDepsPath != "",
let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]),
v.isDirectory == true
{
let indexToRemove = package.dependencies.firstIndex { dependency in
switch dependency.kind {
case .sourceControl(
name: _,
location: "https://github.com/awslabs/swift-aws-lambda-runtime.git",
requirement: _
):
return true
default:
return false
}
}
if let indexToRemove {
package.dependencies.remove(at: indexToRemove)
}
print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)")
package.dependencies += [
.package(name: "swift-aws-lambda-runtime", path: localDepsPath)
]
}
+65
View File
@@ -0,0 +1,65 @@
# Multi-Source API Example
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.
## Overview
The Lambda handler receives events as `ByteBuffer` and attempts to decode them as either:
- `ALBTargetGroupRequest` - for requests from Application Load Balancer
- `APIGatewayV2Request` - for requests from API Gateway V2
Based on the successfully decoded type, it returns an appropriate response.
## Building
```bash
swift package archive --allow-network-connections docker
```
## Deploying
Deploy using SAM:
```bash
sam deploy \
--resolve-s3 \
--template-file template.yaml \
--stack-name MultiSourceAPI \
--capabilities CAPABILITY_IAM
```
## Testing
After deployment, SAM will output two URLs:
### Test API Gateway V2:
```bash
curl https://<api-id>.execute-api.<region>.amazonaws.com/apigw/test
```
Expected response:
```json
{"source":"APIGatewayV2","path":"/apigw/test"}
```
### Test ALB:
```bash
curl http://<alb-dns-name>/alb/test
```
Expected response:
```json
{"source":"ALB","path":"/alb/test"}
```
## How It Works
The handler uses Swift's type-safe decoding to determine the event source:
1. Receives raw `ByteBuffer` event
2. Attempts to decode as `ALBTargetGroupRequest`
3. If that fails, attempts to decode as `APIGatewayV2Request`
4. Returns appropriate response based on the decoded type
5. Throws error if neither decoding succeeds
This pattern is useful when a single Lambda function needs to handle requests from multiple sources.
@@ -0,0 +1,80 @@
//===----------------------------------------------------------------------===//
//
// 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
import NIOCore
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
struct MultiSourceHandler: StreamingLambdaHandler {
func handle(
_ event: ByteBuffer,
responseWriter: some LambdaResponseStreamWriter,
context: LambdaContext
) async throws {
let decoder = JSONDecoder()
let data = Data(event.readableBytesView)
// Try to decode as ALBTargetGroupRequest first
if let albRequest = try? decoder.decode(ALBTargetGroupRequest.self, from: data) {
context.logger.info("Received ALB request to path: \(albRequest.path)")
let response = ALBTargetGroupResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: "{\"source\":\"ALB\",\"path\":\"\(albRequest.path)\"}"
)
let encoder = JSONEncoder()
let responseData = try encoder.encode(response)
try await responseWriter.write(ByteBuffer(bytes: responseData))
try await responseWriter.finish()
return
}
// Try to decode as APIGatewayV2Request
if let apiGwRequest = try? decoder.decode(APIGatewayV2Request.self, from: data) {
context.logger.info("Received API Gateway V2 request to path: \(apiGwRequest.rawPath)")
let response = APIGatewayV2Response(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: "{\"source\":\"APIGatewayV2\",\"path\":\"\(apiGwRequest.rawPath)\"}"
)
let encoder = JSONEncoder()
let responseData = try encoder.encode(response)
try await responseWriter.write(ByteBuffer(bytes: responseData))
try await responseWriter.finish()
return
}
// Unknown event type
context.logger.error("Unable to decode event as ALB or API Gateway V2 request")
throw LambdaError.invalidEvent
}
}
enum LambdaError: Error {
case invalidEvent
}
let runtime = LambdaRuntime(handler: MultiSourceHandler())
try await runtime.run()
+138
View File
@@ -0,0 +1,138 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Multi-source API Lambda function with ALB and API Gateway V2
Resources:
MultiSourceAPIFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MultiSourceAPI/MultiSourceAPI.zip
Handler: provided
Runtime: provided.al2
Architectures:
- arm64
MemorySize: 256
Timeout: 30
Environment:
Variables:
LOG_LEVEL: trace
Events:
ApiGatewayEvent:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
# VPC for ALB
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
RouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Route:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref RouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
SubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref RouteTable
SubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref RouteTable
# Application Load Balancer
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for ALB
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup
ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: ALBLambdaInvokePermission
Properties:
TargetType: lambda
Targets:
- Id: !GetAtt MultiSourceAPIFunction.Arn
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ALBTargetGroup
ALBLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt MultiSourceAPIFunction.Arn
Action: lambda:InvokeFunction
Principal: elasticloadbalancing.amazonaws.com
Outputs:
ApiGatewayUrl:
Description: API Gateway endpoint URL
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
ALBUrl:
Description: Application Load Balancer URL
Value: !Sub "http://${ApplicationLoadBalancer.DNSName}"