There and Back Again: A FaaS Tale

Functions as a service (FaaS) have really become a staple in most development circles. They provide little infrastructure overhead in the development of microservices and are often very lightweight. Yet FaaS face their own sets of challenges when it comes to costs, response times, and observability.

I believe that architectures will evolve over time. In some cases you may never want to change your FaaS but there may be cases that you’ll want to move it to it’s own application. The problem is you may want to go back or you may not want to rewrite all of the business logic injected into that FaaS. So what do you do?

Clean Architecture

Clean/Hex Architecture talks about building projects so that the business logic is removed from frameworks and IO devices called “ports”. This allows the business logic to be portable without worrying about underlying technologies such as database or http endpoints.

In the above scenario we do not want to build a system that is tied to either a FaaS framework or an API framework. To build code that has it’s logic separated allows you to have portable code that can freely move between FaaS or an application.

Example

Thank you to cpliakas for the original AWS Lambda Code

We will be using AWS SAM CLI to run our lambda function and we will be writing our program in Go.

Please also take a look at the README on the source to make sure you have all of the dependencies set up.

Domain

As part of the Hex Architecture we will be separating our business logic in order to make it portable. This is a simple “Hello World” program so we will create a package called hello and it will create handlers that respond with different responses. It could be argued that handlers could be considered “ports” since they invoke a specific type of http request, however for this example I will argue that we are using it as a communication layer between the “port” and the “core business logic”.

package hello

// ContentType is the Content-Type header set in responses.
import (
	"encoding/json"
	"net/http"
)

const ContentType = "application/json; charset=utf8"

// Message contains a simple message response.
type Message struct {
	Message string `json:"message"`
}

// Messages used by http.HandlerFunc functions.
var (
	WelcomeMessage = Message{"Welcome to the example API!"}
	HelloMessage   = Message{"Hello, world!"}
	GoodbyeMessage = Message{"Goodbye, world!"}
)

// RootHandler is a http.HandlerFunc for the / path.
func RootHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(WelcomeMessage)
}

// HelloHandler is a http.HandlerFunc for the /hello path.
func HelloHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(HelloMessage)
}

// GoodbyeHandler is a http.HandlerFunc for the /goodbye path.
func GoodbyeHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(GoodbyeMessage)
}

// RegisterRoutes registers the API's routes.
func RegisterRoutes() {
	http.Handle("/", h(RootHandler))
	http.Handle("/hello", h(HelloHandler))
	http.Handle("/goodbye", h(GoodbyeHandler))
}

// h wraps a http.HandlerFunc and adds common headers.
func h(next http.HandlerFunc) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", ContentType)
		next.ServeHTTP(w, r)
	})
}

FaaS Port

Next we will create the logic for faas, this will house the necessary implementation for our FaaS (Lambda Function). The package will need to be main since this is the required package for it to build. This class will import our hello package and call the RegisterRoutes function to initialize the routes. In this code we are using the Apex package which is a drop in replacement for the net/http package specifically for AWS Lambda.

package main

import (
	"golang-serverless-and-server/hello"

	"github.com/apex/gateway"
)

func main() {
	hello.RegisterRoutes()
	if err := gateway.ListenAndServe(":3000", nil); err != nil {
		panic(err)
	}
}

Server Port

Next we will add logic for a server port to the main.go file with a flag to run, which will look very similar to the FaaS Port but instead uses the Go http package to serve the content registered by the RegisterRoutes method.

package main

import (
	"flag"
	"fmt"
	"golang-serverless-and-server/hello"
	"net/http"

	"github.com/apex/gateway"
)

func main() {
	server := flag.Bool("server", false, "run as server")
	flag.Parse()

	hello.RegisterRoutes()
	if *server {
		fmt.Println("Running as server on port 3000")
		if err := http.ListenAndServe(":3000", nil); err != nil {
			panic(err)
		}
	} else {
		fmt.Println("Running as lambda on port 3000")
		if err := gateway.ListenAndServe(":3000", nil); err != nil {
			panic(err)
		}
	}
}

Building and Running

In both examples give you the flexibility now between building the FaaS implementation or the Server implementation.

To build and run the FaaS:

GOOS=linux go build -o main main.go
sam local start-api

You will see the server start and be able to make requests on port :3000

To build and run the server app:

GOOS=linux go build -o main main.go
./main -server

You will see the server start and be able to make requests on port :3000

Conclusion

You can imagine how this pattern can be applied to other “ports” such as workers at the end of a queue or a GRPC call. Hex architectures allow for a lot of flexibility to allow developers to go between different implementations. This is just one example about thinking outside of the FaaS/Application realm to allow your code to be portable.

Edit 09/05/2018: Changed format of server to run with flags

Related