Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
53 changes: 53 additions & 0 deletions Examples/MultiSourceAPI/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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
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)
}

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}"