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

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

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

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:

  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