This guide is intended as a companion to the presentation given at Melbourne CocoaHeads on 8th May 2019.
At re:Invent 2018 AWS introduced Layers for AWS Lambda. This allows for custom runtimes or libraries to be shared and made available to Lambda functions that were previously limited to existing supported language runtimes, or through the use of shims to pre-built binaries.
You could write your lambda functions in any language that suited, but you had to ship static binaries that would run on Amazon Linux.
All of the credit for this should go to Toni Suiter. When Lambda Layers were announced, many people attempted to get Swift runtimes working, but Toni's implementation is the most complete. His aws-lambda-swift repository provides a way to create your own Swift runtimes and the AWSLambdaSwift framework.
So you don't have to package your own Swift 5.0 runtimes, I've published a series of these and will keep them up to date with new Swift versions; at least until Apple or AWS publish officially supported layers.
Swift version 5.0.1 (swift-5.0.1-RELEASE)
Target: x86_64-unknown-linux-gnu
Region | Amazon Resource Name (ARN) |
---|---|
ap-northeast-1 | arn:aws:lambda:ap-northeast-1:262192482026:layer:swift-501:1 |
ap-northeast-2 | arn:aws:lambda:ap-northeast-2:262192482026:layer:swift-501:1 |
ap-south-1 | arn:aws:lambda:ap-south-1:262192482026:layer:swift-501:1 |
ap-southeast-1 | arn:aws:lambda:ap-southeast-1:262192482026:layer:swift-501:1 |
ap-southeast-2 | arn:aws:lambda:ap-southeast-2:262192482026:layer:swift-501:1 |
ca-central-1 | arn:aws:lambda:ca-central-1:262192482026:layer:swift-501:1 |
eu-central-1 | arn:aws:lambda:eu-central-1:262192482026:layer:swift-501:1 |
eu-north-1 | arn:aws:lambda:eu-north-1:262192482026:layer:swift-501:1 |
eu-west-1 | arn:aws:lambda:eu-west-1:262192482026:layer:swift-501:1 |
eu-west-2 | arn:aws:lambda:eu-west-2:262192482026:layer:swift-501:1 |
eu-west-3 | arn:aws:lambda:eu-west-3:262192482026:layer:swift-501:1 |
sa-east-1 | arn:aws:lambda:sa-east-1:262192482026:layer:swift-501:1 |
us-east-1 | arn:aws:lambda:us-east-1:262192482026:layer:swift-501:1 |
us-east-2 | arn:aws:lambda:us-east-2:262192482026:layer:swift-501:1 |
us-west-1 | arn:aws:lambda:us-west-1:262192482026:layer:swift-501:1 |
us-west-2 | arn:aws:lambda:us-west-2:262192482026:layer:swift-501:1 |
Its important to know a few things before you embark on making your Cloud Swifty:
- Lambda functions run on AWS Linux. So many of your favourite
Foundation
data types may not have a Linux version yet. - You can't build Linux binaries with Xcode. So we use Docker.
- There is no AWS Swift framework yet. Their existing iOS SDK is written in Objective-C, and is unlikely to ever work on Linux.
- This is very much a work in progress.
- Error handling needs work.
Use Homebrew to install Docker and the AWS command line client:
brew cask install docker
brew install awscli
mkdir ProjectName && cd ProjectName
swift package init --type executable
2. Add a dependency on https://github.com/tonisuter/aws-lambda-swift in Package.swift
:
dependencies: [
.package(url: "https://github.com/tonisuter/aws-lambda-swift.git", .branch("master"))
],
And to your target:
target (
name: "ProjectName",
dependencies: [ "AWSLambdaSwift" ]
)
swift package update
swift package generate-xcodeproj
open ProjectName.xcodeproj
The AWSLambdaSwift package provides a very simple interface for registering Lambda functions. They take a Decodable
input, and provide Encodable
output, either as the returned value of a function or via a completionHandler.
So very simply:
import Foundation
struct Request: Decodable {
// Your properties here
}
struct Response: Encodable {
// Your properties here
}
Normal Codable applies, so go nuts 🎉
Now you get to what you came here for. Pure Swift. In the Cloud.
The available definitions for the handler functions are:
@escaping (Decodable, Context) throws -> Encodable
// Example
func doThing (input: Decodable, context: Context) -> Encodable {
// magic here
return ...
}
@escaping (Decodable, Context, @escaping (Encodable) -> Void) -> Void
// Example
func doThing (input: Decodable, context: Context, completion: (Encodable) -> Void) {
// magic here
completion(...)
}
Now that you've written your best code ever, we need to expose it to Lambda. AWS Lambda requires a handler be specified as part of the function definition. The handler is in the format <BinaryName>.<FunctionReference>. The BinaryName is typically the name of your Swift Target (unless you've overwritten settings or renamed the binary), and the FunctionName is something you provide to AWSLambdaSwift. It maintains a list of FunctionName => closure references.
import AWSLambdaSwift
let runtime = try Runtime()
runtime.registerLambda("FunctionName", doThing)
try runtime.start()
I recommend creating a Makefile
to handle your build needs:
#
# Makefile for ProjectName Deployment
#
PACKAGE = lambda.zip
PROFILE = <awscli profile name>
REGION = <region>
LAMBDA = ProjectName
SWIFT_DOCKER_IMAGE = swift:5.0
clean:
rm -f $(PACKAGE) || true
rm -rf .build || true
build:
docker run \
--rm \
--volume "$(shell pwd)/:/src" \
--workdir "/src" \
$(SWIFT_DOCKER_IMAGE) \
swift build
run:
docker run \
--rm \
--volume "$(shell pwd)/:/src" \
--workdir "/src" \
$(SWIFT_DOCKER_IMAGE) \
swift run
package: clean build
zip -r -j $(PACKAGE) ./.build/debug/$(LAMBDA)
(The full Makefile
is available as a separate file in this gist.)
This will automate the building and packaging of your function:
# Build Linux Binary
make build
# Run Linux Binary in Docker
make run
# Create Linux package in lambda.zip
make package
The final step in this whole piece is creating your Lambda function. This guide won't step you through the AWS Lambda console, but it will provide the key information you're looking for:
- Author From Scratch
- Runtime: Custom Runtime
- Permissions: Default unless you're looking for something special
With the function selected, there should be a Function Code section of the lamdba. There is only one value we need to change here:
- Handler: The BinaryName.FunctionName pair. See the "Registering Lambda" section above for more information.
Then select the "Layers" section and "Add a Layer".
- Provide a layer version ARN
- Enter the ARN from the layers table above
You can also zip up your binary and upload it to the console here, but I recommend uploading your code from the command line as per the next section.
Some additional targets in your Makefile
are recommended here, but you can always run the commands directly if you like:
deploy: package
aws --profile $(PROFILE) --region $(REGION) lambda update-function-code --function-name $(LAMBDA) --zip-file fileb://$(PACKAGE)
Then all you need to do to ship updates is:
make deploy
This is as easy as creating a Lambda Test Event in the console and running that, or running your function from the command line:
# Invoke the lambda, saving the response to /tmp/outputfile
aws lambda invoke --function-name $(LAMBDA) --payload '<json here>' /tmp/outputfile
# Output the response
cat /tmp/outputfile