James Thomas

Notes on JavaScript

Serverless Swift With OpenWhisk

Swift is one of the fastest growing programming languages with developers.

Swift has reached a Top 15 ranking faster than any other language we have tracked.

Created for building mobile applications, the language is now popular with backend development.

But for Swift developers beginning to build backend applications, they now find themselves having to manage computing infrastructure to run their applications in the cloud.

Enter serverless cloud platforms… ☁️☁️☁️

These services allow developers to push code, rather than VMs, into the cloud. The platforms allow you to connect external event sources like API requests or message queues to functions in your code. As events occur, your code is instantiated and executed to process each request. Developers are only billed for the milliseconds needed to process each request.

Serverless platforms let you run applications in the cloud without worrying about infrastructure. 😎

Apache OpenWhisk is currently the only serverless platform to support Swift language functions.

Let’s have a look at how you can use Swift with OpenWhisk before diving into how the platform implements this feature to give us some tips and tricks for Swift on OpenWhisk…

Swift On OpenWhisk

Using the CLI

Create a Swift file with the following source code in.

1
2
3
4
5
6
7
func main(args: [String:Any]) -> [String:Any] {
    if let name = args["name"] as? String {
        return [ "greeting" : "Hello \(name)!" ]
    } else {
        return [ "greeting" : "Hello stranger!" ]
    }
}

Swift actions must consume and return a dictionary. The dictionary passed as the function argument will contain event parameters. Returned dictionary values must support serialisation to JSON.

Create and invoke a new OpenWhisk action using the command-line utility.

1
2
3
4
5
6
7
8
9
10
$ wsk action create swift action.swift
ok: created action swift
$ wsk action invoke swift --result
{
    "greeting": "Hello stranger!"
}
$ wsk action invoke swift --result --param name World
{
    "greeting": "Hello World!"
}

The result flag will only show the action output in the console rather than the full API response.

The source file must have a function called main. Each invocation executes this function. The function name to invoke can be overridden as shown below.

1
2
3
func foo(args: [String:Any]) -> [String:Any] {
    return [ "greeting" : "Hello foo!" ]
}
1
2
3
4
5
6
$ wsk action create foobar action.swift --main foo
ok: created action foobar
$ wsk action invoke foobar --result
{
    "greeting": "Hello foo!"
}

Choosing the runtime for the action can be set using the kind flag. If the source file has the .swift extension this will be automatically set to swift:default.

OpenWhisk uses Swift 3.0.2 that runs on the Linux environment. There are open issues to support Swift 3.1 and Swift 4.

Using the Serverless Framework

The Serverless Framework is a popular open-source framework for building serverless applications. It provides CLI tools and a workflow for managing serverless development.

Developers use a YAML file to define their application functions, events and resources. The framework handles deploying the application to their serverless provider.

Having started as a tool for AWS Lambda, the framework recently added multi-provider support. It now also works with Apache OpenWhisk, Azure Functions and Google Cloud Functions.

Let’s look at an example of using this framework to create a new OpenWhisk Swift application. Using a provider name and runtime, the framework can scaffold a new serverless application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ serverless create -t openwhisk-swift -p swift-action
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/home/me/swift-action"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.16.0
 -------'

Serverless: Successfully generated boilerplate for template: "openwhisk-swift"
$ tree swift-action/
swift-action/
├── README.md
├── package.json
├── ping.swift
└── serverless.yml

0 directories, 4 files

The openwhisk-swift directory contains the boilerplate application ready to deploy. It includes a sample action (ping.swift) and the configuration file (serverless.yml).

