Skip to content

Swift implementation of AWS Lambda Runtime

License

Notifications You must be signed in to change notification settings

swift-server/swift-aws-lambda-runtime

Swift AWS Lambda Runtime

Many modern systems have client components like iOS, macOS or watchOS applications as well as server components that those clients interact with. Serverless functions are often the easiest and most efficient way for client application developers to extend their applications into the cloud.

Serverless functions are increasingly becoming a popular choice for running event-driven or otherwise ad-hoc compute tasks in the cloud. They power mission critical microservices and data intensive workloads. In many cases, serverless functions allow developers to more easily scale and control compute costs given their on-demand nature.

When using serverless functions, attention must be given to resource utilization as it directly impacts the costs of the system. This is where Swift shines! With its low memory footprint, deterministic performance, and quick start time, Swift is a fantastic match for the serverless functions architecture.

Combine this with Swift's developer friendliness, expressiveness, and emphasis on safety, and we have a solution that is great for developers at all skill levels, scalable, and cost effective.

Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift simple and safe. The library is an implementation of theAWS Lambda Runtime APIand uses an embedded asynchronous HTTP Client based onSwiftNIOthat is fine-tuned for performance in the AWS Runtime context. The library provides a multi-tier API that allows building a range of Lambda functions: From quick and simple closures to complex, performance-sensitive event handlers.

Getting started

If you have never used AWS Lambda or Docker before, check out thisgetting started guidewhich helps you with every step from zero to a running Lambda.

First, create a SwiftPM project and pull Swift AWS Lambda Runtime as dependency into your project

// swift-tools-version:5.7

import PackageDescription

letpackage=Package(
name:"MyLambda",
products:[
.executable(name:"MyLambda",targets:["MyLambda"]),
],
dependencies:[
.package(url:"https://github.com/swift-server/swift-aws-lambda-runtime.git",from:"1.0.0-alpha"),
],
targets:[
.executableTarget(name:"MyLambda",dependencies:[
.product(name:"AWSLambdaRuntime",package:"swift-aws-lambda-runtime"),
]),
]
)

Next, create aMyLambda.swiftand implement your Lambda. Note that the file can not be namedmain.swiftor you will encounter the following error:'main' attribute cannot be used in a module that contains top-level code.

Using async function

The simplest way to useAWSLambdaRuntimeis to use theSimpleLambdaHandlerprotocol and pass in an async function, for example:

// Import the module
import AWSLambdaRuntime

@main
structMyLambda:SimpleLambdaHandler{
// in this example we are receiving and responding with strings
funchandle(_ name:String,context:LambdaContext)asyncthrows->String{
"Hello,\(name)"
}
}

More commonly, the event would be a JSON, which is modeled usingCodable,for example:

// Import the module
import AWSLambdaRuntime

// Request, uses Codable for transparent JSON encoding
structRequest:Codable{
letname:String
}

// Response, uses Codable for transparent JSON encoding
structResponse:Codable{
letmessage:String
}

@main
structMyLambda:SimpleLambdaHandler{
// In this example we are receiving and responding with `Codable`.
funchandle(_ request:Request,context:LambdaContext)asyncthrows->Response{
Response(message:"Hello,\(request.name)")
}
}

Since most Lambda functions are triggered by events originating in the AWS platform likeSNS,SQSorAPIGateway,theSwift AWS Lambda Eventspackage includes anAWSLambdaEventsmodule that provides implementations for most common AWS event types further simplifying writing Lambda functions. For example, handling aSQSmessage:

First, add a dependency on the event packages:

// swift-tools-version:5.7

import PackageDescription

letpackage=Package(
name:"MyLambda",
products:[
.executable(name:"MyLambda",targets:["MyLambda"]),
],
dependencies:[
.package(url:"https://github.com/swift-server/swift-aws-lambda-runtime.git",from:"1.0.0-alpha"),
.package(url:"https://github.com/swift-server/swift-aws-lambda-events.git",branch:"main"),
],
targets:[
.executableTarget(name:"MyLambda",dependencies:[
.product(name:"AWSLambdaRuntime",package:"swift-aws-lambda-runtime"),
.product(name:"AWSLambdaEvents",package:"swift-aws-lambda-events"),
]),
]
)

Then in your Lambda:

// Import the modules
import AWSLambdaRuntime
import AWSLambdaEvents

