Skip to content

Enable local HTTP server for testing #423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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: "[ 'HelloWorld', 'APIGateway','S3_AWSSDK', 'S3_Soto', 'Streaming', 'BackgroundTasks' ]"
examples: "[ 'APIGateway', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'S3_AWSSDK', 'S3_Soto', 'Streaming' ]"

archive_plugin_enabled: true

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
*.build
*.index-build
/.xcodeproj
*.pem
.podspecs
Expand All @@ -10,3 +11,4 @@ Package.resolved
.serverless
.vscode
Makefile
.devcontainer
4 changes: 4 additions & 0 deletions Examples/HelloJSON/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
response.json
samconfig.toml
template.yaml
Makefile
59 changes: 59 additions & 0 deletions Examples/HelloJSON/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// swift-tools-version:6.0

import PackageDescription

// needed for CI to test the local version of the library
import struct Foundation.URL

#if os(macOS)
let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)]
#else
let platforms: [PackageDescription.SupportedPlatform]? = nil
#endif

let package = Package(
name: "swift-aws-lambda-runtime-example",
platforms: platforms,
products: [
.executable(name: "HelloJSON", targets: ["HelloJSON"])
],
dependencies: [
// during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main")
],
targets: [
.executableTarget(
name: "HelloJSON",
dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime")
]
)
]
)

if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"],
localDepsPath != "",
let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]),
v.isDirectory == true
{
// when we use the local runtime as deps, let's remove the dependency added above
let indexToRemove = package.dependencies.firstIndex { dependency in
if case .sourceControl(
name: _,
location: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
requirement: _
) = dependency.kind {
return true
}
return false
}
if let indexToRemove {
package.dependencies.remove(at: indexToRemove)
}

// then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..)
print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)")
package.dependencies += [
.package(name: "swift-aws-lambda-runtime", path: localDepsPath)
]
}
80 changes: 80 additions & 0 deletions Examples/HelloJSON/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Hello JSON

This is a simple example of an AWS Lambda function that takes a JSON structure as input parameter and returns a JSON structure as response.

The runtime takes care of decoding the input and encoding the output.

## Code

The code defines a `HelloRequest` and `HelloResponse` data structure to represent the input and outpout payload. These structures are typically shared with a client project, such as an iOS application.

The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when an event triggers the Lambda function.

The handler is `(event: HelloRequest, context: LambdaContext)`. The function takes two arguments:
- the event argument is a `HelloRequest`. It is the parameter passed when invoking the function.
- the context argument is a `Lambda Context`. It is a description of the runtime context.

The function return value will be encoded to an `HelloResponse` as your Lambda function response.

## Build & Package

To build & archive the package, type the following commands.

```bash
swift package archive --allow-network-connections docker
```

If there is no error, there is a ZIP file ready to deploy.
The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip`

## Deploy

Here is how to deploy using the `aws` command line.

```bash
# Replace with your AWS Account ID
AWS_ACCOUNT_ID=012345678901

aws lambda create-function \
--function-name HelloJSON \
--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip \
--runtime provided.al2 \
--handler provided \
--architectures arm64 \
--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution
```

The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`.

Be sure to define the `AWS_ACCOUNT_ID` environment variable with your actual AWS account ID (for example: 012345678901).

## Invoke your Lambda function

To invoke the Lambda function, use this `aws` command line.

```bash
aws lambda invoke \
--function-name HelloJSON \
--payload $(echo '{ "name" : "Seb", "age" : 50 }' | base64) \
out.txt && cat out.txt && rm out.txt
```

Note that the payload is expected to be a valid JSON string.

This should output the following result.

```
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
{"greetings":"Hello Seb. You look younger than your age."}
```

## Undeploy

When done testing, you can delete the Lambda function with this command.

```bash
aws lambda delete-function --function-name HelloJSON
```
40 changes: 40 additions & 0 deletions Examples/HelloJSON/Sources/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// 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 AWSLambdaRuntime

// in this example we are receiving and responding with a JSON structure

// the data structure to represent the input parameter
struct HelloRequest: Decodable {
let name: String
let age: Int
}

// the data structure to represent the output response
struct HelloResponse: Encodable {
let greetings: String
}

// the Lambda runtime
let runtime = LambdaRuntime {
(event: HelloRequest, context: LambdaContext) in

HelloResponse(
greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age."
)
}

// start the loop
try await runtime.run()
2 changes: 2 additions & 0 deletions Examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This directory contains example code for Lambda functions.

- **[BackgroundTasks](BackgroundTasks/README.md)**: a Lambda function that continues to run background tasks after having sent the response (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)).

- **[HelloJSON](HelloJSON/README.md)**: a Lambda function that accepts a JSON as input parameter and responds with a JSON output (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)).

- **[HelloWorld](HelloWorld/README.md)**: a simple Lambda function (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)).

