Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]"
archive_plugin_enabled: true

Expand Down
11 changes: 11 additions & 0 deletions Examples/MultiSourceAPI/.gitignore
Original file line number Diff line number Diff line change
@@ -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 changes: 55 additions & 0 deletions Examples/MultiSourceAPI/Package.swift
Original file line number Diff line number Diff line change
@@ -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 changes: 65 additions & 0 deletions Examples/MultiSourceAPI/README.md
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 80 additions & 0 deletions Examples/MultiSourceAPI/Sources/main.swift
Original file line number Diff line number Diff line change
@@ -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 changes: 138 additions & 0 deletions Examples/MultiSourceAPI/template.yaml
Original file line number Diff line number Diff line change
@@ -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}"