@main
structMyLambda:SimpleLambdaHandler{
// In this example we are receiving a SQS Event, with no response (Void).
funchandle(_ event:SQSEvent,context:LambdaContext)asyncthrows{
...
}
}

In some cases, the Lambda needs to do work on initialization. In such cases, use theLambdaHandlerinstead of theSimpleLambdaHandlerwhich has an additional initialization method. For example:

import AWSLambdaRuntime

@main
structMyLambda:LambdaHandler{
init(context:LambdaInitializationContext)asyncthrows{
...
}

funchandle(_ event:String,context:LambdaContext)asyncthrows->Void{
...
}
}

Modeling Lambda functions as async functions is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets hang, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API.

Using EventLoopLambdaHandler

Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime usesSwiftNIOas its underlying networking engine which means the APIs are based onSwiftNIOconcurrency primitives like theEventLoopandEventLoopFuture.For example:

// Import the modules
import AWSLambdaRuntime
import AWSLambdaEvents
import NIOCore

@main
structHandler:EventLoopLambdaHandler{
typealiasEvent=SNSEvent.Message// Event / Request type
typealiasOutput=Void// Output / Response type

staticfuncmakeHandler(context:LambdaInitializationContext)->EventLoopFuture<Self>{
context.eventLoop.makeSucceededFuture(Self())
}

// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread
// while the closure-based handlers do.
funchandle(_ event:Event,context:LambdaContext)->EventLoopFuture<Output>{
...
context.eventLoop.makeSucceededFuture(Void())
}
}

