Skip to content
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