Ivan C Myrvold
Published 2021-01-15

Test Swift Lambda Locally

When trying to find a way to test a Lambda function coded in Swift locally, I came over Localstack. We can launch Localstack in a docker container, and it will act as a local AWS cloud environment, without you requiring to log into your AWS account.

I will show how we can make localstack work with our Swift Lambda function. I will use the SquareNumber lambda function that I posted in Deploy a Lambda Function with a Swift Image, but now we can test it locally 😀.

You will have to have AWS CLI installed to follow this post, with brew install awscli.

Set up localstack

Make a new folder and cd into it with the cli command: mkdir SquareNumberLocal && cd SquareNumberLocal

I find it convenient to use a docker-compose file to set up localstack:

Make a docker-compose.yml file and set the content to:

version: '3'
services:
  localstack:
    image: localstack/localstack:latest
    ports:
      - 4566-4583:4566-4583
    container_name: square-number-lambda-localstack
    environment:
      - SERVICES=serverless
      - LAMBDA_EXECUTOR=docker
      - DOCKER_HOST=unix:///var/run/docker.sock
      - DEBUG=1
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"

Start the localstack docker container with: docker-compose up -d

All the AWS API's is accessible from a single edge service, which is accessible on http://localhost:4566. Documentation for localstack is available on the localstack/localstack GitHub repository.

Make sure that we don't have any lambda functions in the localstack with the command:

aws --endpoint-url http://localhost:4566 lambda list-functions

This should return this:

{
    "Functions": []
}

We always add the --endpoint-url http://localhost:4566 parameter to aws cli commands, to direct the cli commands to localstack. We can install a thin wrapper to this with awslocal that can be installed from localstack/awscli-local GitHub repository, which makes it possible to replace aws --endpoint-url http://localhost:4566 with just awslocal. The aws commands will then simply be awslocal lambda list-functions. But I will be using the --endpoint-url in this post.

Set up Swift Lambda function

Start a new Swift project with: swift package init --type executable, and open it in Xcode with open Package.swift

Add swift-aws-lambda-runtime to dependencies in Package.swift:

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "SquareNumberLocal",
    dependencies: [
        .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .upToNextMajor(from: "0.3.0"))
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "SquareNumberLocal",
            dependencies: [
                .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime")
            ]),
        .testTarget(
            name: "SquareNumberLocalTests",
            dependencies: ["SquareNumberLocal"]),
    ]
)

Replace the content of main.swift with:

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
    print("input:", input)
    callback(.success(Output(result: input.number * input.number)))
}

Compile with amazonlinux2 container and zip

Compile the swift project with swift:5.3.2-amazonlinux2 container:

docker run \
--rm \
--volume "$(pwd)/:/src" \
--workdir "/src/" \
swift:5.3.2-amazonlinux2 \
swift build --product NotificationServer -c release -Xswiftc -static-stdlib

Fabian Fett gives a nice explanation in his excellent blog post (Step 5) of all the parameters of this command.

Make a new folder scripts, and add the script package.sh to it:

#!/bin/bash

set -eu

executable=$1

target=.build/lambda/$executable
rm -rf "$target"
mkdir -p "$target"
cp ".build/release/$executable" "$target/"
cd "$target"
ln -s "$executable" "bootstrap"
zip --symlinks lambda.zip *

Make it executable with chmod +x scripts/package.sh

Run the command scripts/package.sh SquareNumberLocal to make the zip file.

Test the lambda function locally with localstack

Now we can test the lambda function, but we first need to create the lambda function in localstack with the command:

aws --endpoint-url http://localhost:4566 lambda create-function \
--function-name SquareNumber \
--runtime provided.al2 \
--role fakerole \
--handler lambda.run \
--zip-file fileb://.build/lambda/SquareNumberLocal/lambda.zip

This will output a result similar to this:

{
    "FunctionName": "SquareNumber",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:SquareNumber",
    "Runtime": "provided.al2",
    "Role": "fakerole",
    "Handler": "lambda.run",
    "CodeSize": 20400688,
    "Description": "",
    "Timeout": 3,
    "LastModified": "2021-01-15T09:40:57.026+0000",
    "CodeSha256": "jQ3Ac8j5E3F0+XVn6RwuB8TsXHwQb/ITZemjrRc5W6Q=",
    "Version": "$LATEST",
    "VpcConfig": {},
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "577c4b3b-cfa6-405c-901b-91b0d864ab5b",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip"
}

Now we can invoke the lambda function, let's try to square the number 35:

aws --endpoint-url http://localhost:4566 lambda invoke \
--function-name SquareNumber \
--cli-binary-format raw-in-base64-out \
--payload '{"number": 35}' \
result.json

We will hopefully get this result:

{
    "StatusCode": 200,
    "LogResult": "",
    "ExecutedVersion": "$LATEST"
}

And if we inspect result.json, we should see the squared result:

{"result":1225}

Conclusion

We are able to use localstack container to test our Swift lambda function locally. The next step would be to integrate with AWS APIGateway. But it looks like we then need to use the Pro version of localstack, because I get only errors when trying to set up APIGateway V2:

aws --endpoint-url http://localhost:4566 apigatewayv2 create-api \
--name sqn \
--protocol-type HTTP

This results in 404 Not Found from localstack.

The source is available from my GitHub repository.

Tagged with: