Getting started with Swift on AWS Lambda

June 01, 2020

So three days ago this happened…

This tutorial shall help you to get started with our new shiny toy swift-aws-lambda-runtime. It’s a beginners’ tutorial focused primarily on the AWS console, since it is the easiest way to get up and running. Further the Lambda we create here is very simple for the purpose of concentrating on project setup and deployment.

You can find the complete code on GitHub.

If you have any questions or recommendations, please contact me on twitter or open an issue on GitHub so that you can get your question answered and this tutorial can be improved.

Note: The following instructions were recorded on June 1, 2020 and the GUI may have changed since then. Feel free to contact me on twitter if you see a different one.

Step 1: Prerequisites

For this tutorial you’ll need a couple of things.

Step 2: Create and Setup the Swift Package

Create a new Swift Package Manager project. For simplicity reasons we will focus solely on squaring numbers with our Lambda function.

  1. Start in your development folder and create a directory for your Lambda.
     $ mkdir SquareNumber
    
  2. Open the folder
     $ cd SquareNumber
    
  3. Create a new Swift project with the Swift Package Manager
     $ swift package init --type executable
    
  4. Open the folder in Finder
     $ open .
    
  5. Double click the Package.swift which will open Xcode. If you are an iOS developer, you might wonder what a Package.swift is. In simple terms your Package.swift defines the dependencies your code has and what products (libraries and/or executables) your code offers.
  6. Let’s change your Package.swift to this. We’ll go over what everything means in more detail in just a second.
     // swift-tools-version:5.2
     // The swift-tools-version declares the minimum version of Swift required to build this package.
        
     import PackageDescription
        
     let package = Package(
       name: "SquareNumber",
       platforms: [
           .macOS(.v10_13),
       ],
       products: [
         .executable(name: "SquareNumber", targets: ["SquareNumber"]),
       ],
       dependencies: [
         .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .upToNextMajor(from:"0.2.0")),
       ],
       targets: [
         .target(
           name: "SquareNumber",
           dependencies: [
             .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
           ]
         ),
       ]
     )
    

    name: "SquareNumber" gives your package a name. This only matters a lot if you want to build a library that is used by other Swift packages. platforms defines on which Apple platforms the code can be executed. Since Lambdas are supposed to be run on Linux servers with Amazon Linux, it is reasonable to make them run only on macOS for debugging. Running Lambdas on iOS, macOS or even watchOS doesn’t lead anywhere meaningful.

    In the dependencies section you can see what external libraries your code depends on. To run code within AWS Lambda you’ll need a runtime that handles the communication with the Lambda Runtime Interface. This is what the AWSLambdaRuntime is for. You import it by specifing its GitHub url.

    In the targets section you specify your own targets. They are pretty comparable to targets you specify within an Xcode project (that’s probably why they share the name 😎). In our example we only want to create an executable that is called SquareNumber. An executable must have a main.swift file that is the starting point of execution. To advertise our executable target as a product of our package, we add it to the products section.

    ⚠️ NOTE: executable targets cannot be used in a testTarget! Testing is a little more complicated, which is why I’ll explain it in another tutorial.

    If this is your first time SwiftPM experience, I would encourage you to learn more about it.

Step 3: Develop your Lambda

Next open your main.swift and create your function. As mentioned earlier, in this example we just want to square numbers although your function can do whatever you want.

import AWSLambdaRuntime

struct Input: Codable {
  let number: Double
}

struct Output: Codable {
  let result: Double
}

Lambda.run { (context, input: Input, callback: @escaping (Result<Output, Error>) -> Void) in
  callback(.success(Output(result: input.number * input.number)))
}

First we define an Input and Output struct, that both conform to Codable. This way we ensure that our Lambda accepts json input and creates json output. The Lambda.run { } function will start the Lambda Runtime and execute your given callback for every invocation.

⚠️ NOTE: You must specifiy your input and output type explicitly, because the Lambda.run method is overloaded. If you see an Unable to infer closure type in the current context compile error, you’ve forgotten to specify your input type.

Step 4: Test your Lambda locally

Before we deploy our Lambda to AWS, we want to ensure that it works locally. In Xcode open the Edit Scheme Menu for your target.

SquareNumber Edit Scheme Menu

And add the environment variable LOCAL_LAMBDA_SERVER_ENABLED=true to your Run settings.

SquareNumber Edit Scheme Menu

Now you can compile and run your Lambda with Xcode. You should see an output like this in your console:

2020-05-31T13:01:14+0200 info: LocalLambdaServer started and listening on 127.0.0.1:7000, receiving payloads on /invoke
2020-05-31T13:01:14+0200 info: lambda lifecycle starting with Configuration
  General(logLevel: info))
  Lifecycle(id: 73023677192036, maxTimes: 0, stopSignal: TERM)
  RuntimeEngine(ip: 127.0.0.1, port: 7000, keepAlive: true, requestTimeout: nil

You can now invoke your Lambda from your terminal with curl:

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"number": 3}' \
  http://localhost:7000/invoke

If you get the response,

{"result": 9}

… it looks as if everything worked fine.

Minions celebrating!

Step 5: Build your Code for the AWS Lambda Environment

First let’s remove the try Lambda.withLocalServer { } part or let’s put it into an #if DEBUG condition.

