LambdaSwiftSprinter is a Swift framework allowing the development of AWS Lambdas based on the AWS Lambda Custom Runtime for Swift.
It's required Swift 5 to build the code.
Follow the instruction on the official Swift web site to prepare your development environment. The repository Swift-Sprinter contains a full description of what is an AWS Lambda Custom Runtime for Swift and how to build and use it.
From the command line, create a directory to contain your project
mkdir HelloWorld
cd HelloWorld
Use swift package manager to start your project:
swift package init --type executable
Edit the file Package.swift by adding the dependency LambdaSwiftSprinter to the target:
import PackageDescription
let package = Package(
name: "HelloWorld",
dependencies: [
.package(url: "https://github.com/swift-sprinter/aws-lambda-swift-sprinter-core", from: "1.0.0")
],
targets: [
.target(
name: "HelloWorld",
dependencies: ["LambdaSwiftSprinter"]),
.testTarget(
name: "HelloWorldTests",
dependencies: ["HelloWorld"]),
]
)
Modify the main.swift file with the following code:
import LambdaSwiftSprinter
import Foundation
struct Event: Codable {
let name: String
}
struct Response: Codable {
let message: String
}
let syncLambda: SyncCodableLambda<Event, Response> = { (event, context) throws -> Response in
let message = "Hello World! Hello \(event.name)!"
return Response(message: message)
}
public func log(_ object: Any, flush: Bool = false) {
fputs("\(object)\n", stderr)
if flush {
fflush(stderr)
}
}
do {
let sprinter = try SprinterCURL()
sprinter.register(handler: "helloWorld", lambda: syncLambda)
try sprinter.run()
} catch let error {
log(String(describing: error))
}
Update the lambda with your code.
- The Event object is the JSON received by the Lambda invocation
- The Context object contains the information to interact with AWS Lambda.
- The Response object is the response of your AWS Lamda invocation.
- The Handler contains the implementation of the code executed by the AWS Lambda. (SyncCodableLambda<Event, Response> in the example)
To run the lambda in the custom runtime environment with SwiftSprinter it's required to:
- Define the lambda handler:
let lambda: SyncCodableLambda<Event, Response> = ...
- Init the sprinter:
let sprinter = try SprinterCURL()
- Register the lambda handler:
sprinter.register(handler: "helloWorld", lambda: lambda)
- Run the sprinter:
try sprinter.run()
- Log: It's a good practice to enclose the code in a do/catch, this will ensure to log all the errors in the AWS Lambda error output.
Refer to the lambda programming model for more info on AWS Lambda.
The examples are maintained here https://github.com/swift-sprinter/aws-lambda-swift-sprinter, to keep the size of this Swift package small.
- HelloWorld: A basic Lambda Swift example
- HTTPSRequest: A basic example showing how to perform an HTTPS request from the Swift Lambda using the LambdaSwiftSprinterNioPlugin
- S3Test: A basic example showing how to access an S3 bucket from the Swift Lambda using https://github.com/swift-aws/aws-sdk-swift.
The LambdaSwiftSprinter framework has been designed to implement the following goals:
- Codable Event and Response
- Dictionary Event and Response
- Synchronous and Asynchronous functions
- Plugin architecture
- No third party dependencies
- Safe Context
- Error throwing
To ensure the Event and Response JSON passed through the lambda are converted into struct by Swift-Sprinter, you need to declare them as Codable:
struct Event: Codable {
let name: String
}
struct Response: Codable {
let message: String
}
and then implement you synchronous AsyncCodableLambda<Event: Decodable, Response: Encodable>
or asynchronous SyncCodableLambda<Event: Decodable, Response: Encodable>
lambda.
// Synchronous lambda example:
let syncLambda: SyncCodableLambda<Event, Response> = { (event, context) throws -> Response in
let message = "Hello World! Hello \(event.name)!"
return Response(message: message)
}
// Asynchronous lambda example:
let asyncLambda: AsyncCodableLambda<Event, Response> = { (event, context, completion) -> Void in
let message = "Hello World! Hello \(event.name)!"
completion(.success(Response(message: message)))
}
Sometime could not be convenient to define the Event and Response with a fixed struct. In this case, could be better to receive a dictionary from the JSON passed through the lambda by Swift-Sprinter.
In this case, it's possible to define the asynchronous lambda with AsyncDictionaryLambda
or the synchronous lambda with SyncDictionaryLambda
.
// Synchronous lambda example:
let syncDictLambda = { (dictionary: [String: Any], context: Context) throws -> [String: Any] in
var result = [String: Any]()
if let name = dictionary["name"] as? String {
let message = "Hello World! Hello \(name)!"
result["message"] = message
} else {
throw MyLambdaError.invalidEvent
}
return result
}
// Asynchronous lambda example:
let asyncDictLambda: AsyncDictionaryLambda = { (dictionary, context, completion) in
var result = [String: Any]()
if let name = dictionary["name"] as? String {
let message = "Hello World! Hello \(name)!"
result["message"] = message
} else {
completion(.failure(MyLambdaError.invalidEvent))
}
completion(.success(result))
}
Asynchronous lambda functions call a completion handler to return a Result:
Use AsyncCodableLambda<Event: Decodable, Response: Encodable>
and AsyncDictionaryLambda
to define an asynchronous lambda. The completion handler takes a Result<Value, Error> as paremater with a .success(value) or .failure(error).
The code inside the asynchronous lambda could be asynchronous and the completion handler must be called to send the result.
let asyncLambda: AsyncCodableLambda<Event, Response> = { event, context, completion in
let message = "Hello World! Hello \(event.name)!"
return completion(.success(Response(message: message)))
}
let asyncDictLambda: AsyncDictionaryLambda = { (dictionary, context, completion) in
var result = [String: Any]()
if let name = dictionary["name"] as? String {
let message = "Hello World! Hello \(name)!"
result["message"] = message
} else {
completion(.failure(MyLambdaError.invalidEvent))
}
completion(.success(result))
}
Synchronous lambda functions returns a value or throws an error:
Use SyncCodableLambda<Event: Decodable, Response: Encodable>
and SyncDictionaryLambda
to define an synchronous lambda. The code inside the synchronous lambda must be synchronous.
let syncLambda: SyncCodableLambda<Event, Response> = { (event, context) throws -> Response in
let message = "Hello World! Hello \(event.name)!"
return Response(message: message)
}
let syncDictLambda = { (dictionary: [String: Any], context: Context) throws -> [String: Any] in
var result = [String: Any]()
if let name = dictionary["name"] as? String {
let message = "Hello World! Hello \(name)!"
result["message"] = message
} else {
throw MyLambdaError.invalidEvent
}
return result
}
It's possible to extend the library by implementing SyncLambdaHandler
and AsyncLambdaHandler
.
The Sprinter class depends on the implementation of the LambdaAPI protocol. This allows adding LambdaAPI classes as a plugin.
public protocol LambdaAPI: class {
init(awsLambdaRuntimeAPI: String) throws
func getNextInvocation() throws -> (event: Data, responseHeaders: [AnyHashable: Any])
func postInvocationResponse(for requestId: String, httpBody: Data) throws
func postInvocationError(for requestId: String, error: Error) throws
func postInitializationError(error: Error) throws
}
The default implementation LambdaApiCURL is based on the Foundation class URLSession and will call the AWS Runtime API using this class.
One of the main issues on server-side swift is resolving the software dependencies. The fragmentation of the existing server-side library and the use of different versions could add complexity to the core library. By design, this core framework does not depend on other third-party frameworks. In particular, this choice allows using differents Logging and Network framework and a custom implementation of the LambdaAPI.
The Context object, passed inside the lambda, contains all the environment variables defined by the Lambda implementation: https://docs.aws.amazon.com/lambda/latest/dg/lambda-environment-variables.html
The Context object, passed inside the lambda, contains all the response headers variables defined by the Lambda Runtime API implementation: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
In case some of the required Environment variables or Response Headers are not found an error is thrown.
By design, the library throws all the errors. This will ensure:
- All the errors are not hidden inside the implementation.
- It's possible to use a custom logging library.
Library errors:
public enum SprinterError: Error {
/// A required Environment variable is missing.
case missingEnvironmentVariables(Context.AWSEnvironmentKey)
/// A required Response Header variable is missing.
case missingResponseHeaderVariables(Context.ResponseHeaderKey)
/// The handler is missing or does not contain a ```.``` in the name.
/// A valid name must have a format similar to 'Executable.handler'
case invalidHandlerName(String)
/// API Runtime Error
case endpointError(String)
/// The JSON payload is nil
case invalidJSON
}
Encoding/Decoding and errors:
If the code uses the Codable Event and Response all the encoding and decoding errors will be thrown.
Basic Logging:
To print all the errors to the lambda error output it's possible to use the following code:
public func log(_ object: Any, flush: Bool = false) {
fputs("\(object)\n", stderr)
if flush {
fflush(stderr)
}
}
A do/catch will ensure all the error will be reported to the AWS lambda output.
do {
let sprinter = try SprinterCURL()
sprinter.register(handler: "helloWorld", lambda: lambda)
try sprinter.run()
} catch let error {
log(String(describing: error))
}
Note:
Use String(describing: error)
to convert an Error to String.
If you use error.localizedDescription
, the string on Linux will be "The operation could not be completed"
.
As documented by sebsto in the AWS Lambda Runtime Swift forum
Trying to make an HTTPS connection from the lambda function (with the Foundation library) it fails with "error setting certificate verify locations:\n CAfile: /etc/ssl/certs/ca-certificates.crt\n CApath: /etc/ssl/certs".
For this reason, it's required a plugin based on NIO 2 to prevent the HTTPS issue. One of the goals of this core library is to allow the developers to use their NIO 2 library to work around the issue.
Contributions are more than welcome! Follow this guide to contribute.
This project has been inspired by the amazing work of the following people:
-
Matthew Burke, Capital One : https://medium.com/capital-one-tech/serverless-computing-with-swift-f515ff052919
-
Justin Sanders : https://medium.com/@gigq/using-swift-in-aws-lambda-6e2a67a27e03
-
Claus Höfele : https://medium.com/@claushoefele/serverless-swift-2e8dce589b68
-
Kohki Miki, Cookpad : https://github.com/giginet/aws-lambda-swift-runtime
-
Toni Sutter : https://github.com/tonisuter/aws-lambda-swift
-
Sébastien Stormacq : https://github.com/sebsto/swift-custom-runtime-lambda
A special thanks to BJSS to sustain me in delivering this project.