How to Serve Serverless with Examples in Azure

Modern development is about fast, modular code, microserverices architectures and cloud-readiness. I wrote about cross-platform containers just a few months ago to explain the myriad benefits of having small, self-contained images with all of your app dependencies in simple, lightweight and reusable containers. Although containers are often cited as the infrastructure of choice for microservices applications, adding true scale and resiliency requires introducing an orchestration platform like Docker Swarm, DC/OS or Kubernetes. All of these solutions require additional infrastructure to manage hosts and clusters.

A more straightforward approach that has recently gained popularity is the so-called serverless solution. Serverless empowers the developer to focus on code, not platform, and, in many cases, even eliminates the need to manage dependencies. Instead of deploying to a specific Platform-as-a-Service (PaaS) target like an Azure Web App, serverless focuses on deploying functions and code snippets. There are many serverless platform providers today, including AWS Lambda, WebTask, and Azure Functions. Despite differences in UI and deployment models, the fundamental concepts are the same across providers and in many cases you can write your code once and deploy your choice of provider.

Getting Started with Azure Functions

I'll use Azure Functions to start with a simple demonstration. If you don't have an Azure account, in most countries you can sign up for free credits at this link. After you log into your account, navigate to New, choose the "Compute" category then choose Function App. The Function App blade will appear, allowing you to enter a name for the collection of functions you will build, the subscription for billing, a hosting plan (choose the default for now), the location, and associated storage account.

Function App Blade

Tap Create and give Azure a few seconds or minutes to create your app. Once it is created, you can click on the Function App name to begin building it. In the navigation, hit the plus sign next to the "Functions" section.

Function Navigation

Under "Get started on your own" choose "Custom function". Select "JavaScript" for the language, then "Core" for the Scenario and tap on the "HttpTrigger-JavaScript" box. Feel free to leave the default name or change it, change the authorization level to "Anonymous" and tap Create.

This will take you to an in-browser code editor. The default function looks something like this:

module.exports = function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');

    if (req.query.name || (req.body && req.body.name)) {
        context.res = {
            // status: 200, /* Defaults to 200 */
            body: "Hello " + (req.query.name || req.body.name)
        };
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
    context.done();
};

In the upper right, you will see "Get function URL." Copy that, append the querystring "?name=YourName" and navigate to the result in your browser. You should see something like this:

Browser

Notice that the code took the query string parameter and returned back the appropriate payload. You've just created your first function!

To change it to do something more meaningful than just echo text, modify the top portion of the if statement to this:

let text = req.query.text || (req.body && req.body.text);

if (text) {
    // status: 200, /* Defaults to 200 */
    let codes = text.split('').map(x => x.charCodeAt(0));
    let result = {};
    codes.forEach(code => {
        if (result[code]) {
            result[code]++;
        }
        else {
            result[code] = 1;
        }
    });
    context.res = result;
}

Keep the else portion and the rest of the original code. Next, instead of navigating through your browser, expand the ribbon to the right that lists "View files" and "Test" and navigate to the "Test" tab. In the request body, put:

{
    "text" : "This is a test."
}

Click "Save and run" and you should see an object appear in the "Output" window that looks like this:

{"32":3,"46":1,"84":1,"97":1,"101":1,"104":1,"105":2,"115":3,"116":2}

This indicates the code successfully parsed your text and returned an object with a count of characters by their ASCII code.

Serverless Still Requires Servers

Serverless is a misleading term because the functions still require servers to run. In fact, you can look "behind the curtain" by tapping the Function App name ("telerikexample" is what I used) to see an overview with a choice of Settings, Platform Features, and API definition. Tap on Platform Features to see your various options:

Platform Features

Among other things you'll find that you can configure your application settings, set up automated deployment, interact with a browser-based console and even use advanced tools called Kudu to further configure the host.

Function apps provide all of the flexibility of other hosted options, from storing secrets in application settings to securing access with certificates and authentication options. Function apps can participate in a full DevOps lifecycle and have different environments for testing versus production deployment. The use of slots makes it easier to stage, test, and roll back unsuccessful deployments.

When you created the function app, you chose the "consumption plan." Every function app has an associated "app service plan" that defines how usage is billed and provides options to scale.

The consumption plan is the simplest plan because it is completely managed by Azure. You are charged based on usage of your function app, essentially how much time it runs to accommodate requests. When a host becomes overloaded with requests, Azure will spin up new hosts and load balance requests to accommodate the additional requests. This is an incredibly low cost model (to put it in perspective, a Docker host of mine running on a very small Linux VM may run up $100 in charges in a month even when nothing is being accessed, compared to a function app I had process 10,000 requests and cost me several cents.) The drawback is that when your service is idle, it may take additional time to spin up a new host when a request first comes in.

You can also use a more traditional app service plan. In the traditional plan, you are able to define the number of hosts to begin with and rules for scaling. For example, you can choose to scale based on CPU usage and set a minimum number of active hosts to process requests immediately, as well as an upper limit to avoid excessive charges.

Tower of Babel

When you created the function, you were able to select a language. A function app scales as a unit, but can have multiple functions and every function can be written in its own language. You can leverage C# and even precompile your function for performance benefits as well as reference Nuget packages for third-party functionality. You can also leverage JavaScript to run on a Node.js host and pull packages from npm. Other options include Bash script on a Linux host, F#, PHP, PowerShell, and Python.

The ability to choose the language that makes sense for the function makes the platform ideal for microservices-style scenarios. Each microservice can be implemented in the language that makes the most sense for it. For example, "Batch" may make the most sense for processing large files. You can specify a provider like OneDrive, Box, DropBox, or even a shared file system and have your batch process called any time a new file is uploaded. If your function involves a financial algorithm you may want to use F# instead of C# to implement the functional logic in your code.

Trigger Happy

The example function used an HTTP trigger for input and output. Hitting an HTTP endpoint fired the trigger and caused the HTTP payload (including the querystring contents and request body) to be passed to the function. The function then set a result value, and an HTTP output enables the result to be written to the output. Notice that you did not have to write any HTTP-specific code yourself, or even reference an HTTP client or server. This was all handled for you.

Azure Functions provide a variety of triggers. You can invoke your code based on an HTTP call, a timer, an event when a blob is written to Azure storage, a webhook from a third-party like GitHub, an item being placed in a queue or even a service bus entry being routed.

Triggers

In addition, you can take the item or payload from the trigger and combine it with an Input (available from the "Integrate" tab in the Function Navigation) to have a table entry, document from Azure DocumentDB or even a blob reference passed into your function automatically. You are also able to define multiple outputs ranging from Azure Event Hub to an external file and even an Azure Service Bus entry.

Inputs

Triggers make it easy to invoke a function and manage data flow, but they are not the only option. You can also access connection strings and directly invoke libraries from within your code. This makes functions truly flexible and powerful despite how easy they are to host and scale.

Conclusion

Modern development is about continuous deployment to the edge and shortening the cycles to provide more bandwidth for innovation. A variety of approaches exist to make this possible, from Infrastructure as Code to Platform-as-a-Service and containers. Serverless provides another path that empowers developers to focus on the important aspect of a solution, the code and unique logic, rather than spinning cycles dealing with repetitive infrastructure concerns. Serverless is yet another example of the power and momentum microservices architectures have in the enterprise today.

Comments