- **[S3_AWSSDK](S3_AWSSDK/README.md)**: a Lambda function that uses the [AWS SDK for Swift](https://docs.aws.amazon.com/sdk-for-swift/latest/developer-guide/getting-started.html) to invoke an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) API (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)).
Expand Down
2 changes: 1 addition & 1 deletion Plugins/AWSLambdaPackager/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ struct AWSLambdaPackager: CommandPlugin {
"\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created"
)
for (product, archivePath) in archives {
print(" * \(product.name) at \(archivePath)")
print(" * \(product.name) at \(archivePath.path())")
}
}

Expand Down
7 changes: 5 additions & 2 deletions Plugins/AWSLambdaPackager/PluginUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ struct Utils {
) throws -> String {
if logLevel >= .debug {
print("\(executable.path()) \(arguments.joined(separator: " "))")
if let customWorkingDirectory {
print("Working directory: \(customWorkingDirectory.path())")
}
}

let fd = dup(1)
Expand Down Expand Up @@ -85,8 +88,8 @@ struct Utils {
process.standardError = pipe
process.executableURL = executable
process.arguments = arguments
if let workingDirectory = customWorkingDirectory {
process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory.path())
if let customWorkingDirectory {
process.currentDirectoryURL = URL(fileURLWithPath: customWorkingDirectory.path())
}
process.terminationHandler = { _ in
outputQueue.async {
Expand Down
20 changes: 12 additions & 8 deletions Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

// commented out as long as we have a fix for Swift 6 language mode CI

#if DEBUG
import Dispatch
import Logging
Expand All @@ -20,7 +22,7 @@ import NIOCore
import NIOHTTP1
import NIOPosix

// This functionality is designed for local testing hence beind a #if DEBUG flag.
// This functionality is designed for local testing hence being a #if DEBUG flag.
// For example:
//
// try Lambda.withLocalServer {
Expand All @@ -32,16 +34,18 @@ extension Lambda {
/// Execute code in the context of a mock Lambda server.
///
/// - parameters:
/// - invocationEndpoint: The endpoint to post events to.
/// - invocationEndpoint: The endpoint to post events to.
/// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call.
///
/// - note: This API is designed strictly for local testing and is behind a DEBUG flag
static func withLocalServer<Value>(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value
{
static func withLocalServer<Value>(
invocationEndpoint: String? = nil,
_ body: @escaping () async throws -> Value
) async throws -> Value {
let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint)
try server.start().wait()
try await server.start().get()
defer { try! server.stop() }
return body()
return try await body()
}
}

Expand All @@ -61,7 +65,7 @@ private enum LocalLambda {
self.logger = logger
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.host = "127.0.0.1"
self.port = 0
self.port = 7000
self.invocationEndpoint = invocationEndpoint ?? "/invoke"
}

Expand Down Expand Up @@ -185,7 +189,7 @@ private enum LocalLambda {
}
Self.invocationState = .waitingForInvocation(promise)
case .some(let invocation):
// if there is a task pending, we can immediatly respond with it.
// if there is a task pending, we can immediately respond with it.
Self.invocationState = .waitingForLambdaResponse(invocation)
self.writeResponse(context: context, response: invocation.makeResponse())
}
Expand Down
55 changes: 38 additions & 17 deletions Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
}

public func run() async throws {
guard let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") else {
throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable)
}

let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1)
let ip = String(ipAndPort[0])
guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) }

let handler = self.handlerMutex.withLockedValue { handler in
let result = handler
handler = nil
Expand All @@ -61,16 +53,45 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce)
}

try await LambdaRuntimeClient.withRuntimeClient(
configuration: .init(ip: ip, port: port),
eventLoop: self.eventLoop,
logger: self.logger
) { runtimeClient in
try await Lambda.runLoop(
runtimeClient: runtimeClient,
handler: handler,
// are we running inside an AWS Lambda runtime environment ?
// AWS_LAMBDA_RUNTIME_API is set when running on Lambda
// https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") {

let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1)
let ip = String(ipAndPort[0])
guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) }

try await LambdaRuntimeClient.withRuntimeClient(
configuration: .init(ip: ip, port: port),
eventLoop: self.eventLoop,
logger: self.logger
)
) { runtimeClient in
try await Lambda.runLoop(
runtimeClient: runtimeClient,
handler: handler,
logger: self.logger
)
}

} else {

// we're not running on Lambda, let's start a local server for testing
try await Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT"))
{

try await LambdaRuntimeClient.withRuntimeClient(
configuration: .init(ip: "127.0.0.1", port: 7000),
eventLoop: self.eventLoop,
logger: self.logger
) { runtimeClient in
try await Lambda.runLoop(
runtimeClient: runtimeClient,
handler: handler,
logger: self.logger
)
}
}
}
}
}
Loading
Loading