Going Serverless with OpenFaaS and Golang - Building Optimized Templates

martinheinz

Martin Heinz

Posted on November 29, 2019

Going Serverless with OpenFaaS and Golang -  Building Optimized Templates

In the last blog post about OpenFaaS function we explored what we can do with OpenFaaS - we created our first OpenFaaS function in Golang, we wrote unit test for it, set up CI/CD with Travis and automated common tasks with Taskfile. Now, it's time to go little deeper.

In this blog post, we will look into how we can build custom templates, optimize them, build functions from them and finally, we will also incorporate Go modules into our new template. So, let's do it!

TL;DR: Full source code for this post, including docs can be found here: https://github.com/MartinHeinz/openfaas-templates

Deeper Look at Templates

If we want to build custom templates, then we first need to understand how they work. Let's first look at what files the template consists of:

  • Template Manifest - the template.yml is a file that describes the template itself, so that OpenFaaS knows how to create and deploy functions from it. In this file you can find information such as language, required packages or fprocess
  • Entrypoint - Next, for the function to be able to work, we need some file (entrypoint) which either listens to HTTP requests or reads STDIO and forwards these request to function itself. What this entrypoint file is, depends on the language, so you can have e.g. main.go, index.py, index.js, etc.
  • Handler - Now, that we have an entrypoint, which passes requests to our function, we need to somehow handle it. For that we have handler.{go,js,py...} which does the actual work and returns response from the function
  • Dependencies - Most of the time, your function will use some packages. These can be specified in language specific way, for example using package.json for JavaScript or using go.mod for Golang
  • Dockerfile - Finally, to package all these files into a function we have Dockerfile, which both builds, tests and creates final runner image that can be deployed to OpenFaaS.

Now that we know what the function consists of, how does it actually work? There are two versions of OpenFaaS templates - classic (watchdog) and new beta (of-watchdog) templates. Both of them operate by creating tiny Golang webserver, which marshals a HTTP request accepted on the API Gateway, which is then forwarded to invoked function. That's that for similarities, now let's look at the differences:

  • watchdog: Classic templates operate by forking one process per request. Once your process is forked the watchdog passes in the HTTP request via stdin and reads a HTTP response via stdout.

  • of-watchdog: On the other hand, the new templates fork a process when watchdog starts, we then forward any request incoming to the watchdog to a HTTP port within the container.

Images above taken from OpenFaaS Architecture Docs: https://docs.openfaas.com/architecture/watchdog/

If you want to delve deeper into how the watchdog (both old and new) works, then go to here or here

Creating Template

First of all, templates have specific file/directory structure, which we need to follow, this is how it looks:

template
└── my-template
    ├── Dockerfile
    ├── function
    │   └── handler.go
    ├── main.go
    └── template.yml

Enter fullscreen mode Exit fullscreen mode

Using the structure above - the template directory is the place where all templates reside. In it, we have one template named my-template and inside it are the files necessary for it to work - all of those we already described in previous section. This is very minimal example and we will be adding more files to that as we go.

That's it for structure, now I think it's finally time to create a template, let's start with "classic watchdog style" template. We can create one by just copying one from official template store here. Considering that this is guide for Golang templates, I will take the basic go template (it's redundant to copy it here, you can see the most up-to-date version of the template here).

When we have it copied into template folder, we should check if it actually works, right? So let's build it:

~ $ cd template/go
template/go $ ls -la
...
-rw-rw-r-- 1 martin martin 1173 nov 23 09:21 Dockerfile  <- We need this to build it
...
template/go $ docker build -t martinheinz/go .
Sending build context to Docker daemon  7.168kB
...
Successfully built 586fd7a36c0c
Successfully tagged martinheinz/go:latest
Enter fullscreen mode Exit fullscreen mode

Looks good, now let's deploy the function, shall we?

~ $ docker push martinheinz/go:latest
~ $ faas-cli deploy --image=martinheinz/go:latest --name=go-test-func
~ $ echo "It's Working!" | faas-cli invoke go-test-func
Hello, Go. You said: It's Working!
Enter fullscreen mode Exit fullscreen mode