Your Lambda will be executed on the Amazon Linux operating system. This is why we need to compile our code for this environment. To achieve this, we use Docker.

  1. First we need to create a Dockerimage with development dependencies installed. We use the already available Amazon Linux 2 nightly image swiftlang/swift:nightly-amazonlinux2 for this. So add a file to your package at the root level called Dockerfile with the following content:

     FROM swiftlang/swift:nightly-amazonlinux2
      
     RUN yum -y install \
         git \
         libuuid-devel \
         libicu-devel \
         libedit-devel \
         libxml2-devel \
         sqlite-devel \
         python-devel \
         ncurses-devel \
         curl-devel \
         openssl-devel \
         tzdata \
         libtool \
         jq \
         tar \
         zip
    

    Note to Docker pros: Of course the list of installed packages is quite comprehensive and not needed for such a simple Lambda. But for the sake of giving people a good foundation to start with, this example contains some dependencies users might need at a later point in time.

    We have now defined what our Dockerimage, that we want to use for building, should look like. Next up: Actually creating the image. We name this image swift-lambda-builder. Of course you can use another name. Just remember to change the name in the following commands as well.

     $ docker build -t swift-lambda-builder .
    
  2. Next we can compile our Lambda for the Amazon Linux 2 environment. Run the following command in the root folder of your project.

     $ docker run \
         --rm \
         --volume "$(pwd)/:/src" \
         --workdir "/src/" \
         swift-lambda-builder \
         swift build --product SquareNumber -c release
    

    Now you should have a SquareNumber executable in your .build/release folder.

    If you are interested in the command line arguments, this shall be a short introduction:

    Next we need to package our lambda for deployment on AWS Lambda.

Step 6: Packing your Executable for Deployment

Swift executables need Swift runtime libraries to be executed. This is why in the last step, we will copy our executable and the Swift runtime libraries together, zip and upload them to AWS.

❓If you are interested in which Swift runtime libraries are needed and how dynamic linking works compared to other languages I highly recommend @Lukasaoz post on the Swift forums.

I recommend you use a script for this task. Create a folder scripts in the root of your project and create a file package.sh within. Copy the following content into the file:

#!/bin/bash

set -eu

executable=$1

target=.build/lambda/$executable
rm -rf "$target"
mkdir -p "$target"
cp ".build/release/$executable" "$target/"
cp -Pv \
  /usr/lib/swift/linux/libBlocksRuntime.so \
  /usr/lib/swift/linux/libFoundation*.so \
  /usr/lib/swift/linux/libdispatch.so \
  /usr/lib/swift/linux/libicu* \
  /usr/lib/swift/linux/libswiftCore.so \
  /usr/lib/swift/linux/libswiftDispatch.so \
  /usr/lib/swift/linux/libswiftGlibc.so \
  "$target"
cd "$target"
ln -s "$executable" "bootstrap"
zip --symlinks lambda.zip *

This script is an adaptation of the package script provided by the swift-aws-lambda-runtime project. The difference here is that we only copy the libraries actually needed for running Swift code. If your Swift Lambda doesn’t import Foundation, FoundationNetworking or FoundationXML you don’t need to copy those either.

So what does the script do?

  1. First we take the first argument and assign it to executable. In our case this will be SquareNumber.
  2. Next an empty folder is created at .build/lambda/SquareNumber.
  3. We copy the executable SquareNumber into our newly created folder.
  4. We copy all needed Swift runtime libraries into this folder as well.
  5. We create a symlink bootstrap that links our executable. This is needed since the Lambda execution looks for an executable called bootstrap on startup. With the symlink we ensure our executable is found.
  6. We zip the folder and name the result lambda.zip

The packaging has to be run with our Dockerimage as well, since we only have access to the Swift Lambda Runtime libraries within the Docker container. Run the following command in your terminal.

$ docker run \
    --rm \
    --volume "$(pwd)/:/src" \
    --workdir "/src/" \
    swift-lambda-builder \
    scripts/package.sh SquareNumber

Since the .build folder is a hidden folder you won’t be able to see the result immediately. In Finder use the keyboard shortcut Cmd + Shift + . to show hidden files. You can now navigate to .build/lambda/SquareNumber/lambda.zip. If everything went well, your lambda.zip should be around 22MB.

Step 7: Create your Lambda on AWS

Open your AWS Console and navigate to Lambda. Select “Functions” in the side navigation and click on “Create function” in the upper right corner. Make sure “Author from Scratch” is selected and give your function a name. I’ll choose “SquareNumber” and select the runtime “Provide your own bootstrap”.

Create your function

Your function has been created. Next we need to upload the lambda.zip.

You should see the section “Function Code” in the lower part of the screen. Select “Upload a zip file” in the “Code entry type”. Click on “Upload” and select your lambda.zip. In the “Handler” field you can fill in whatever you want (at least one character), since this field is not used by our runtime‌. Next click “Save”.

Upload your lambda code

Step 8: Invoke your Lambda on AWS

The only thing left is to invoke your lambda. Select “Test” (in the upper right corner) and change your test payload to whatever json you want to supply to your function. Since I want numbers squared mine is as follows:

{
  "number": 3
}

Since AWS wants to reuse your event for tests over and over again, you need to give your test event a name. Mine is “Number3”. Click “Save” and you can click “Test” again, and this time your lambda will be executed. If everything went well, you should see a screen like this:

The lambda invocation is a success!

Closing Notes

In this tutorial you’ve learned what is necessary to create and deploy a Lambda on AWS. If you are an iOS developer and not familiar with a number of tools used here, you shouldn’t worry too much, we all need to start somewhere. If you have open questions please reach out.

You can support Swift on Lambda best by just using it, Amazon is watching the usage of the Swift runtime:

If you want to learn more about Swift on Lambda, let me know on twitter, this might enourage me to write more. I’m @fabianfett. Topics for future posts could be:

Let me know what your preferences are.