diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0d0b8886..6245e4af 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -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 diff --git a/Examples/MultiSourceAPI/.gitignore b/Examples/MultiSourceAPI/.gitignore new file mode 100644 index 00000000..b3b842e5 --- /dev/null +++ b/Examples/MultiSourceAPI/.gitignore @@ -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 diff --git a/Examples/MultiSourceAPI/Package.swift b/Examples/MultiSourceAPI/Package.swift new file mode 100644 index 00000000..83ec9e02 --- /dev/null +++ b/Examples/MultiSourceAPI/Package.swift @@ -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) + ] +} diff --git a/Examples/MultiSourceAPI/README.md b/Examples/MultiSourceAPI/README.md new file mode 100644 index 00000000..097e4f2f --- /dev/null +++ b/Examples/MultiSourceAPI/README.md @@ -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://.execute-api..amazonaws.com/apigw/test +``` + +Expected response: +```json +{"source":"APIGatewayV2","path":"/apigw/test"} +``` + +### Test ALB: +```bash +curl http:///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. diff --git a/Examples/MultiSourceAPI/Sources/main.swift b/Examples/MultiSourceAPI/Sources/main.swift new file mode 100644 index 00000000..461de51d --- /dev/null +++ b/Examples/MultiSourceAPI/Sources/main.swift @@ -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() diff --git a/Examples/MultiSourceAPI/template.yaml b/Examples/MultiSourceAPI/template.yaml new file mode 100644 index 00000000..2ad85a3d --- /dev/null +++ b/Examples/MultiSourceAPI/template.yaml @@ -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}"