1
2
3
4
5
6
7
8
9
10
11
func main(args: [String:Any]) -> [String:Any] {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    let now = formatter.string(from: Date())

    if let name = args["name"] as? String {
      return [ "greeting" : "Hello \(name)! The time is \(now)" ]
    } else {
      return [ "greeting" : "Hello stranger! The time is \(now)" ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
service: swift-action

provider:
  name: openwhisk
  runtime: swift

functions:
  hello:
    handler: ping.main

plugins:
  - serverless-openwhisk

Install the provider plugin using npm install and type serverless deploy to deploy this application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ serverless deploy
Serverless: Packaging service...
Serverless: Compiling Functions...
Serverless: Compiling API Gateway definitions...
Serverless: Compiling Rules...
Serverless: Compiling Triggers & Feeds...
Serverless: Deploying Functions...
Serverless: Deployment successful!

Service Information
platform: openwhisk.ng.bluemix.net
namespace:    _
service:  swift-action

actions:
swift-action-dev-hello
...
$ serverless invoke -f hello
{
    "greeting": "Hello stranger! The time is 2017-06-23 10:52:02"
}

For more information on using the Serverless Framework with OpenWhisk, please see this documentation: https://serverless.com/framework/docs/providers/openwhisk/.

How It Works

Swift is a statically typed compiled language. Unlike JavaScript or Python, Swift source code must be compiled into a binary for execution.

Swift actions in OpenWhisk can be created from Swift source files, rather than binaries, meaning the platform must run this compilation step.

Swift on Docker

OpenWhisk uses Docker containers to manage the action runtime environments. This Dockerfile documents the build steps for generating the Swift runtime image used in OpenWhisk.

Images for each of the OpenWhisk runtime environments are available on Docker Hub. Creating containers from these images allows you to explore the Swift runtime environment.

1
2
$ docker pull openwhisk/swift3action
$ docker run -it --rm openwhisk/swift3action bash

For more information on the API exposed by runtime containers to initialise and invoke actions, please see this blog post.

Building Swift actions

Swift runtime environments has a template package available in the /swift3Action/spm-build directory.

All the Swift sources files provided by the user are written into that package’s main.swift file. The following source code is appended to main.swift to support execution within the OpenWhisk runtime. It parses the input parameters from the environment, invokes the registered function name and returns the computation response as a JSON string.

Dependencies for the following packages are included in the existing Package.swift file. These packages can be used from the action source code without further configuration.

1
2
3
4
5
6
7
8
9
10
import PackageDescription

let package = Package(
    name: "Action",
        dependencies: [
          .Package(url: "https://github.com/IBM-Swift/Kitura-net.git", "1.0.1"),
            .Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", "14.2.0"),
            .Package(url: "https://github.com/IBM-Swift/swift-watson-sdk.git", "0.4.1")
        ]
)

During initialisation, the Swift build process is executed to generate the action binary.

This artifact (/swift3Action/spm-build/.build/release/Action) will be executed for each invocation received by the platform.

Container re-use

Containers used for action runtimes are re-used with subsequent requests. This means any initialisation cost, e.g. compiling Swift source code, will only be incurred once per runtime container.

Runtime containers are evicted from the cache ten minutes after the last activation. Future invocations for that runtime will use a new container and have to run the initialisation step again.

Additionally, runtimes containers cannot process concurrent requests. If a request arrives before the previous one has finished processing, a new environment will need to be initialised.

Improving cold start time

Swift build times are not known for being fast.

Build time is included in the request processing time for each new runtime container provisioned.

In an attempt to reduce this delay, OpenWhisk runs the minimum build steps necessary to compile the source code, rather than a full release build.

During the Docker build for the Swift runtime image, the full release build is executed for the empty action package. This generates object files and other intermediary build outputs which are stored in the build cache.

Logs from the build process are parsed to retrieve the individual compilation and linking commands for the main.swift file. These commands are written into a new shell script (/swift3Action/spm-build/swiftbuildandlink.sh).

When a new Swift runtime container is initialised, the source code for the action is written into the main.swift file. Rather than running a full re-build, the runtime just executes the shell script containing the compilation and linking steps. This re-uses the cached build objects and reduces compilation time.

Modifying package dependencies

Swift packages uses a manifest file (Packages.swift) to list package dependencies. Dependencies are automatically downloaded and compiling during the package build process.

The Swift environment used by OpenWhisk uses the package manifest shown above. This includes dependencies for JSON and HTTP libraries.

Swift actions can be created from Swift source code or zip files. Zip files are expanded into the package directory (/swift3action/spm-build) before initialisation.

If the zip file contains a new package manifest, this will overwrite the default manifest in the environment.

1
2
3
4
5
6
7
8
9
10
11
import PackageDescription

let package = Package(
    name: "Action",
        dependencies: [
          .Package(url: "https://github.com/IBM-Swift/Kitura-net.git", "1.0.1"),
            .Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", "14.2.0"),
            .Package(url: "https://github.com/IBM-Swift/swift-watson-sdk.git", "0.4.1"),
          .Package(url: "https://github.com/IBM-Swift/swift-html-entities", majorVersion: 3, minor: 0),
        ]
)

Running a full build will download new package dependencies and make them available for use in our action.

OpenWhisk uses a shell script (swiftbuildandlink.sh) to manage the build process during initialisation. This defaults to only running the compiler and linker commands for the main.swift file, rather than a full release build.

Including a replacement swiftbuildandlink.sh file in the zip file will allow us to modify the build command used, e.g. swift build -v -c release.

1
2
3
4
#!/bin/bash
echo "Release build running..."
swift build -v -c release
echo "Release build finished."

Downloading additional packages will add a significant delay to initialising new runtime containers.

If this is an issue, let’s look at skipping the compile step entirely…

Compiling binaries locally

Swift actions execute a binary that is available at the following path: /swift3action/spm-build/.build/release/Action.

The runtime uses the existence of this binary to control running the build process. If the file does not exist, the build step is executed. It ensures that compilation is only ran once per runtime container.

This also means that developers can include a locally compiled Swift binary inside the action zip file. During initialisation, the existence of this file will stop the build process from running.

If you want to use lots of additional Swift packages, the compile time penalty won’t have to be incurred during action invocations. This will dramatically speed up invocation times for “cold” actions.

Binaries must be compatible with the platform environment they are being executed within. OpenWhisk uses Swift 3.0.2 on Linux.

OpenWhisk publishes the runtime environments as Docker images. Using containers from these images to compile our action binaries will ensure the binary is compatible.

These instructions show you how to compile your source code into a compatible platform binary.

1
2
3
4
5
6
7
8
9
10
11
12
13
# run an interactive Swift action container
docker run -it -v `pwd`:/ow openwhisk/swift3action bash
cd /ow
# now inside the docker shell
# copy the source code and prepare to build it
cat /swift3Action/epilogue.swift >> main.swift
echo '_run_main(mainFunction:main)' >> main.swift
# build and link (the expensive step)
swift build -v -c release
# create the zip archive
zip action.zip .build/release/Action
# exit the docker shell
exit

The action.zip file can then be deployed as a new action using the following command-line.

1
wsk action create static-swift action.zip --kind swift:3

Conclusion

Swift is one of the fastest growing programming languages with developers. People are increasingly using it to develop backend APIs and services. Being able to use Swift on serverless cloud platforms means developers can focus on writing code, rather than managing infrastructure.

Apache OpenWhisk, an open-source serverless platform, supports Swift as a first-class language. Developers can provide Swift source code and have the platform execute these functions in response to external events.

Because OpenWhisk is open-source, we can discover how the platform executes the code using the Swift runtime. Understanding this process allows us to modify the build step to use additional Swift packages within our actions. We can also improve performance by skipping the compilation stage entirely by providing a native binary.

Comments