8 Mar 2019

Serverless API with Azure Functions

In this post I am going to work on a pretty simple use case. While executing a deployment pipeline FlexDeploy may produce some human tasks that should be either approved or rejected. For example, someone has to approve a deployment to the production environment. It can be done either in FlexDeploy UI or with some external communication channels. Today I am going to focus on the scenario when a FlexDeploy human task is approved/rejected with Slack:


There are a few requirements and considerations that I would like to take into account:

  • I don't want to teach FlexDeploy to communicate with Slack
  • I don't want to provide Slack with the details of FlexDeploy API
  • I don't want to expose FlexDeploy API to public 
  • I do want to be able to easily change Slack to something different or add other communication tools without touching FlexDeploy
Basically, I want to decouple FlexDeploy from the details of the external communication mechanism. For that reason I am going to introduce an extra layer, an API between FlexDeploy and Slack. It looks like serverless paradigm is a very attractive approach to implement this API. Today I am going to build it with Azure Functions, because ... why not? 

So, technically, a poc version of the solution looks like this:

Once a new human task comes up, FlexDeploy notifies the serverless API about that providing an internal task id and task description. There is a function SaveTask that saves the provided task details along with a generated token (just some uid) to Azure Table storage. This token has an expiration time meaning that it should be used before that time to approve/reject the task.

const azure = require('azure-storage');
const uuidv1 = require('uuid/v1');

module.exports = async function (context, taskid) {   
    var tableSvc = azure.createTableService('my_account', 'my_key');
    var entGen = azure.TableUtilities.entityGenerator;
    var token = uuidv1();
    var tokenEntity = {
        PartitionKey: entGen.String('tokens'),
        RowKey: entGen.String(token),
        TaskId: entGen.String(taskid),
        dueDate: entGen.DateTime(new Date(Date.now() + 24 * 60 * 60 * 1000))
      };
     
      tableSvc.insertEntity('tokens',tokenEntity, function (error, result, response) { });

    return token; 
};


Having the token saved, the PostToSlack function is invoked posting a message to a Slack channel. SaveTask and PostToSlack functions are orchestrated into a durable function NotifyOnTask which is actually being invoked by FlexDeploy:
const df = require("durable-functions");

module.exports = df.orchestrator(function*(context){   
    var task = context.df.getInput()
    var token = yield context.df.callActivity("SaveTask",  task.taskid)
    return yield context.df.callActivity("PostToSlack",  {"token": token, "description": task.description})
});

The message in Slack contains two buttons to approve and reject the task.


The buttons refer to webhooks pointing to the ActionOnToken durable function:
const df = require("durable-functions");

module.exports = df.orchestrator(function*(context){   
    var input = context.df.getInput()
    var taskId = yield context.df.callActivity("GetTaskId",  input.token)
    if (input.action == 'approve') {
        yield context.df.callActivity("ApproveTask",  taskId)
    } else if (input.action == 'reject') {
        yield context.df.callActivity("RejectTask",  taskId)
    }
});


ActionOnToken invokes GetTaskId function retrieving task id from the storage by the given token:
const azure = require('azure-storage');

module.exports = async function (context, token) {
    var tableSvc = azure.createTableService('my_account', 'my_key');

    function queryTaskID(token) {
        return new Promise(function (resolve, reject) {
            tableSvc.retrieveEntity('tokens', 'tokens', token, 
             function (error, result, response) {
                if (error) {
                    reject(error)
                } else {
                    resolve(result)
                }
            });
        });
    }

    var tokenEntity = await queryTaskID(token);
    if (tokenEntity) {
        var dueDate = tokenEntity.dueDate._
        if (dueDate > Date.now()) {
            return tokenEntity.TaskId._
        }
    }
};
Having done that it either approves or rejects the task by invoking either ApproveTask or RejectTask functions.  These functions in their turn make corresponding calls to FlexDeploy REST API.
const request = require('sync-request');
const fd_url = 'http://dkrlp01.flexagon:8000';

module.exports = async function (context, taskid) {   
    var taskid = taskid;
    var res = request('PUT',
              fd_url+'/flexdeploy/rest/v1/tasks/approval/approve/'+taskid,{        
                    });

};

I could start developing my serverless application directly in the cloud on Azure Portal, but I decided to implement everything and play with it locally and move to the cloud later. The fact that I can do that, develop and test my functions locally is actually very cool, not every serverless platform gives you that feature. The only thing I have configured in the cloud is Azure Table storage account with a table to store my tokens and task details. 

A convenient way to start working with Azure Functions locally is to use Visual Studio Code as a development tool. I am working on Mac, so I have downloaded and installed a version for Mac OS X.   VS Code is all about extensions, for every technology you are working with you are installing one or a few extensions. Same is about Azure Functions. There is an extension for that:



Having done that, you are getting a new tab where you can create a new function application and start implementing your functions:


While configuring a new project the wizard is asking you to select a language you prefer to implement the functions with:


Even though I love Java, I have selected JavaScript because on top of regular functions I wanted to implement durable functions and they support C#, F# and JavaScript only. At the moment of writing this post JavaScript was the closest to me.

Th rest is as usual. You create functions, write the code, debug, test, fix, and all over again. You just click F5 and VS Code starts the entire application in debug mode for you:


When you start the application for the first time, VS Code will propose you to install the functions runtime on your computer if it is not there. So basically, assuming that you have on your laptop runtime of your preferred language (Node.js), you just need to have VS Code with the functions extension to start working with Azure Functions. It will do the rest of installations for you. 

So, once the application is started I can test it by invoking NotifyOnTask function which initiates the entire cycle:
curl -X POST --data '{"taskid":"8900","description":"DiPocket v.1.0.0.1 is about to be deployed to PROD"}'  -H "Content-type: application/json" http://localhost:7071/api/orchestrators/NotifyOnTask

The source code of the application is available on GitHub.

Well, the general opinion of Azure Functions so far is ... it is good. It just works. I didn't run into any annoying issue (so far) while implementing this solution (except some stupid mistakes that I made because I didn't read the manual carefully). I will definitely keep playing and posting on Azure Functions enriching and moving this solution to the cloud and, probably, implementing something different.

That's it!



No comments:

Post a comment

Post Comment