For the last 10 months I’ve been working with Azure Functions and mulling over the implications of computing without servers.
If Azure Functions were an entrée, I’d say the dish layers executable code over unspecified hardware in the cloud with a sprinkling of input and output bindings on top. However, Azure Functions are not an entrée, so it might be better to describe the capabilities without using culinary terms.
With Azure Functions, I can write a function definition in a variety of languages – C#, F#, JavaScript, Bash, Powershell, and more. There is no distinction between compiled languages, interpreted languages, and languages typically associated with a terminal window. I can then declaratively describe an input to my function. An input might be a timer (I want the function to execute every 2 hours), or an HTTP message (I want the function invoked when an HTTPS request arrives at functions-test.azurewebsites.net/api/foo), or when a new blob appears in a storage container, or a new message appears in a queue. There are other inputs, as well as output bindings to send results to various destinations. By using a hosting plan known as a consumption plan, I can tell Azure to execute my function with whatever resources are necessary to meet the demand on my function. It’s like saying “here is my code, don’t let me down”.
Azure Functions are cheap. While most cloud services will send a monthly invoice based on how many resources you’ve provisioned, Azure Functions only bill you for the resources you actively consume. Cost is based on execution time and memory usage for each function invocation.
Azure Functions are simple for simple scenarios. If you have the need for a single webhook to take some data from an HTTP POST and place the data into a database, then with functions there is no need to create an entire project, or provision an app service plan.
The amount of code you write in a function will probably be less than writing the same behavior outside of Azure Functions. There’s less code because the declarative input and output bindings can remove boilerplate code. For example, when using Azure storage, there is no need to write code to connect to an account and find a container. The function runtime will wire up the everything the function needs and pass more actionable objects as function parameters. The following is an example function.json file that defines the bindings for a function.
{ "disabled": false, "bindings": [ { "authLevel": "function", "name": "req", "type": "httpTrigger", "direction": "in" }, { "name": "$return", "type": "http", "direction": "out" } ] }
Azure Function are scalable. Of course, many resources in Azure are scalable, but other resources require configuration and care to behave well under load.
One criticism of Azure functions, and PaaS solutions in general, is vendor lock-in. The languages and the runtimes for Azure Functions are not specialized, meaning the C# and .NET or JavaScript and NodeJS code you write will move to other environments. However, the execution environment for Azure Functions is specialized. The input and output bindings that remove the boilerplate code necessary for connecting to other resources is a feature only the function environment provides. Thus, it requires some work to move Azure Function code to another environment.
One of the biggest drawbacks to Azure functions, actually, is that deploying, authoring, testing, and executing a function has been difficult in any environment outside of Azure and the Azure portal, although this situation is improving (see The Future section below). There have been a few attempts at creating Visual Studio project templates and command line tools which have never progressed beyond a preview version. The experience for maintaining multiple functions in a larger scale project has been frustrating. One of the selling points of Azure Functions is how the technology is so simple to use, but you can cross a threshold where Azure Functions become too simple to use.
There have been a few announcements this year that have Azure Functions moving in the right direction. First, there is now an installer for the Azure Functions runtime. With the installer, you can setup an execution environment on a Windows server outside of Azure.
Secondly, in addition to script files, Azure Functions now supports a class library deployment. Class libraries are more familiar to most .NET developers compared to C# script files. Plus, class libraries are easier to author with Intellisense and Visual Studio, as well as being easier to build and unit test.
Azure Functions are hitting two sweet spots by providing a simple approach to put code in the cloud with the scripting model, while class libraries support projects with larger ambitions.
However, the ultimate goal for many people coming to Azure Functions is the cost effectiveness and scalability of serverless computing. The serverless model is so appealing, I can imagine a future where instead of moving code into a serverless platform, more serverless platforms will appear and come to your code.
Adding Feedback from Solvingj:
Great summary! I always look forward to your point of view on new .NET technologies.
I wanted to add some recent updates you will surely want to be aware of and perhaps mention:
-Deployment slots (preview)
-Built-In Swagger Support (getting there)
-The option to use more traditional annotation-based mapping of API routes
-Durable functions in beta
This enables a stateful orchestrator pattern, which is always on, can call other functions, etc. Whereas Azure Functions originally represented a paradigm shift away from monolithic ASP.NET applications with many methods to tiny stateless applications, durable functions represent a slight paradigm shift back the other way. I think the end-result is Azure Functions supporting a much broader (yet still healthy) range of workloads that can be moved over from ASP.NET monoliths, while maintaining it's "serverless" advantages.
-Continuous Integration
I know you're aware of this feature since it's been part of Azure App service for ages. However, you did not mention it, and for us, this was perhaps the most attractive feature. The combination of Azure Functions Runtime + GIT integration enables the complete elimination of whole categories of tedious DevOps engineering concerns during rapid prototyping. No Docker, no Appveyor, just GIT and an Azure Function App. Of course, you can still add Appveyor for automated testing when it makes sense, but it's not required early on.
Other Experiences
-Precompiled Functions
I happen to agree with Chris regarding precompiled functions. In fact, once we refactored away from the .csx scripts and into libraries, what we were left with was a normal ASP.net structure, with one project for the function.json files and all the advantages of the Azure Functions runtime (most mentioned here). Once we switched to using Functions with this pattern, the entire Cons section you described no longer applied to us.
-Bindings
One of the Cons we found that you did not mention was in the binding and configurations strategy. We liked the idea of declarative configurations and removal of boiler plate code for building external connections in theory. However, in practice, the output bindings were simply too inflexible for our applications. Our functions were more interactive, needing to make multiple connections to the outside within a single function, and the bindings did not provide our code a handle to it's connection managers. Thus, we ended up having to build our own anyway, rendering the built-in output bindings pointless. I submitted feature requests for this however.