Create AWS Lambda functions using the AWS SDK for Swift - AWS SDK for Swift

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 repository on GitHub for more information about the runtime package.

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 bug that can cause problems when the entry point is in a file named main.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 the AWSLambdaRuntime module.

  • If you're using SwiftPM to manage your project dependencies, add the runtime package and its AWSLambdaRuntime module to your Package.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 the Decodable protocol. The Swift AWS Lambda Runtime Events library contains 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 an Encodable 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:

  1. The standard Apple Foundation API.

  2. AWSLambdaRuntimeis the Swift Lambda Runtime's main module.

  3. AWSS3 is the Amazon S3 module from the AWS SDK for Swift.

  4. The AWSClientRuntime.AWSServiceError protocol describes service errors returned by the SDK.

  5. 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:

  1. 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-sandbox

    This creates a ZIP file containing the function executable, placing the output in .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/target-name/executable-name.zip

  2. 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 by BUCKET_NAME.

  3. 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.json

    The file output.json contains the results of the invocation (or the error message injected by our code).

Additional information