Openwhisk and Go

In an earlier blog post, I explained how to use Go language binaries on OpenWhisk using Docker-based Actions. It relied on building Docker images for each serverless function and hosting them on Docker Hub.

Recent updates to Docker-based Actions have made this process much simpler. Developers don’t need to build and expose public images anymore.

Let’s re-visit the example from the previous post and see how to get it running using this new approach…

Have you seen this post explaining how Docker-based Actions work? This post assumes you have already read that first.

Go Language Actions

Go’s build system combines application source code and dependencies into a single execution binary. Bundling this binary into a zip file allows us to overwrite the runtime stub prior to invocation.

Runtime binaries will be executed by the Python-based invoker for each invocation. Request parameters will be passed as a JSON string using the first command-line argument. The invoker expects the Action result to be written to standard output as a JSON string.

Action Source Code

Here’s a simple Go function that returns a greeting string from an input parameter. It parses the JSON string provided on the command-line to look for a name parameter. If this isn’t present, it defaults to Stranger. It returns a JSON object with the greeting string (msg) by writing to the console.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "encoding/json"
import "fmt"
import "os"

func main() {
	// native actions receive one argument, the JSON object as a string
	arg := os.Args[1]

	// unmarshal the string to a JSON object
	var obj map[string]interface{}
	json.Unmarshal([]byte(arg), &obj)
	name, ok := obj["name"].(string)
	if !ok {
		name = "Stranger"
	}
	msg := map[string]string{"msg": ("Hello, " + name + "!")}
	res, _ := json.Marshal(msg)
	fmt.Println(string(res))
}

Building this locally allows us to test it works.

1
2
$ go run test.go '{"name": "James"}'
{"msg":"Hello, James!"}

Before we can deploy this binary to OpenWhisk, it must be compiled for the platform architecture.

Cross-Compiling Locally

Go 1.5 introduced much improved support for cross-compilation.

If you have the development environment installed locally, you can compile the binary for another platform by setting environment variables. The full list of supported architectures is available here.

OpenWhisk uses an Alpine Linux-based environment to execute Actions.

1
$ env GOOS=linux GOARCH=amd64 go build exec.go

Checking the file type demonstrates we have built a static binary for the Linux x86_64 platform.

1
2
$ file exec
exec: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Cross-Compiling Using Docker

If you don’t want to install the Go development toolchain, Docker can be used to start a container with the environment set up.

1
2
3
4
5
6
7
8
$ docker pull golang
$ docker run -it -v $(pwd):/go/src golang
root@0a2f1655eece:/go# cd src/
root@0a2f1655eece:/go/src# go build exec.go
root@0a2f1655eece:/go/src# ls 
exec  exec.go
$ file exec
exec: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Create & Deploy Archive

Add the binary to a zip file, ensuring the file is named exec in the archive.

Use the wsk command-line to create a new Docker Action using this archive.

1
2
3
4
$ zip action.zip exec
  adding: exec (deflated 66%)
$ wsk action create go_test action.zip --docker
ok: created action go_test

Invoking Action

Test the action from the command-line to verify it works.

1
2
3
4
5
6
7
8
$ wsk action invoke go_test --blocking --result
{
    "msg": "Hello, Stranger!"
} 
$ wsk action invoke go_test --blocking --result --param name James
{
    "msg": "Hello, James!"
}

Success 😎.