Using Soto on AWS Lambda
Last year Apple along with AWS released the Swift AWS Lambda runtime. This eased the building of AWS Lambda functions using Swift. Soto is the perfect match for this runtime as it provides access to all AWS resources for your Swift developed Lambda functions. Below are some points to take into account when developing your Soto enhanced Lambdas.
AWSClient lifecycle
When using Soto you need to manage the lifecycle of your AWSClient
explicitly as AWSClient.syncShutdown()
needs to be called before it can be deleted.
Initial attempt
Assuming you have Request
and Response
structs for your Lambda input and output an initial implementation of your Lambda may be as follows.
Lambda.run { (context, request: Request, callback: @escaping (Result<Response, Error>) -> Void) in
let awsClient = AWSClient(httpClientProvider: .createNew)
let service = Service(client: awsClient)
let result = try service.doSomething()
}
This will not work though. The AWSClient
goes out of scope straight away and complains about not being shut down correctly.
There are a couple of ways to get around this.
Global variable
If the AWSClient
is a global variable. The shutdown of the AWSClient
can be added to a defer
block which will be called on Lambda shutdown. You could implement as follows
let awsClient = AWSClient(httpClientProvider: .createNew)
defer { try? awsClient.syncShutdown() }
Lambda.run { (context, request: Request, callback: @escaping (Result<Response, Error>) -> Void) in
let service = Service(client: awsClient)
service.doSomething().whenComplete { result in
callback(result)
}
}
EventLoopLambdaHandler
You can also use an EventLoopLambdaHandler
to control the lifecycle of AWSClient
. This is the prefered method and gives you some advantage. Below is an example of this.
struct MyHandler: EventLoopLambdaHandler {
typealias In = Request
typealias Out = Response
let awsClient: AWSClient
init(context: Lambda.InitializationContext) {
self.awsClient = AWSClient(httpClientProvider: .createNewWithEventLoopGroup(context.eventLoop))
}
func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture<Void> {
let promise = context.eventLoop.makePromise(of: Void.self)
promise.completeWithTask {
try await awsClient.shutdown()
}
return promise.futureResult
}
func handle(context: Lambda.Context, event: In) -> EventLoopFuture<Out> {
let promise = context.eventLoop.makePromise(of: Out.self)
promise.completeWithTask {
let service = Service(client: awsClient)
return try await service.doSomething()
}
return promise.futureResult
}
}
The EventLoopLambdaHandler
has init
and shutdown
functions which can be used to manage the lifecycle of the AWSClient
. To invoke the handler object add the following code
Lambda.run { MyHandler(context: $0) }
The closure supplied to Lambda.run
is called on cold start of the Lambda and will create your EventLoopLambdaHandler
. Whenever the Lambda is invoked the handle
function is called.
Using this method allows you to use the EventLoop
the Lambda runtime is using instead of creating a new EventLoopGroup
.
Default variables
In the above examples there is no mention of region or credentials. This is because they are extracted out of the environment. A Lambda is always initialized with the environment variables AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
set to the credentials for the execution role. The default credential provider for AWSClient
will use these envionment variables if they exist. You could make this more explicit if so desired by creating your AWSClient
as follows.
let awsClient = AWSClient(credentialProvider: .environment, ...)
The AWS_DEFAULT_REGION
environment variable is set to the region the Lambda is running in. The Service
object will use this if no region is provided at initialization.