Beyond the small cognitive complexity of using theEventLoopFuturebased APIs, note these APIs should be used with extra care. AnEventLoopLambdaHandlerwill execute the user code on the sameEventLoop(thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning.

Testing Locally

Before deploying your code to AWS Lambda, you can test it locally by setting theLOCAL_LAMBDA_SERVER_ENABLEDenvironment variable to true. It will look like this on CLI:

LOCAL_LAMBDA_SERVER_ENABLED=true swift run

This starts a local HTTP server listening on port 7000. You can invoke your local Lambda function by sending an HTTP POST request tohttp://127.0.0.1:7000/invoke.

The request must include the JSON payload expected as anEventby your function. You can create a text file with the JSON payload documented by AWS or captured from a trace. In this example, we usedthe APIGatewayv2 JSON payload from the documentation,saved asevents/create-session.jsontext file.

Then we use curl to invoke the local endpoint with the test JSON payload.

curl -v --header"Content-Type:\ application/json"--data @events/create-session.json http://127.0.0.1:7000/invoke
*Trying 127.0.0.1:7000...
*Connected to 127.0.0.1 (127.0.0.1) port 7000
>POST /invoke HTTP/1.1
>Host: 127.0.0.1:7000
>User-Agent: curl/8.4.0
>Accept:*/*
>Content-Type:\application/json
>Content-Length: 1160
>
<HTTP/1.1 200 OK
<content-length: 247
<
*Connection#0 to host 127.0.0.1 left intact
{"statusCode":200,"isBase64Encoded":false,"body":"...","headers":{"Access-Control-Allow-Origin":"*","Content-Type":"application\/json; charset=utf-8","Access-Control-Allow-Headers":"*"}}

Modifying the local endpoint

By default, when using the local Lambda server, it listens on the/invokeendpoint.

Some testing tools, such as theAWS Lambda runtime interface emulator,require a different endpoint. In that case, you can use theLOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINTenvironment variable to force the runtime to listen on a different endpoint.

Example:

LOCAL_LAMBDA_SERVER_ENABLED=true LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run

Increase logging verbosity

You can increase the verbosity of the runtime using theLOG_LEVELenvironment variable.

  • LOG_LEVEL=debugdisplays information about the Swift AWS Lambda Runtime activity and lifecycle
  • LOG_LEVEL=tracedisplays a string representation of the input event as received from the AWS Lambda service (before invoking your handler).

You can modify the verbosity of a Lambda function by passing the LOG_LEVEL environment variable both during your local testing (LOG_LEVEL=trace LOCAL_LAMBDA_SERVER_ENABLED=true swift run) or when you deploy your code on AWS Lambda. You candefine environment variables for your Lambda functionsin the AWS console or programmatically.

This repository followsSwift's Log Level Guidelines.AtLOG_LEVEL=trace,the AWS Lambda runtime will display a string representation of the input event as received from the AWS Lambda service before invoking your handler, for maximum debuggability.

Deploying to AWS Lambda

To deploy Lambda functions to AWS Lambda, you need to compile the code for Amazon Linux which is the OS used on AWS Lambda microVMs, package it as a Zip file, and upload to AWS.

Swift AWS Lambda Runtime includes a SwiftPM plugin designed to help with the creation of the zip archive. To build and package your Lambda, run the following command:

swift package archive

Thearchivecommand can be customized using the following parameters

  • --output-pathA valid file system path where a folder with the archive operation result will be placed. This folder will contain the following elements:
    • A file link namedbootstrap
    • An executable file
    • AZipfile ready to be uploaded to AWS
  • --verboseA number that sets the command output detail level between the following values:
    • 0(Silent)
    • 1(Output)
    • 2(Debug)
  • --swift-versionSwift language version used to define the Amazon Linux 2 Docker image. For example "5.7.3"
  • --base-docker-imageAn Amazon Linux 2 docker image name available in your system.
  • --disable-docker-image-updateIf flag is set, docker image will not be updated and local image will be used.

Both--swift-versionand--base-docker-imageare mutually exclusive

Here's an example

swift package archive --output-path /Users/JohnAppleseed/Desktop --verbose 2

This command execution will generate a folder at/Users/JohnAppleseed/Desktopwith the lambda zipped and ready to upload it and set the command detail output level to2(debug)

on macOS, the archiving plugin uses docker to build the Lambda for Amazon Linux 2, and as such requires to communicate with Docker over the localhost network. At the moment, SwiftPM does not allow plugin communication over network, and as such the invocation requires breaking from the SwiftPM plugin sandbox. This limitation would be removed in the future.

swift package --disable-sandbox archive

AWS offers several tools to interact and deploy Lambda functions to AWS Lambda includingSAMand theAWS CLI.TheExamples Directoryincludes complete sample build and deployment scripts that utilize these tools.

Note the examples mentioned above use dynamic linking, therefore bundle the required Swift libraries in the Zip package along side the executable. You may choose to link the Lambda function statically (using-static-stdlib) which could improve performance but requires additional linker flags.

To build the Lambda function for Amazon Linux 2, use the Docker image published by Swift.org onSwift toolchains and Docker images for Amazon Linux 2,as demonstrated in the examples.

Architecture

The library defines four protocols for the implementation of a Lambda Handler. From low-level to more convenient:

ByteBufferLambdaHandler

AnEventLoopFuturebased processing protocol for a Lambda that takes aByteBufferand returns aByteBuffer?asynchronously.

ByteBufferLambdaHandleris the lowest level protocol designed to power the higher levelEventLoopLambdaHandlerandLambdaHandlerbased APIs. Users are not expected to use this protocol, though some performance sensitive applications that operate at theByteBufferlevel or have special serialization needs may choose to do so.

publicprotocolByteBufferLambdaHandler{
/// Create a Lambda handler for the runtime.
///
/// Use this to initialize all your resources that you want to cache between invocations. This could be database
/// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance
/// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it
/// minimizes thread hopping.
staticfuncmakeHandler(context:LambdaInitializationContext)->EventLoopFuture<Self>

/// The Lambda handling method.
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
/// - parameters:
/// - context: Runtime ``LambdaContext``.
/// - event: The event or input payload encoded as `ByteBuffer`.
///
/// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine.
/// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error`.
funchandle(_ buffer:ByteBuffer,context:LambdaContext)->EventLoopFuture<ByteBuffer?>
}

EventLoopLambdaHandler

EventLoopLambdaHandleris a strongly typed,EventLoopFuturebased asynchronous processing protocol for a Lambda that takes a user definedEventand returns a user definedOutput.

EventLoopLambdaHandlerprovidesByteBuffer->Eventdecoding andOutput->ByteBuffer?encoding forCodableandString.

EventLoopLambdaHandlerexecutes the user provided Lambda on the sameEventLoopas the core runtime engine, making the processing fast but requires more care from the implementation to never block theEventLoop.It it designed for performance sensitive applications that useCodableorStringbased Lambda functions.

publicprotocolEventLoopLambdaHandler{
/// The lambda functions input. In most cases this should be `Codable`. If your event originates from an
/// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events),
/// which provides a number of commonly used AWS Event implementations.
associatedtypeEvent
/// The lambda functions output. Can be `Void`.
associatedtypeOutput

/// Create a Lambda handler for the runtime.
///
/// Use this to initialize all your resources that you want to cache between invocations. This could be database
/// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance
/// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it
/// minimizes thread hopping.
staticfuncmakeHandler(context:LambdaInitializationContext)->EventLoopFuture<Self>

/// The Lambda handling method.
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
/// - parameters:
/// - context: Runtime ``LambdaContext``.
/// - event: Event of type `Event` representing the event or request.
///
/// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine.
/// The `EventLoopFuture` should be completed with either a response of type ``Output`` or an `Error`.
funchandle(_ event:Event,context:LambdaContext)->EventLoopFuture<Output>

/// Encode a response of type ``Output`` to `ByteBuffer`.
/// Concrete Lambda handlers implement this method to provide coding functionality.
/// - parameters:
/// - value: Response of type ``Output``.
/// - buffer: A `ByteBuffer` to encode into, will be overwritten.
///
/// - Returns: A `ByteBuffer` with the encoded version of the `value`.
funcencode(value:Output,into buffer:inoutByteBuffer)throws

/// Decode a `ByteBuffer` to a request or event of type ``Event``.
/// Concrete Lambda handlers implement this method to provide coding functionality.
///
/// - parameters:
/// - buffer: The `ByteBuffer` to decode.
///
/// - Returns: A request or event of type ``Event``.
funcdecode(buffer:ByteBuffer)throws->Event
}

LambdaHandler

LambdaHandleris a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user definedEventand returns a user definedOutput.

LambdaHandlerprovidesByteBuffer->Eventdecoding andOutput->ByteBufferencoding forCodableandString.

LambdaHandleroffloads the user provided Lambda execution to an async task making processing safer but slightly slower.

publicprotocolLambdaHandler{
/// The lambda function's input. In most cases this should be `Codable`. If your event originates from an
/// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events),
/// which provides a number of commonly used AWS Event implementations.
associatedtypeEvent
/// The lambda function's output. Can be `Void`.
associatedtypeOutput

/// The Lambda initialization method.
/// Use this method to initialize resources that will be used in every request.
///
/// Examples for this can be HTTP or database clients.
/// - parameters:
/// - context: Runtime ``LambdaInitializationContext``.
init(context:LambdaInitializationContext)asyncthrows

/// The Lambda handling method.
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
/// - parameters:
/// - event: Event of type `Event` representing the event or request.
/// - context: Runtime ``LambdaContext``.
///
/// - Returns: A Lambda result ot type `Output`.
funchandle(_ event:Event,context:LambdaContext)asyncthrows->Output

/// Encode a response of type ``Output`` to `ByteBuffer`.
/// Concrete Lambda handlers implement this method to provide coding functionality.
/// - parameters:
/// - value: Response of type ``Output``.
/// - buffer: A `ByteBuffer` to encode into, will be overwritten.
///
/// - Returns: A `ByteBuffer` with the encoded version of the `value`.
funcencode(value:Output,into buffer:inoutByteBuffer)throws

/// Decode a `ByteBuffer` to a request or event of type ``Event``.
/// Concrete Lambda handlers implement this method to provide coding functionality.
///
/// - parameters:
/// - buffer: The `ByteBuffer` to decode.
///
/// - Returns: A request or event of type ``Event``.
funcdecode(buffer:ByteBuffer)throws->Event
}

SimpleLambdaHandler

SimpleLambdaHandleris a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user definedEventand returns a user definedOutput.

SimpleLambdaHandlerprovidesByteBuffer->Eventdecoding andOutput->ByteBufferencoding forCodableandString.

SimpleLambdaHandleris the same asLambdaHandler,but does not require explicit initialization.

publicprotocolSimpleLambdaHandler{
/// The lambda function's input. In most cases this should be `Codable`. If your event originates from an
/// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events),
/// which provides a number of commonly used AWS Event implementations.
associatedtypeEvent
/// The lambda function's output. Can be `Void`.
associatedtypeOutput

init()

/// The Lambda handling method.
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
/// - parameters:
/// - event: Event of type `Event` representing the event or request.
/// - context: Runtime ``LambdaContext``.
///
/// - Returns: A Lambda result ot type `Output`.
funchandle(_ event:Event,context:LambdaContext)asyncthrows->Output

/// Encode a response of type ``Output`` to `ByteBuffer`.
/// Concrete Lambda handlers implement this method to provide coding functionality.
/// - parameters:
/// - value: Response of type ``Output``.
/// - buffer: A `ByteBuffer` to encode into, will be overwritten.
///
/// - Returns: A `ByteBuffer` with the encoded version of the `value`.
funcencode(value:Output,into buffer:inoutByteBuffer)throws

/// Decode a `ByteBuffer` to a request or event of type ``Event``.
/// Concrete Lambda handlers implement this method to provide coding functionality.
///
/// - parameters:
/// - buffer: The `ByteBuffer` to decode.
///
/// - Returns: A request or event of type ``Event``.
funcdecode(buffer:ByteBuffer)throws->Event
}

Context

When calling the user provided Lambda function, the library provides aLambdaContextclass that provides metadata about the execution context, as well as utilities for logging and allocating buffers.

publicstructLambdaContext:CustomDebugStringConvertible,Sendable{
/// The request ID, which identifies the request that triggered the function invocation.
publicvarrequestID:String{
self.storage.requestID
}

/// The AWS X-Ray tracing header.
publicvartraceID:String{
self.storage.traceID
}

/// The ARN of the Lambda function, version, or alias that's specified in the invocation.
publicvarinvokedFunctionARN:String{
self.storage.invokedFunctionARN
}

/// The timestamp that the function times out.
publicvardeadline:DispatchWallTime{
self.storage.deadline
}

/// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider.
publicvarcognitoIdentity:String?{
self.storage.cognitoIdentity
}

/// For invocations from the AWS Mobile SDK, data about the client application and device.
publicvarclientContext:String?{
self.storage.clientContext
}

/// `Logger` to log with.
///
/// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable.
publicvarlogger:Logger{
self.storage.logger
}

/// The `EventLoop` the Lambda is executed on. Use this to schedule work with.
/// This is useful when implementing the ``EventLoopLambdaHandler`` protocol.
///
/// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care.
/// Most importantly the `EventLoop` must never be blocked.
publicvareventLoop:EventLoop{
self.storage.eventLoop
}

/// `ByteBufferAllocator` to allocate `ByteBuffer`.
/// This is useful when implementing ``EventLoopLambdaHandler``.
publicvarallocator:ByteBufferAllocator{
self.storage.allocator
}
}

Similarally, the library provides a context if and when initializing the Lambda.

publicstructLambdaInitializationContext:Sendable{
/// `Logger` to log with.
///
/// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable.
publicletlogger:Logger

/// The `EventLoop` the Lambda is executed on. Use this to schedule work with.
///
/// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care.
/// Most importantly the `EventLoop` must never be blocked.
publicleteventLoop:EventLoop

/// `ByteBufferAllocator` to allocate `ByteBuffer`.
publicletallocator:ByteBufferAllocator

/// ``LambdaTerminator`` to register shutdown operations.
publicletterminator:LambdaTerminator
}

Configuration

The library’s behavior can be fine tuned using environment variables based configuration. The library supported the following environment variables:

  • LOG_LEVEL:Define the logging level as defined bySwiftLog.Set to INFO by default.
  • MAX_REQUESTS:Max cycles the library should handle before exiting. Set to none by default.
  • STOP_SIGNAL:Signal to capture for termination. Set toTERMby default.
  • REQUEST_TIMEOUT:Max time to wait for responses to come back from the AWS Runtime engine. Set to none by default.

AWS Lambda Runtime Engine Integration

The library is designed to integrate with AWS Lambda Runtime Engine via theAWS Lambda Runtime APIwhich was introduced as part ofAWS Lambda Custom Runtimesin 2018. The latter is an HTTP server that exposes three main RESTful endpoint:

  • /runtime/invocation/next
  • /runtime/invocation/response
  • /runtime/invocation/error

A single Lambda execution workflow is made of the following steps:

  1. The library calls AWS Lambda Runtime Engine/nextendpoint to retrieve the next invocation request.
  2. The library parses the response HTTP headers and populate theContextobject.
  3. The library reads the/nextresponse body and attempt to decode it. Typically it decodes to user providedEventtype which extendsDecodable,but users may choose to write Lambda functions that receive the input asStringorByteBufferwhich require less, or no decoding.
  4. The library hands off theContextandEventevent to the user provided handler. In the case ofLambdaHandlerbased handler this is done on a dedicatedDispatchQueue,providing isolation between user's and the library's code.
  5. User provided handler processes the request asynchronously, invoking a callback or returning a future upon completion, which returns aResulttype with theOutputorErrorpopulated.
  6. In case of error, the library posts to AWS Lambda Runtime Engine/errorendpoint to provide the error details, which will show up on AWS Lambda logs.
  7. In case of success, the library will attempt to encode the response. Typically it encodes from user providedOutputtype which extendsEncodable,but users may choose to write Lambda functions that return aStringorByteBuffer,which require less, or no encoding. The library then posts the response to AWS Lambda Runtime Engine/responseendpoint to provide the response to the callee.

The library encapsulates the workflow via the internalLambdaRuntimeClientandLambdaRunnerstructs respectively.

Lifecycle Management

AWS Lambda Runtime Engine controls the Application lifecycle and in the happy case never terminates the application, only suspends its execution when no work is available.

As such, the library's main entry point is designed to run forever in a blocking fashion, performing the workflow described above in an endless loop.

That loop is broken if/when an internal error occurs, such as a failure to communicate with AWS Lambda Runtime Engine API, or under other unexpected conditions.

By default, the library also registers a Signal handler that trapsINTandTERM,which are typical Signals used in modern deployment platforms to communicate shutdown request.

Integration with AWS Platform Events

AWS Lambda functions can be invoked directly from the AWS Lambda console UI, AWS Lambda API, AWS SDKs, AWS CLI, and AWS toolkits. More commonly, they are invoked as a reaction to an events coming from the AWS platform. To make it easier to integrate with AWS platform events,Swift AWS Lambda Runtime Eventslibrary is available, designed to work together with this runtime library.Swift AWS Lambda Runtime Eventsincludes anAWSLambdaEventstarget which provides abstractions for many commonly used events.

Performance

Lambda functions performance is usually measured across two axes:

  • Cold start times:The time it takes for a Lambda function to startup, ask for an invocation and process the first invocation.

  • Warm invocation times:The time it takes for a Lambda function to process an invocation after the Lambda has been invoked at least once.

Larger packages size (Zip file uploaded to AWS Lambda) negatively impact the cold start time, since AWS needs to download and unpack the package before starting the process.

Swift provides great Unicode support viaICU.Therefore, Swift-based Lambda functions include the ICU libraries which tend to be large. This impacts the download time mentioned above and an area for further optimization. Some of the alternatives worth exploring are using the system ICU that comes with Amazon Linux (albeit older than the one Swift ships with) or working to remove the ICU dependency altogether. We welcome ideas and contributions to this end.

Security

Please seeSECURITY.mdfor details on the security process.

Project status

This is a community-driven open-source project actively seeking contributions. There are several areas which need additional attention, including but not limited to:

  • Further performance tuning
  • Additional documentation and best practices
  • Additional examples

Version 0.x (previous version) documentation


Getting started

If you have never used AWS Lambda or Docker before, check out thisgetting started guidewhich helps you with every step from zero to a running Lambda.

First, create a SwiftPM project and pull Swift AWS Lambda Runtime as dependency into your project

// swift-tools-version:5.6

import PackageDescription

letpackage=Package(
name:"my-lambda",
products:[
.executable(name:"MyLambda",targets:["MyLambda"]),
],
dependencies:[
.package(url:"https://github.com/swift-server/swift-aws-lambda-runtime.git",from:"0.1.0"),
],
targets:[
.executableTarget(name:"MyLambda",dependencies:[
.product(name:"AWSLambdaRuntime",package:"swift-aws-lambda-runtime"),
]),
]
)

Next, create amain.swiftand implement your Lambda.

Using Closures

The simplest way to useAWSLambdaRuntimeis to pass in a closure, for example:

// Import the module
import AWSLambdaRuntime

// in this example we are receiving and responding with strings
Lambda.run{(context,name:String,callback:@escaping(Result<String,Error>)->Void)in
callback(.success("Hello,\(name)"))
}

More commonly, the event would be a JSON, which is modeled usingCodable,for example:

// Import the module
import AWSLambdaRuntime

// Request, uses Codable for transparent JSON encoding
privatestructRequest:Codable{
letname:String
}

// Response, uses Codable for transparent JSON encoding
privatestructResponse:Codable{
letmessage:String
}

// In this example we are receiving and responding with `Codable`.
Lambda.run{(context,request:Request,callback:@escaping(Result<Response,Error>)->Void)in
callback(.success(Response(message:"Hello,\(request.name)")))
}

Since most Lambda functions are triggered by events originating in the AWS platform likeSNS,SQSorAPIGateway,theSwift AWS Lambda Eventspackage includes anAWSLambdaEventsmodule that provides implementations for most common AWS event types further simplifying writing Lambda functions. For example, handling anSQSmessage:

First, add a dependency on the event packages:

// swift-tools-version:5.6

import PackageDescription

letpackage=Package(
name:"my-lambda",
products:[
.executable(name:"MyLambda",targets:["MyLambda"]),
],
dependencies:[
.package(url:"https://github.com/swift-server/swift-aws-lambda-runtime.git",from:"0.1.0"),
],
targets:[
.executableTarget(name:"MyLambda",dependencies:[
.product(name:"AWSLambdaRuntime",package:"swift-aws-lambda-runtime"),
.product(name:"AWSLambdaEvents",package:"swift-aws-lambda-runtime"),
]),
]
)
// Import the modules
import AWSLambdaRuntime
import AWSLambdaEvents

// In this example we are receiving an SQS Event, with no response (Void).
Lambda.run{(context,message:SQS.Event,callback:@escaping(Result<Void,Error>)->Void)in
...
callback(.success(Void()))
}

Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets hang, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API.

Using EventLoopLambdaHandler

Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime usesSwiftNIOas its underlying networking engine which means the APIs are based onSwiftNIOconcurrency primitives like theEventLoopandEventLoopFuture.For example:

// Import the modules
import AWSLambdaRuntime
import AWSLambdaEvents
import NIO

// Our Lambda handler, conforms to EventLoopLambdaHandler
structHandler:EventLoopLambdaHandler{
typealiasIn=SNS.Message// Request type
typealiasOut=Void// Response type

// In this example we are receiving an SNS Message, with no response (Void).
funchandle(context:Lambda.Context,event:In)->EventLoopFuture<Out>{
...
context.eventLoop.makeSucceededFuture(Void())
}
}

Lambda.run(Handler())

Beyond the small cognitive complexity of using theEventLoopFuturebased APIs, note these APIs should be used with extra care. AnEventLoopLambdaHandlerwill execute the user code on the sameEventLoop(thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning.

Deploying to AWS Lambda

To deploy Lambda functions to AWS Lambda, you need to compile the code for Amazon Linux which is the OS used on AWS Lambda microVMs, package it as a Zip file, and upload to AWS.

AWS offers several tools to interact and deploy Lambda functions to AWS Lambda includingSAMand theAWS CLI.TheExamples Directoryincludes complete sample build and deployment scripts that utilize these tools.

Note the examples mentioned above use dynamic linking, therefore bundle the required Swift libraries in the Zip package along side the executable. You may choose to link the Lambda function statically (using-static-stdlib) which could improve performance but requires additional linker flags.

To build the Lambda function for Amazon Linux, use the Docker image published by Swift.org onSwift toolchains and Docker images for Amazon Linux 2,as demonstrated in the examples.

Architecture

The library defines three protocols for the implementation of a Lambda Handler. From low-level to more convenient:

ByteBufferLambdaHandler

AnEventLoopFuturebased processing protocol for a Lambda that takes aByteBufferand returns aByteBuffer?asynchronously.

ByteBufferLambdaHandleris the lowest level protocol designed to power the higher levelEventLoopLambdaHandlerandLambdaHandlerbased APIs. Users are not expected to use this protocol, though some performance sensitive applications that operate at theByteBufferlevel or have special serialization needs may choose to do so.

publicprotocolByteBufferLambdaHandler{
/// The Lambda handling method
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
/// - parameters:
/// - context: Runtime `Context`.
/// - event: The event or request payload encoded as `ByteBuffer`.
///
/// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine.
/// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error`
funchandle(context:Lambda.Context,event:ByteBuffer)->EventLoopFuture<ByteBuffer?>
}

EventLoopLambdaHandler

EventLoopLambdaHandleris a strongly typed,EventLoopFuturebased asynchronous processing protocol for a Lambda that takes a user definedInand returns a user definedOut.

EventLoopLambdaHandlerextendsByteBufferLambdaHandler,providingByteBuffer->Indecoding andOut->ByteBuffer?encoding forCodableandString.

EventLoopLambdaHandlerexecutes the user provided Lambda on the sameEventLoopas the core runtime engine, making the processing fast but requires more care from the implementation to never block theEventLoop.It it designed for performance sensitive applications that useCodableorStringbased Lambda functions.

publicprotocolEventLoopLambdaHandler:ByteBufferLambdaHandler{
associatedtypeIn
associatedtypeOut

/// The Lambda handling method
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
/// - parameters:
/// - context: Runtime `Context`.
/// - event: Event of type `In` representing the event or request.
///
/// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine.
/// The `EventLoopFuture` should be completed with either a response of type `Out` or an `Error`
funchandle(context:Lambda.Context,event:In)->EventLoopFuture<Out>

/// Encode a response of type `Out` to `ByteBuffer`
/// Concrete Lambda handlers implement this method to provide coding functionality.
/// - parameters:
/// - allocator: A `ByteBufferAllocator` to help allocate the `ByteBuffer`.
/// - value: Response of type `Out`.
///
/// - Returns: A `ByteBuffer` with the encoded version of the `value`.
funcencode(allocator:ByteBufferAllocator,value:Out)throws->ByteBuffer?

/// Decode a`ByteBuffer` to a request or event of type `In`
/// Concrete Lambda handlers implement this method to provide coding functionality.
///
/// - parameters:
/// - buffer: The `ByteBuffer` to decode.
///
/// - Returns: A request or event of type `In`.
funcdecode(buffer:ByteBuffer)throws->In
}

LambdaHandler

LambdaHandleris a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user definedInand returns a user definedOut.

LambdaHandlerextendsByteBufferLambdaHandler,performingByteBuffer->Indecoding andOut->ByteBufferencoding forCodableandString.

LambdaHandleroffloads the user provided Lambda execution to aDispatchQueuemaking processing safer but slower.

publicprotocolLambdaHandler:EventLoopLambdaHandler{
/// Defines to which `DispatchQueue` the Lambda execution is offloaded to.
varoffloadQueue:DispatchQueue{get}

/// The Lambda handling method
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
/// - parameters:
/// - context: Runtime `Context`.
/// - event: Event of type `In` representing the event or request.
/// - callback: Completion handler to report the result of the Lambda back to the runtime engine.
/// The completion handler expects a `Result` with either a response of type `Out` or an `Error`
funchandle(context:Lambda.Context,event:In,callback:@escaping(Result<Out,Error>)->Void)
}

Closures

In addition to protocol-based Lambda, the library provides support for Closure-based ones, as demonstrated in the overview section above. Closure-based Lambdas are based on theLambdaHandlerprotocol which mean they are safer. For most use cases, Closure-based Lambda is a great fit and users are encouraged to use them.

The library includes implementations forCodableandStringbased Lambda. Since AWS Lambda is primarily JSON based, this covers the most common use cases.

publictypealiasCodableClosure<In:Decodable,Out:Encodable>=(Lambda.Context,In,@escaping(Result<Out,Error>)->Void)->Void
publictypealiasStringClosure=(Lambda.Context,String,@escaping(Result<String,Error>)->Void)->Void

This design allows for additional event types as well, and such Lambda implementation can extend one of the above protocols and provided their ownByteBuffer->Indecoding andOut->ByteBufferencoding.

Context

When calling the user provided Lambda function, the library provides aContextclass that provides metadata about the execution context, as well as utilities for logging and allocating buffers.

publicfinalclassContext{
/// The request ID, which identifies the request that triggered the function invocation.
publicletrequestID:String

/// The AWS X-Ray tracing header.
publiclettraceID:String

/// The ARN of the Lambda function, version, or alias that's specified in the invocation.
publicletinvokedFunctionARN:String

/// The timestamp that the function times out
publicletdeadline:DispatchWallTime

/// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider.
publicletcognitoIdentity:String?

/// For invocations from the AWS Mobile SDK, data about the client application and device.
publicletclientContext:String?

/// `Logger` to log with
///
/// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable.
publicletlogger:Logger

/// The `EventLoop` the Lambda is executed on. Use this to schedule work with.
/// This is useful when implementing the `EventLoopLambdaHandler` protocol.
///
/// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care.
/// Most importantly the `EventLoop` must never be blocked.
publicleteventLoop:EventLoop

/// `ByteBufferAllocator` to allocate `ByteBuffer`
/// This is useful when implementing `EventLoopLambdaHandler`
publicletallocator:ByteBufferAllocator
}