Skip to content

Instantly share code, notes, and snippets.

@bok-
Created May 7, 2019 05:11
Show Gist options
  • Save bok-/cba90476b27d4aaba014888ef7ed39ee to your computer and use it in GitHub Desktop.
Save bok-/cba90476b27d4aaba014888ef7ed39ee to your computer and use it in GitHub Desktop.
Getting up and running with Swift on AWS Lambda

Serverless Swift

This guide is intended as a companion to the presentation given at Melbourne CocoaHeads on 8th May 2019.

Background

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.

Swift Support

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.

Swift 5 Lambda Layers

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 5.0.1

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

Known Issues

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.

Getting Started

Dependencies

Use Homebrew to install Docker and the AWS command line client:

brew cask install docker
brew install awscli

Project Setup

1. Create and init your swift executable package.

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" ]
)

3. Fetch dependencies

swift package update

4. Optionally generate an Xcode project:

swift package generate-xcodeproj
open ProjectName.xcodeproj

Define Request / Responses

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 🎉

Write Your Code

Now you get to what you came here for. Pure Swift. In the Cloud.

The available definitions for the handler functions are:

Synchronous

@escaping (Decodable, Context) throws -> Encodable

// Example
func doThing (input: Decodable, context: Context) -> Encodable {

  // magic here

  return ...
}

Asynchronous

@escaping (Decodable, Context, @escaping (Encodable) -> Void) -> Void

// Example
func doThing (input: Decodable, context: Context, completion: (Encodable) -> Void) {

  // magic here

  completion(...)
}

Registering Lambda

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()

Building Linux Binaries

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

Creating Your Lambda Function

On the Console

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:

On the Create screen

  • Author From Scratch
  • Runtime: Custom Runtime
  • Permissions: Default unless you're looking for something special

On the Function screen

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.

Uploading Code From the Command Line

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

Testing Your Lambda Function

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
#
# Makefile for Cards Deployment
#
PACKAGE = lambda.zip
PROFILE = unsignedapps
REGION = ap-southeast-2
LAMBDA = Cards
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)
deploy: package
aws --profile $(PROFILE) --region $(REGION) lambda update-function-code --function-name $(LAMBDA) --zip-file fileb://$(PACKAGE)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment