Building a Cloud Agnostic Serverless Web Application

Summary

Many people are moving to utilizing Function as a Service (FaaS) offerings to deploy their applications to the cloud. How do we take advantage of the benefits of FaaS while mitigating the risks associated with vendor lockin? What if we want to deploy across multiple cloud providers? In this post, I will be discussing how to isolate your vendor dependencies in a boundary layer when writing serverless applications.

Overview

“By creating a thin abstraction layer at the boundary of our applications, we can isolate the vendor specific implementations of FaaS.”

More and more companies are moving towards developing serverless applications cloud offerings like AWS Lambda and GCP Functions. These products allow us to build elastic applications that run in the cloud without the need to manage scaling servers. But each of these vendors have different mechanisms and interfaces for creating applications and the necessary networking to make them available on the internet. How are we to avoid having our choice of cloud vendor bleed on our application? What if we want to deploy to multiple cloud providers?

By creating a thin abstraction layer at the edge of our applications, we can isolate the vendor specific implementations of FaaS. This edge layer will serve as the interface between the network from the cloud provider and your boundary layer. Like in traditional ECB architecture, the boundary layer serves to isolate the input into your application and format the output.

Proof of Concept

The diagram below illustrates the concept as it has been applied in the proof of concept that follows.

In the proof of concept, we have several different deployment mechanisms. We can deploy to AWS Lambda, GCP Cloud Functions, and as a standalone process. The latter can be deployed via a Docker container or by running an executable. This also makes local development easier as it gives you the option of running your application locally without having to rely on vendor local development toolkits. The proof of concept is written in go. I felt like go would make it easier to see the necessary typings required to build this architecture.

Controller

The controller is the core of our application. It is where all the business logic is held. For that reason we would like to keep it clear and concise and not filled up with serverless vendor implementation details. Obviously, we would like to reuse this code and not duplicate it.

For this contrived example we have a simple function that responds with a hello message. It pulls an environment variable to demonstrate how we might configure the different environments. You could create a configuration file where you put cloud-specific application settings and pull that in with an environment variable.

Deployment

Now we will look at the various deployment mechanism and how they are built in the example. We will start with GCP as it is the most straightforward (at least using Go as the runtime).

GCP Functions

GCP Functions allow us to run code in GCP without creating servers. GCP Functions have a concept called triggers which we will use to make our application accessible on the internet. Specifically, we add an HTTP trigger, which gives us an endpoint to call our function. With Go on GCP the runtime needs a function that implements the http.HandlerFunc interface. If you have written go you will most likely have used this interface before. If not, it’s essentially an object that has a single method ServeHTTP which takes a request and a response and actions on them.

In our simple implementation, we are using the path of the request to decide which controller function to call. We have three basic actions:

  • / - redirects to /hello
  • /hello - say hello
  • /{default} - returns 404

This is done using a simple if statement below, however you could easily add a more complex router here. As you can see, we call the controller function that returns some data. The success function is responsible for marshalling the data into a usable format. For this implementation, it will write a string straight to the request body and marshall anything else to json.

The beauty of it is that this code is used no matter how the application is deployed. It is worth noting that this is assuming a ‘monofunction’ application, where a single function serves all the requests for the application.

Also notice there is a path prefix mechanism as GCP and AWS will add components to your url and you do not always have control over that. For that reason, we can initialize our handler from the provider edge to include whichever prefix is applicable to that provider. For GCP we use the /execute prefix.

var handler := boundary.NewRequestHandler("/execute")

Standalone Process / Docker

Next we will take a step up from the ServeHTTP method and look at how we call it from a standalone process, whether running in Docker or not. This scenario has been covered exhaustively across the internet so I will not spend much time on it.

As you can see in the above code, we simply create an instance of our Handler struct. This instance is configured with no prefix so the application is available at /. The handler contains the previously discussed ServeHTTP function that all the deployments will be calling.

AWS Lambda

Things get a little more complicated when you look at how AWS lambda maps the HTTP request to a lambda and back. Since we get an event containing request information and build a response. There is no active network connection available. So it becomes necessary to create a small shim to bridge between the lambda event and Go’s net/http request and response. There are a few of these out there in the wild but I prefer to just write my own for these scenarios unless the cloud vendor happens to publish an official one (which there may be, but I didn’t check).

In this snippet, you can see we must extract some information from the request before creating our handler. This is b/c AWS Lambda will add the stage variable to the prefix of the URL. Once we have that we create our request and response shims. The code is not pictured for these, but the request struct contains the path and the method and the response has 3 methods to write the body, set the status, and set headers. Admittedly, I took the “happy path” on these and implemented what I needed. But with just the little I implemented you can go a long way.

Getting the Code

The full source code for this example is available in github. Check out the readme to see how to get these deployed.

Conclusion

I encourage everyone building serverless applications to keep an eye out for runtime environment idioms bleeding into their code. Even if you are not planning on doing multicloud, creating a simple abstraction layer will keep your code clean and make it easy to run locally.

Avatar
Kerry Wilson
Principal Cloud Architect

Coming from a development background, Kerry’s focus is on application development, infrastructure and security automation, and applying agile software development practices to IT operations in the cloud.

Related