And just like that, we have created our own template (well, we really just borrowed it from official store) and deployed function made from it. The whole process however, feels quite clunky, as we had to use quite a few commands to run it as well as call docker commands directly instead of e.g. faas-cli build and faas-cli push. We will simplify this process with Taskfile and few scripts in the next blog post. For the time being though, let's optimize the template, that we have...

What Could We Improve?

The classic template from previous section is nice and all, but there are quite few issues with it:

  • No Go modules: This template doesn't use Golang module system introduced in 1.11, which means that impractical and outdated dependency management has to be used.
  • Non-optimal Dockerfile: Dockerfile for this template has quite a few unnecessary layers making it slow to build and little thicker then needed.
  • of-watchdog is faster: As mentioned above, it's a classic template, so it doesn't use of-watchdog. Therefore we can improve it's performance by switching to of-watchdog and using HTTP mode.

So, let's fix those!

Note: Here is a good time to mention, that OpenFaaS community already did a lot of work on these templates. Classic one is legacy template and you should be using middleware or http versions of it and possibly applying tweaks and changes presented below.

Some of those issues can be solved by just using openfaas-incubator/golang-http-template here, but we can push it a little further, so let's take the golang-http from here and build on top of that.

Here I will just show you the final Dockerfile I created using this as a base and I will walk you through all the changes and rationale behind them:

FROM openfaas/of-watchdog:0.7.2 as watchdog
FROM golang:1.13.1-alpine3.10 as build

ARG GO111MODULE="on"
ENV CGO_ENABLED=0

RUN apk --no-cache add git

COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
RUN chmod +x /usr/bin/fwatchdog

WORKDIR /go/src/handler
COPY . .

# Run go test, gofmt, go vet
RUN chmod +x test.sh \
    && ./test.sh \
    && go build --ldflags "-s -w" -a -installsuffix cgo -o handler .

# Final runner image
FROM alpine:3.10
# Add non root user and certs
RUN apk --no-cache add ca-certificates \
    && addgroup -S app && adduser -S -g app app \
    && mkdir -p /home/app \
    && chown app /home/app

WORKDIR /home/app
COPY --from=build /go/src/handler/handler /usr/bin/fwatchdog /go/src/handler/function/ ./
RUN chown -R app /home/app

USER app
ENV fprocess="./handler" mode="http" upstream_url="http://127.0.0.1:8082"
CMD ["./fwatchdog"]
Enter fullscreen mode Exit fullscreen mode

Note: You can view the Dockerfile in the repository here

We start with 2 FROM commands as we need both Golang image for building the function as well as watchdog binary. Next, we turn Go modules on and disable CGO, which would allow us to call C code which we don't need here. Following line installs git into the builder image as we need it to download Golang packages during build. On next 2 lines, we copy watchdog binary and make it executable. Now it's time to build our function binary - we first copy the source code to /go/src/handler, then we run 2 commands against it - test and build. For testing, we use test.sh script included in template. Let's see what it does:

# Collect test targets
SRC_DIRS="function"
TARGETS=$(for d in "$SRC_DIRS"; do echo ./$d/...; done)

# Run tests
echo "Running tests:"
go test -installsuffix "static" ${TARGETS} 2>&1
echo

# Collect all `.go` files and run `gofmt` against them. If some need formatting - print them.
echo -n "Checking gofmt: "
ERRS=$(find "$SRC_DIRS" -type f -name \*.go | xargs gofmt -l 2>&1 || true)
if [ -n "${ERRS}" ]; then
    echo "FAIL - the following files need to be gofmt'ed:"
    for e in ${ERRS}; do
        echo "    $e"
    done
    echo
    exit 1
fi
echo "PASS"
echo

# Run `go vet` against all targets. If problems are found - print them.
echo -n "Checking go vet: "
ERRS=$(go vet ${TARGETS} 2>&1 || true)
if [ -n "${ERRS}" ]; then
    echo "FAIL"
    echo "${ERRS}"
    echo
    exit 1
fi
echo "PASS"
echo
Enter fullscreen mode Exit fullscreen mode

Note: You can view the test.sh file in the repository here

