Serverless Go Actions

There’s now a better way to do this! See here: http://jamesthom.as/blog/2017/01/17/openwhisk-and-go/

OpenWhisk, the open-source serverless platform, provides the ability to invoke custom Docker containers as serverless functions.

Developers can create new Actions, referencing public images on Dockerhub. OpenWhisk manages creating and executing containers using these images per invocation request.

Using this feature, developers can write serverless functions using the Go language. Compiled Go language binaries are embedded within custom Docker images and pushed into the platform.

So, how do we start?

This blog post will explain how to get your Go language functions running as “serverless functions” on OpenWhisk. If you’re impatient to get to the code, this repository contains the examples for everything discussed below.

OpenWhisk helps developers create custom Actions using Docker through an SDK…

OpenWhisk Docker SDK

Using the wsk command-line utility, developers can install the SDK into the current directory.

$ wsk sdk install docker

The SDK provides the source for a custom Docker image, which executes a custom binary in response to invocation requests. The default SDK copies the executable file, located at the client/action, into the image during the build process. Users build the image locally before pushing this to Dockerhub.

$ docker build -t <dockerhub_user>/docker_action .
$ docker push <dockerhub_user>/docker_action

Using the command-line utility, users can then create a new Action referencing this public Docker image. When this Action is invoked, the platform will spin up a new container from this custom image.

$ wsk action create docker_action --docker <dockerhub_user>/docker_action
$ wsk action invoke --blocking --result docker_action 

OpenWhisk Docker Action

OpenWhisk SDK’s Docker image uses a Node.js application to handle the JSON invocation request from the platform and spawns a process to execute the binary. Invocation parameters are passed as a JSON string through a command-line argument to the binary. The executable must write the JSON response to stdout, the handler will return this to the platform.

Containers used to run OpenWhisk Actions must be expose a HTTP API on port 8080 with two paths, /init and /run. The platform sends HTTP POST requests to these paths to initialise the Action and schedule invocations.

The /init path is used to provide the Action source for languages which support runtime evaluation. User-provided Docker images do not need to implement this method, other than returning a non-error HTTP response.

The /run path is called by the platform for each invocation request. Parameters for the invocation are passed as the value property of the JSON request body. Any non-empty JSON response will be interpreted as the invocation result.

Go Actions using the Docker SDK

Using Go binaries with the Docker SDK requires the developer to cross-compile the source for the platform architecture and copy the binary to the client/action path.

export GOARCH=386
export GOOS=linux
go build -o action
mv action client/action

The Go code must parse the invocation parameters as a JSON string from the command-line argument. Data written to stdout will be parsed as JSON and returned as the Action response.

This sample Go source demonstrates using this method to implement a “reverse string” Action.

package main

import "os"
import "encoding/json"
import "log"

type Params struct {
  Payload string `json:"payload"`
}

type Result struct {
  Reversed string `json:"reversed"`
}

// extract invocation parameters, passed as JSON string argument on command-line.
func params() Params {
  var params Params
  source := os.Args[1]
  buf := []byte(source)
  if err := json.Unmarshal(buf, &params); err != nil {
    log.Fatal(err)
  }
  return params
}

// convert struct back to JSON for response
func return_result(result Result) {
  buf, err := json.Marshal(result)
  if err != nil {
    log.Fatal(err)
  }
  os.Stdout.Write(buf)
}

func main() {
  input := params()

  // reverse the string passed from invocation parameters
  chars := []rune(input.Payload)
  for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
    chars[i], chars[j] = chars[j], chars[i]
  }
  result := Result{
    Reversed: string(chars),
  }

  return_result(result)
}

Docker SDK Base Image

Building a base image from the OpenWhisk Docker SDK and publishing on Dockerhub simplifies the process of building a Docker-based Action. Developers can now use the following image (jamesthomas/openwhisk_docker_action), without having to install the SDK locally.

FROM jamesthomas/openwhisk_docker_action
COPY action /blackbox/action

This base image includes the Node.js handler to manage the platform HTTP requests. An executable file at /blackbox/action will be called for each invocation. JSON parameters and responses are still passed using command-line arguments and stdout.

Custom Go Handler

Using the Docker SDK for OpenWhisk relies on a Node.js application to handle the platform HTTP requests, spawning a process to execute the user binary file.

Implementing the HTTP API, described above, in Go would allow us to remove the Node.js handler from the image. Compiling the Go Action source with the HTTP API handler into a single binary and using an Alpine Linux base image will dramatically reduce the image size.

This should improve execution performance, by removing the Node.js VM process, and cold start-up time, through having a smaller Docker image.

Using this Go package, jthomas/ow, users can automate the process of creating Go-based Actions.

go get jthomas/ow

The package provides a method for registering Action callbacks and implements the HTTP endpoints for handling platform requests.

Invocation parameters are passed using a function parameter, rather than a raw JSON string. Returned interface values will be automatically serialised to JSON as the Action response.

openwhisk.RegisterAction(func(value json.RawMessage) (interface{}, error) {
   ...  
}

Re-writing the “reverse string” Action above to use this package is shown here.

package main

import (
    "encoding/json"
    "github.com/jthomas/ow"
)

type Params struct {
    Payload string `json:"payload"`
}

type Result struct {
    Reversed string `json:"reversed"`
}

func reverse_string(to_reverse string) string {
    chars := []rune(to_reverse)
    for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
        chars[i], chars[j] = chars[j], chars[i]
    }
    return string(chars)
}

func main() {
    ow.RegisterAction(func(value json.RawMessage) (interface{}, error) {
        var params Params
        err := json.Unmarshal(value, &params)
        if err != nil {
            return nil, err
        }
        return Result{Reversed: reverse_string(params.Payload)}, nil
    })
}

Cross-compiling the Action source, bundling this package, creates a single lightweight binary.

Embedding this file within a Docker image, using a minimal base image, creates a tiny image (<10MB). Containers from these images only execute a single process to handle both the HTTP requests and running the Action source.

FROM alpine:3.4
COPY action /action
EXPOSE 8080
CMD ["./action"]

Pushing the local image to Dockerhub and then using it to create an Action follows the same instructions above.

Conclusion

Running OpenWhisk Actions from user-provided Docker images allows developers to execute “serverless functions” using any language. This is a fantastic feature not currently supported by many of the other serverless providers.

OpenWhisk provides an SDK letting users build a local Docker image which executes their Action and handles the HTTP requests from the platform. Using this with Go-based Actions requires us to cross-compile our binary for the platform and handle passing JSON through command-line arguments and stdout.

Re-writing the HTTP handler natively in Go means the Docker image can contain and execute a single binary for both tasks. Using this Go package provides an interface for registering Actions and handles the HTTP requests automatically.

This project contains examples for the “reverse string” Action using both the Docker SDK and Go-based handler detailed above.