Creating 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
Setting 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: "2.0.0-beta.1"), .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.
Creating 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.
import Foundation import AWSLambdaRuntime @preconcurrency import AWSS3 import protocol AWSClientRuntime.AWSServiceError import enum Smithy.ByteStream /// 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) } let currentRegion = ProcessInfo.processInfo.environment["AWS_REGION"] ?? "us-east-1" let s3Client = try S3Client(region: currentRegion) /// Create a new object on Amazon S3 whose name is based on the current /// timestamp, containing the text specified. /// /// - Parameters: /// - body: The text to store in the new S3 object. /// - bucketName: The name of the Amazon S3 bucket to put the new object /// into. /// /// - Throws: Errors from `PutObject`. /// /// - Returns: The name of the new Amazon S3 object that contains the /// specified body text. func putObject(body: String, bucketName: 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. _ = try await s3Client.putObject( input: PutObjectInput( body: inputStream, bucket: bucketName, key: objectName ) ) // Return the name of the file return objectName } let runtime = LambdaRuntime { (event: Request, context: LambdaContext) async throws -> Response in var responseMessage: String // Get the name of the bucket to write the new object into from the // environment variable `BUCKET_NAME`. guard let bucketName = ProcessInfo.processInfo.environment["BUCKET_NAME"] else { context.logger.error("Set the environment variable BUCKET_NAME to the name of the S3 bucket to write files to.") throw S3ExampleLambdaErrors.noEnvironmentVariable("BUCKET_NAME") } do { let filename = try await putObject(body: event.body, bucketName: bucketName) // Generate the response text and update the log. responseMessage = "The Lambda function has successfully stored your data in S3 with name '\(filename)'" context.logger.info("Data successfully stored in S3.") } catch let error as AWSServiceError { // Generate the error message and update the log. responseMessage = "The Lambda function encountered an error and your data was not saved. Root cause: \(error.errorCode ?? "") - \(error.message ?? "")" context.logger.error("Failed to upload data to Amazon S3.") } return Response(req_id: context.requestID, body: responseMessage) } // Start up the runtime. try await runtime.run()
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
Packaging and uploading 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).