Soto icon

Soto

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 AWSClientis 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.