Hedge – Building a new cloud deployment tool with service abstraction

At the start of my Siili career as an apprentice, our group was introduced to a mysterious tool: Hedge. Our apprentice project was to build an internal tool using Hedge as a cloud platform abstraction library and a deployment tool. After our apprentice program had come to an end, I was offered an opportunity to continue the development of Hedge in an internal project. Because we had already used Hedge I had some experience as a framework user. The task I was offered was to implement basic Amazon Web Services(AWS) support into Hedge.

What is this Hedge you are talking about?

Hedge offers automated serverless function code generated from common ring compatible handler methods, libraries for various levels of abstractions, and a set of common commands to build handlers, create artifacts, and to deploy created artifacts. Hedge is an open source software and is available in our github.

“Hedge is a platform agnostic ClojureScript framework for deploying ring compatible handlers to various environments with focus on serverless deployments.”

Hedge developer

Back to my journey with Hedge development

The scope of platform agnostic in Hedge was uncertain for me when I started the development. First, I implemented a simple feature parity for AWS code creation and deployment with Serverless Framework. Later the definition of the platform agnostic, roadmap, and context of Hedge became more clear.

When I started working with Hedge I had little Clojure experience and almost no hands-on experience with AWS. For AWS deployment I had checked how other tools do deployment and had chats with my peers. These information sources gave good advice on how to handle deployment.

Why Clojure(Script)? ClojureScript in backend?!

Clojure(Script) is popular and it is in high demand in Finland and at Siili Solutions. Using ClojureScript gives access to ClojureScript and JavaScript libraries. The functional paradigm has steep learning curve, but after learning the basics it is easy to realize that immutable data fits well with serverless handlers. Multithread-safety adds more security if one of the supported clouds is re-using processes with multiple threads.

One of the Clojure build tools, Boot, is also extensively used with Hedge. Boot tasks are great for creating and chaining commands. It is fast and easy to develop a set of tasks which for example build input files, create all artifacts, and finally deploy artifacts to cloud. Then those tasks can be combined into one large task which does all with one command or uses small tasks to store artifacts into disk and later deploys artifacts to cloud.

Why platform agnostic framework?

Following code snippets reveals the first problems with current serverless platforms:

AWS function handler example

'use strict';
module.exports.hello =
(event, context, callback) =>; {
  console.log(’log msg!')
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless!',
      input: event,
    }),
   };
 callback(null, response);
};

Azure function handler example

