mirror of
https://github.com/swift-server/swift-aws-lambda-runtime.git
synced 2026-05-03 07:22:27 +00:00
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:
committed by
GitHub
parent
5c7a55500c
commit
97583a78c2
@@ -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
|
||||
@@ -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)
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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}"
|
||||
Reference in New Issue
Block a user