First it collects test targets, which is our source code from function directory. Then it runs go test against these target files, if tests pass, then it's time to check formatting and look for suspicious constructs in code, that's what gofmt and go vet are for. At that point - after tests pass successfully - going back to Dockerfile - we build the binary called handler.

Final part of the Dockerfile is runner image. Here, commands that we run really start to matter. We want to reduce all calls to RUN, COPY, and ADD as those create extra layers and would bloat the final image. So, first we use single RUN command to add CA, add user called app under which the function will run and move to it's home directory. Next we COPY all the needed files from builder image to runner directory from which function will run in single layer, this includes Golang binary, watchdog binary and everything in function directory. Finally, we set our user, set environment variables and set watchdog binary as default startup command.

Now, that we have Dockerfile out of the way, we can have a look at the source code. For the sake of simplicity, I just use both main.go and handler.go from previously mentioned golang-http, as there is not much to change when it comes to template code. What is missing though, is some unit test template, so let's see what we can do about that...

Time For Testing

If you read any of my previous posts, you already know what's coming - unit testing. The template can include any code - which includes tests, so I think it's appropriate to add simple test template to have some starting point for testing your functions. So, here is example test:

package function

import (
    "github.com/openfaas-incubator/go-function-sdk"
    "github.com/stretchr/testify/assert"
    "net/http"
    "testing"
)

func TestHandleReturnsCorrectResponse(t *testing.T) {
    expected := handler.Response{Body: []byte("Hello world, input was: John"), StatusCode: http.StatusOK}
    response, err := Handle(handler.Request{Body: []byte("John"), Method: "GET"})

    assert.Nil(t, err)
    assert.Equal(t, response.StatusCode, expected.StatusCode)
    assert.Equal(t, response.Body, expected.Body)
}
Enter fullscreen mode Exit fullscreen mode

This test lives in handler_test.go file right next to handler.go. This test covers the basic checks that one would expect when doing HTTP request/response, that being - status code check and body of response validation. So, first we create expected value using handler.Response, next we do the request to our function represented by Handle function, passing in our request, and finally we check it's response against expected values.

Bringing It All Together

We finally have all the source code needed for our template, but we can't use it quite yet. We are missing template.yml which we need to build functions. Here it is:

language: golang-mod
fprocess: ./handler
welcome_message: |
  You created Golang function, that uses Go modules.
  It includes simple handler as well as example unit test.

Enter fullscreen mode Exit fullscreen mode

Not much to talk about here. We specify language, binary that will be ran as fprocess and a simple welcome message. We could also include build_options and packages here, but in this case it is not necessary.

Next thing we need to complete the template are the dependencies and considering that we are using Go modules, all we need to do is create go.mod file alongside main.go, that looks like this

module handler

go 1.12

replace handler/function => ./function
Enter fullscreen mode Exit fullscreen mode

And then run go mod tidy which will populate the file with all our dependencies (openfaas-incubator/go-function-sdk and stretchr/testify)

Only thing left to do now is to build it, push it and deploy it:

.../golang-mod $ docker build -t martinheinz/golang-mod .
...
Running tests:
ok      handler/function    0.002s  <- Out test passed!

Checking gofmt: PASS

Checking go vet: PASS
...
Successfully built 26847942c5c4
Successfully tagged martinheinz/golang-mod:latest
.../golang-mod $ docker push martinheinz/golang-mod:latest
.../golang-mod $ faas-cli deploy --image=martinheinz/golang-mod:latest --name=golang-mod-test
.../golang-mod $ echo "It's Working!" | faas-cli invoke golang-mod-test
Hello world, input was: It's Working!
Enter fullscreen mode Exit fullscreen mode

Conclusion

And just like that, we created custom template that we can use to build and deploy OpenFaaS functions. With that said, there are few things left to do... In the next blog post we will build personal template store, where we can put all our custom templates, then add automatic validation of templates and include it in CI/CD pipeline, and finally we will simplify all the tasks related to building and running templates with Taskfile like we did with functions in previous post. For a sneak peek at source code, see my repository here and feel free to leave feedback or just star the repository if you like these kinds of posts. 😉

💖 💪 🙅 🚩
martinheinz
Martin Heinz

Posted on November 29, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related