'use strict';
module.exports.hello =
(context, req) => {
  context.log(’log msg!');
  context.res = {
    // status: 200, /* Defaults to 200 */
    body: {
      message: 'Go Serverless!',
      input: event,
    }
  };
  context.done();
};

Above code snippets are simple Hello World serverless function handlers but the problem is already visible.

  • Handler function signatures(line 3)
  • Logging(line 4)
  • Output(line 5) handling(line 12)
  • Exit condition signaling(line 12)

all are different.

The main goal of Hedge is platform agnosticism: once the code and configuration has been written for one cloud platform, it is re-deployable to another cloud provider by changing deployment command and a few configuration directives. This improves code re-usability and limits the risk of being locked with specific cloud provider.

To make the implementation easier, Hedge supports only a small common subset of provided cloud features. Limiting number of supported features might be a risk and might lower acceptance rate amongst developers. It is unknown if developers will use a framework which allows using only a small fraction of cloud features. The small number of features definitely narrows down the creativity of developers and might also limit usage of Hedge for some projects.

Our implementation

Hedge adds abstraction layers for function handler code, infrastructure and deployment. Following code snippets are examples of abstraction layers of current development version.

Unified Hedge handler example

(defn hello
[req]
  (info "log msg!")
  {:status 200
   :body "Go Clojure!"})

Above example shows how Hedge unifies function handler features:

  • Handler function signature(lines 1 & 2)
  • Logging (line 3)
  • Payload creation and emit(lines 4 & 5)
  • Function exit condition signalling(line 5)
  • Persistent storage and queues(Still in WIP list)

Infrastructure abstaction

{:api {
       "hello-json" {:handler handler.core/hello-json}
       "calc" {:handler handler.core/calc}
       "fail-hard" {:handler handler.core/fail-hard}}
 :timer {"timer" {:handler handler.core/hello
                  :cron "*/15 * * * *"}}}

Infrastructure abstraction is done as EDN configuration file. Hedge will create platform specific templates from EDN.

Deployment abstraction


$ boot help
...
 aws/deploy                  ** Build and deploy function app(s) **
 aws/deploy-from-directory   ** Deploy files from directory. **
 aws/deploy-to-directory     ** removed for readability **

 azure/deploy                  ** Build and deploy function app(s) **
 azure/deploy-from-directory   ** Deploy files from directory **
 azure/deploy-to-directory     ** removed for readability **
...
$ boot aws/deploy -n my-stack
Deploying to AWS
Stack is ready!
API endpoint base URL :
https://iftc4hog23.execute-api.eu-west-3.amazonaws.com/Prod

cloud-abstractionAbove snippet is example how to run Hedge. Help command lists all available command and simple aws/deploy or azure/deploy commands can be used to deploy project to selected cloud.

Deployment is abstracted with boot tasks. Identical set of command can be used for build, artifact creation and deployment.

Hedge HTTP function handlers resembles Ring handers which is well-known abstraction for HTTP in Clojure. Hedge reads user-supplied configuarion files and serververless function hander then creates cloud-specific wrapper code between a user supplied handler function and cloud’s native handler entry points. The wrapped code handles differences between cloud providers and in the future, there will be libraries for the rest of code abstraction.

Hedge is currently under heavy development and some of the listed features are still on roadmap.

Lessons learned

During this project I had to learn how create Cloudformation templates and stacks from template. Cloudformation templates for lambda functions with API Gateway endpoints first seemed overwhelming, but AWS SAM simplified template creation. AWS SAM is still a young technology and I did not find any big projects using it.

I had to find one important piece of information from StackOverflow. Cloudformations templates using SAM must be deployed using change sets. Luckily it was documented there, since otherwise I probably would have spent a lot of time debugging a feature which was not supposed to work.

As the documentation of SAM is still vague and mostly in GitHub only, I personally can recommend using SAM for serverless Cloudformation templates.

See also:
“Modern Application Development: Should you skip microservices and go directly to serverless?”

A Guidance Framework for Architecting Portable Cloud and Multicloud Applications

Multi-cloud, what are the options? – Low level abstraction libraries

Pros and Cons of a Multi Cloud approach

AWS Helsinki Meetup January 2018 slides

(<3 siili_ clojure) – hope you do too!

Why should I choose functional paradigm?

Choosing a functional paradigm language has been a hot topic for a while. Functional vs. Object Oriented Paradigm is a constant discussion topic within the developers.

Before thinking the benefits of FP, the working environment must be suitable for modern work

The key of success at the management point of view is not to micromanage. Let the team choose their working methods and tools. The project is always a compromise between time, features and given resources. It’s all about how the project management sees that triangle.

Every tool and way to work has their own place. Telling the developers HOW to do work instead of WHAT to do is always a problem. It kills creativeness and get the authority outside the dev team. Too often the author is someone who doesn’t know about the actual developer work.

Treat the professional developers with this Sledge Hammer principle: “Trust me, I know what I’m doing”. Developers should know exactly how the program works, because code is the most detailed specification of the solution. It’s all about communication between the people – and between the human and the computer.

When the environment is suitable for working, then we can discuss about the benefits of FP

So why should I choose FP instead of OOP/imperative? What benefits it would bring the table? Here’s some thinking I’ve done in the past years based on my experiences:

Functional programming makes especially list handling a way easier. There are effective functional languages like Clojure or Scala. Another option is to use functional libraries like Ramda or Lodash. I like flexible code, so I prefer using scalable languages, like ES6, Scala, Java8 or C#/LINQ. With OOP and FP combined I have more options available. It’s also a safe choice when there ain’t so much pure functional programmers in the team (yet). Scaling the Object Oriented code with functional flavour is understandable compromise with functional averages like me. Actually I’m on my way of using only functional languages.

Here’s some benefits I found on functional paradigm which could save some money

It’s stateless and then also immutable, so no side effects

Object Oriented Paradigm is all about sharing the lifecycle between the objects. OOP makes problem solving often too complex, and complexity costs. Mutable state makes side effects possible, so code is harder to test with a full coverage. Developing with FP forces to code small functions. Simplified syntax (especially with pure FP languages) gives more time to think the actual business logic. Focusing on one thing at the time is clever.

When the state is immutable, it’s way easier to scale up and still control the whole

When programming high capacity systems, it’s crucial to design the software architecture to be scalable for parallel processing. If the program isn’t handling any state inside the runtime, it’s way easier to scale by using micro services or FaaS container platforms like AWS or Azure. Then the runtime isn’t blocked to serve only one client for a long time – there’s more time to handle many short tasks instead of a one big blocker.
So no more if-else-if-else or switch-case spagetti, or recursive for loop hell. Only function chains like map, filter or reduce one-liners.
It might be hard to transform the brain in the right mode after coding years with OOP. Functional code readability might cause many WTFs in the beginning, but – trust me – It’s worth of it. Like Twelve-factor manifest says “Execute the app as one or more stateless processes“. Start by one and try to write more if it feels right and suitable for the context.
Learning functional programming makes you a better OOP developer. You don’t have to be an expert, but it’s beneficial to learn something new. It’s hard at the beginning, but with the supportive team it’s possible to achieve.
Functional languages are simple by design when you let it be so.

Here’s a recap of my points:

  • Let the developers to choose the right tools and processes for building the high quality solution
  • As a team member, encourage the colleagues at least try to code with FP
  • Functional code is simple and easy to maintain. when it works, it works (no side-effects like in OOP). Broken code is easier to recognise and refactoring is less risky operation
  • Data must persist outside the code (like in Redux architecture). It shortens or eliminates debugging marathons. Less risk for wasting the time hunting the bugs from the production environment

See also: