Create AWS Lambda functions using the AWS SDK for Swift
Overview
You can use the AWS SDK for Swift from within an AWS Lambda function by
using the Swift AWS Lambda Runtime package in your project. This
package is part of Apple's swift-server
repository of packages that can be used to develop server-side Swift
projects.
See the documentation for the swift-aws-lambda-runtime
Set up a project to use AWS Lambda
If you're starting a new project, create the project in Xcode or open a shell session and use the following command to use Swift Package Manager (SwiftPM) to manage your project:
$
swift package init --type executable --name LambdaExample
Remove the file Sources/main.swift
. The
source code file will have be
Sources/lambda.swift
to work around a known Swift
bugmain.swift
.
Add the swift-aws-lambda-runtime
package to the
project. There are two ways to accomplish this:
-
If you're using Xcode, choose the Add package dependencies... option in the File menu, then provide the package URL:
https://github.com/swift-server/swift-aws-lambda-runtime.git
. Choose theAWSLambdaRuntime
module. -
If you're using SwiftPM to manage your project dependencies, add the runtime package and its
AWSLambdaRuntime
module to yourPackage.swift
file to make the module available to your project:import PackageDescription let package = Package( name: "LambdaExample", platforms: [ .macOS(.v12) ], // The product is an executable named "LambdaExample", which is built // using the target "LambdaExample". products: [ .executable(name: "LambdaExample", targets: ["LambdaExample"]) ], // Add the dependencies: these are the packages that need to be fetched // before building the project. dependencies: [ .package( url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), .package(url: "https://github.com/awslabs/aws-sdk-swift.git", from: "1.0.0"), ], targets: [ // Add the executable target for the main program. These are the // specific modules this project uses within the packages listed under // "dependencies." .executableTarget( name: "LambdaExample", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "AWSS3", package: "aws-sdk-swift"), ] ) ] )
This example adds a dependency on the Amazon S3 module of the AWS SDK for Swift in addition to the Lambda runtime.
You may find it useful to build the project at this point. Doing so will pull the dependencies and may make them available for your editor or IDE to generate auto-completion or inline help:
$
swift build
Warning
As of this article's last revision, the Swift AWS Lambda Runtime package is in a preview state and may have flaws. It also may change significantly before release. Keep this in mind when making use of the package.
Create a Lambda function
To create a Lambda function in Swift, you generally need to define several components:
-
A
struct
that represents the data your Lambda function will receive from the client. It must implement theDecodable
protocol. The Swift AWS Lambda Runtime Events librarycontains a variety of struct definitions that represent common messages posted to a Lambda function by other AWS services. -
An
async
handler function that performs the Lambda function's work. -
An optional
init(context:)
function that configures logging and sets up other variables or services that must be created once per execution environment. -
An optional
struct
that represents the data returned by your Lambda function. This is usually anEncodable
struct
describing the contents of a JSON document returned to the client.
Imports
Create the file Sources/lambda.swift
and
begin by importing the needed modules and types:
import Foundation import AWSLambdaRuntime import AWSS3 import protocol AWSClientRuntime.AWSServiceError import enum Smithy.ByteStream
These imports are:
-
The standard Apple
Foundation
API. -
AWSLambdaRuntime
is the Swift Lambda Runtime's main module. -
AWSS3
is the Amazon S3 module from the AWS SDK for Swift. -
The
AWSClientRuntime.AWSServiceError
protocol describes service errors returned by the SDK. -
The
Smithy.ByteStream
enum is a type that represents a stream of data. The Smithy library is one of the SDK's core modules.
Define structs and enums
Next, define the structs that represent the incoming requests and the responses sent back by the Lambda function, along with the enum used to identify errors thrown by the handler function:
/// Represents the contents of the requests being received from the client. /// This structure must be `Decodable` to indicate that its initializer /// converts an external representation into this type. struct Request: Decodable, Sendable { /// The request body. let body: String } /// The contents of the response sent back to the client. This must be /// `Encodable`. struct Response: Encodable, Sendable { /// The ID of the request this response corresponds to. let req_id: String /// The body of the response message. let body: String } /// The errors that the Lambda function can return. enum S3ExampleLambdaErrors: Error { /// A required environment variable is missing. The missing variable is /// specified. case noEnvironmentVariable(String) /// The Amazon Simple Storage Service (S3) client couldn't be created. case noS3Client }
The Lambda handler struct
This example creates a Lambda function that writes a string to
an Amazon Simple Storage Service object. To do so, a struct of type
LambdaHandler
is needed:
@main struct S3ExampleLambda: LambdaHandler { let s3Client: S3Client? ... }
Initializer
The S3ExampleLambda
struct's initializer
init(context:)
prepares the handler by logging
the configured logging level (as specified by the
environment variable LOG_LEVEL
), fetches the
AWS Region configured by the environment variable
AWS_REGION
, and creates an instance of the
SDK for Swift S3Client
, which will be used for all
Amazon S3 requests:
/// Initialize the AWS Lambda runtime. /// /// ^ The logger is a standard Swift logger. You can control the verbosity /// by setting the `LOG_LEVEL` environment variable. init(context: LambdaInitializationContext) async throws { // Display the `LOG_LEVEL` configuration for this process. context.logger.info( "Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "info" )" ) // Initialize the Amazon S3 client. This single client is used for every // request. let currentRegion = ProcessInfo.processInfo.environment["AWS_REGION"] ?? "us-east-1" self.s3Client = try? S3Client(region: currentRegion) }
Function handler
To receive, process, and respond to incoming requests, the
Swift Lambda Runtime's LambdaHandler
protocol
requires you to implement the
handle(event:context:)
function:
/// The Lambda function's entry point. Called by the Lambda runtime. /// /// - Parameters: /// - event: The `Request` describing the request made by the /// client. /// - context: A `LambdaContext` describing the context in /// which the lambda function is running. /// /// - Returns: A `Response` object that will be encoded to JSON and sent /// to the client by the Lambda runtime. func handle(_ event: Request, context: LambdaContext) async throws -> Response { // Get the bucket name from the environment. guard let bucketName = ProcessInfo.processInfo.environment["BUCKET_NAME"] else { throw S3ExampleLambdaErrors.noEnvironmentVariable("BUCKET_NAME") } // Make sure the `S3Client` is valid. guard let s3Client else { throw S3ExampleLambdaErrors.noS3Client } // Call the `putObject` function to store the object on Amazon S3. var responseMessage: String do { let filename = try await putObject( client: s3Client, bucketName: bucketName, body: event.body) // Generate the response text. responseMessage = "The Lambda function has successfully stored your data in S3 with name \(filename)'" // Send the success notification to the logger. context.logger.info("Data successfully stored in S3.") } catch let error as AWSServiceError { // Generate the error message. responseMessage = "The Lambda function encountered an error and your data was not saved. Root cause: \(error.errorCode ?? "") - \(error.message ?? "")" // Send the error message to the logger. context.logger.error("Failed to upload data to Amazon S3.") } // Return the response message. The AWS Lambda runtime will send it to the // client. return Response( req_id: context.requestID, body: responseMessage) }
The name of the bucket to use is first fetched from the
environment variable BUCKET_NAME
. Then the
putObject(client:bucketName:body:)
function is
called to write the text into an Amazon S3 object, and the text
of the response is set depending on whether or not the
object is successfully written to storage. The response is
created with the response message and the request ID string
that was in the original Lambda request.
Helper function
This example uses a helper function,
putObject(client:bucketName:body:)
, to write
strings to Amazon S3. The string is stored in the specified
bucket by creating an object whose name is based on the
current number of seconds since the Unix epoch:
/// Write the specified text into a given Amazon S3 bucket. The object's /// name is based on the current time. /// /// - Parameters: /// - s3Client: The `S3Client` to use when sending the object to the /// bucket. /// - bucketName: The name of the Amazon S3 bucket to put the object /// into. /// - body: The string to write into the new object. /// /// - Returns: A string indicating the name of the file created in the AWS /// S3 bucket. private func putObject(client: S3Client, bucketName: String, body: String) async throws -> String { // Generate an almost certainly unique object name based on the current // timestamp. let objectName = "\(Int(Date().timeIntervalSince1970*1_000_000)).txt" // Create a Smithy `ByteStream` that represents the string to write into // the bucket. let inputStream = Smithy.ByteStream.data(body.data(using: .utf8)) // Store the text into an object in the Amazon S3 bucket. let putObjectRequest = PutObjectInput( body: inputStream, bucket: bucketName, key: objectName ) let _ = try await client.putObject(input: putObjectRequest) // Return the name of the file. return objectName }
Build and test locally
While you can test your Lambda function by adding it in the Lambda console, the Swift AWS Lambda Runtime provides an integrated Lambda server you can use for testing. This server accepts requests and dispatches them to your Lambda function.
To use the integrated web server for testing, define the
environment variable LOCAL_LAMBDA_SERVER_ENABLED
before running the program.
In this example, the program is built and run with the Region
set to eu-west-1
, the bucket name set to
amzn-s3-demo-bucket
, and the local Lambda server
enabled:
$
AWS_REGION=eu-west-1 \ BUCKET_NAME=amzn-s3-demo-bucket \ LOCAL_LAMBDA_SERVER_ENABLED=true \ swift run
After running this command, the Lambda function is available on
the local server. Test it by opening another terminal session
and using it to send a Lambda request to
http://127.0.0.1:7000/invoke
, or to port 7000 on
localhost
:
$
curl -X POST \ --data '{"body":"This is the message to store on Amazon S3."}' \ http://127.0.0.1:7000/invoke
Upon success, a JSON object similar to this is returned:
{ "req_id": "290935198005708", "body": "The Lambda function has successfully stored your data in S3 with name '1720098625801368.txt'" }
You can remove the created object from your bucket using this AWS CLI command:
$
aws s3 rm s3://amzn-s3-demo-bucket
/file-name
Package and upload the app
To use a Swift app as a Lambda function, compile it for an x86_64 or ARM Linux target depending on the build machine's architecture. This may involve cross-compiling, so you may need to resolve dependency issues, even if they don't happen when building for your build system.
The Swift Lambda Runtime includes an archive
command
as a plugin for the Swift compiler. This plugin lets you
cross-compile from macOS to Linux just using the standard
swift
command. The plugin uses a Docker container to
build the Linux executable, so you'll
need Docker installed
To build your app for use as a Lambda function:
-
Build the app using the SwiftPM
archive
plugin. This automatically selects the architecture based on that of your build machine (x86_64 or ARM).$
swift package archive --disable-sandboxThis creates a ZIP file containing the function executable, placing the output in
.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/
target-name
/executable-name
.zip -
Create a Lambda function using the method appropriate for your needs, such as:
Warning
Things to keep in mind when deploying the Lambda function:
-
Use the same architecture (x86_64 or ARM64) for your function and your binary.
-
Use the Amazon Linux 2 runtime.
-
Define any environment variables required by the function. In this example, the
BUCKET_NAME
variable needs to be set to the name of the bucket to write objects into. -
Give your function the needed permissions to access AWS resources. For this example, the function needs IAM permission to use
PutObject
on the bucket specified byBUCKET_NAME
.
-
-
Once you've created and deployed the Swift-based Lambda function, it should be ready to accept requests. You can invoke the function using the
Invoke
Lambda API.$
aws lambda invoke \ --region eu-west-1 \ --function-name LambdaExample \ --cli-binary-format raw-in-base64-out \ --payload '{"body":"test message"}' \ output.jsonThe file
output.json
contains the results of the invocation (or the error message injected by our code).