In this blog post I’ll show you how to build functions using the new builder package for the Go SDK for OpenFaaS.
You can use the builder package to access the Function Builder API, which takes source code and builds container images without needing root or Docker. It’s designed for SaaS companies who want their users to be able to supply their own code to integrate into their product. It can also be used by a platform or managed services team that manages a multi-tenant OpenFaaS environment.
Since the Function Builder API has been available, we’ve mainly seen customers create a user-experience that resembles AWS Lambda, but for their own users. Users open a dashboard hosting a web IDE like Ace, CodeMirror or Monaco and supply code, it’s then built and deployed to OpenFaaS via a single click.
Customer spotlight
Patchworks is an iPaaS platform that helps businesses transform and connect data between retail systems such as e-commerce, ERP, Warehouse management and marketplaces. They have a pre-built library of Connectors to sync to/from associated applications, a pre-built library of OpenFaaS hosted scripts, and also allow customers to supply their own code using Custom Scripting which supports 6 programming languages via OpenFaaS with others able to be configured in if needed.
Whether the Custom Script is pre-supplied by the Patchworks team, or created by a customer, it is built and deployed using the same approach by making a HTTP call to the Function Builder API. The resulting container image is published to a registry, and then gets deployed as an OpenFaaS function. Whenever a script is needed by a user’s workflow, it is invoked over HTTP via the OpenFaaS gateway.
For an example of the user-experience Patchworks created for their users, see: Patchworks docs: Adding & testing custom scripts
E2E Networks Limited is an NSE-listed, infrastructure company headquartered in India. They turned to OpenFaaS to offer customers a Function As A Service (FaaS) offering alongside their more traditional VPC, Compute, Object Storage, and Kubernetes services, with a goal to help customers migrate from existing cloud vendors. Users can supply code using predefined OpenFaaS templates, invoke their functions, and monitor the results in one place. E2E Networks also supply a CLI and GitHub Actions integration.
See also: E2E networks: Function as a Service (FaaS)
Contents
A quick recap on function templates
Complete example with Go SDK and other considerations
How to create a tenant namespace and deploy the function
How to build multi-arch images
How to pass build-arguments
Conclusion
A quick recap on function templates
OpenFaaS is a serverless platform for building, deploying, monitoring, triggering, and scaling functions on Kubernetes. The logical unit is the Open Container Initiative (OCI) image which contains the Operating System (OS), function’s source code and all its dependencies. The start-up process is generally the (optional) watchdog which is responsible for starting the process that handles HTTP requests from the OpenFaaS API. Existing code or containers can also be supplied so long as they conform to the OpenFaaS workload contract, and expose HTTP traffic on port 8080.
The purpose of a template is to make creating a new function easy, without having to think about boilerplate code like Dockerfiles, HTTP frameworks, or dependency management. The official templates are fetched from Git repositories, and the built-in tooling in faas-cli can download them to scaffold new functions. Templates can be written for any language, so long as they target Linux, expose a HTTP server, and can be built into a container image using a Dockerfile.
Let’s take the golang-middleware template as an example.
This is how the CLI can be used to pull the template and scaffold a new function, notice how the user only needs to edit the handler.go file, and the go.mod file if they need to add dependencies.
$ faas-cli template store pull golang-middleware
$ faas-cli new --lang golang-middleware hello-world-go
$ ls
hello-world-go.yaml
hello-world-go/
hello-world-go/handler.go
hello-world-go/go.mod
If you look at the Git repository, or the local template folder, you’ll see all the additional files which are kept hidden from the user. This includes main.go that starts the process and HTTP server, implements a graceful shutdown, etc, and the multi-arch Dockerfile:
template/golang-middleware
template/golang-middleware/Dockerfile
template/golang-middleware/main.go
template/golang-middleware/.gitignore
template/golang-middleware/template.yml
template/golang-middleware/go.work
template/golang-middleware/function
template/golang-middleware/function/handler.go
template/golang-middleware/function/go.mod
template/golang-middleware/go.mod
In order to reduce the cognitive load, and repetition between functions, code for the user is kept in the function directory, and template authors write and maintain the files outside of that directory.
Every time a build is performed, the CLI will create a folder such as build/hello-world, then it writes the base content of the template into that directory and then copies the user’s code into the function directory. The build/hello-world directory is then used as the build context for the container build.
Templates can be shared with the community, or kept private within an organization. They can be versioned, and they can be updated with new features or bug-fixes.
Complete example with Go SDK
The Function Builder API takes a Docker build context as an input, and returns the URL to a published image, along with the logs from the container builder. It can run as non-root, and does not require Docker to be installed. The feature is available in OpenFaaS for Enterprises and is deployed via separate Helm chart.
You can read how the Function Builder API works in the OpenFaaS documentation, which also includes step-by-step examples using Bash, curl, and tar to show exactly how to prepare a bundle of source code and configuration for the builder.
The below code in the Go SDK abstracts some of the process away so that if you write code in Go, you can integrate the Function Builder API into your existing systems in a short period of time.
Briefly:
You need to load the payload secret from a file, or from a secret store, to sign the payload sent to the builder API.
You’ll need to create a instance of the builder client with the URL of the builder API and the HMAC secret specified, remember to strip any whitespace from the secret.
Then you will need to create a tar archive with the build context
Specify the build configuration, this will be written out as a file named com.openfaas.build.config in the tar archive.
Invoke the builder API with the tar archive, and the builder will build the image and push it to the registry.
Finally, handle the HTTP status code and the logs from the API.
The code below assumes that you already have the required templates in the working directory, and that you have a payload.txt file with the HMAC secret (use the command in the docs to obtain this).
For the example, we used the same functionName and handler, and the language is python3-http. The image name is ttl.sh/hello-world-python:30m and the platform is linux/amd64.
This is the equivalent to the following OpenFaaS stack.yaml file:
version: 1.0
provider:
name: openfaas
gateway: http://127.0.0.1:8080
functions:
hello-world-python:
lang: python3-http
handler: ./hello-world-python
image: ttl.sh/hello-world-python:30m
The code for main.go:
package main
import (
"bytes"
"log"
"net/http"
"net/url"
"os"
"github.com/openfaas/go-sdk/builder"
)
func main() {
functionName := "hello-world-python"
handler := "hello-world-python"
lang := "python3-http"
// Load the HMAC secret used for payload authentication with the builder API.
payloadSecret, err := os.ReadFile(os.ExpandEnv("$HOME/.openfaas/payload.txt"))
if err != nil {
log.Fatal(err)
}
payloadSecret = bytes.TrimSpace(payloadSecret)
// Initialize a new builder client.
builderURL, _ := url.Parse("http://127.0.0.1:8081")
b := builder.NewFunctionBuilder(builderURL, http.DefaultClient, builder.WithHmacAuth(string(payloadSecret)))
// Create a temporary file for the build tar.
tarFile, err := os.CreateTemp(os.TempDir(), "build-context-*.tar")
if err != nil {
log.Fatalf("failed to temporary file: %s", err)
}
tarFile.Close()
tarPath := tarFile.Name()
defer os.Remove(tarPath)
// Create the function build context using the provided function handler and language template.
buildContext, err := builder.CreateBuildContext(functionName, handler, lang, []string{})
if err != nil {
log.Fatalf("failed to create build context: %s", err)
}
// Configuration for the build.
// Set the image name plus optional build arguments and target platforms for multi-arch images.
buildConfig := builder.BuildConfig{
Image: "ttl.sh/hello-world-python:30m",
Platforms: []string{"linux/amd64"},
BuildArgs: map[string]string{},
}
// Prepare a tar archive that contains the build config and build context.
if err := builder.MakeTar(tarPath, buildContext, &buildConfig); err != nil {
log.Fatal(err)
}
// Invoke the function builder with the tar archive containing the build config and context
// to build and push the function image.
result, err := b.Build(tarPath)
if err != nil {
log.Fatal(err)
}
log.Println("Build from builder:")
for _, logMsg := range result.Log {
log.Printf("%s\n", logMsg)
}
log.Printf("\nStatus: %s, imageRef: %s", result.Status, result.Image)
}
When the build completes successfully, the result.Status will contain success and the result.Image will contain the URL to the published image. If the result is not equal to success, then there was an error during the build process, and the result.Log property should contain the logs from the build process.
A failed build could be