Going Serverless with OpenFaaS and Golang - Building Optimized Templates
Martin Heinz
Posted on November 29, 2019
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 aslanguage
, requiredpackages
orfprocess
- 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 usinggo.mod
for Golang -
Dockerfile
- Finally, to package all these files into a function we haveDockerfile
, 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 viastdin
and reads a HTTP response viastdout
. -
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
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
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!
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 useof-watchdog
. Therefore we can improve it's performance by switching toof-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"]
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
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)
}
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.
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
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!
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. 😉
Posted on November 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.