Introduction
For those of you working with Azure today, one of the most useful features is the ability to generate entire infrastructure architectures as code through ARM Templates. Having worked with the raw Azure APIs before ARM existed, I can definitely appreciate the value that it brings, with a declarative model that allows repeatable deployments and idempotent releases among other things.
ARM - room for improvement
Unfortunately, ARM does have several limitations including the fact that it is essentially a JSON dialect. This means not only that it's pretty verbose, but also that is requires a lot of "embedded" stringly-typed code in order to achieve what might be pretty simple in a "proper" programming language, such as references variables and parameters or writing what should be trivial programming elements such as for loops.
This means that, whilst once created working with ARM templates themselves is relatively straightforward, the authoring of the templates themselves is time-consuming and error-prone.
Whilst there have been some recent improvements to ARM - including tooling improvements in VS Code through the ARM extension, we think that we can do much better than relying on tooling for a specific IDE. However, this means looking at something apart from JSON whendirectly authoring ARM templates themselves.
Enter Farmer
We've been working on a lightweight project that we call Farmer which tries to simplify the process of rapidly generating ARM templates. Farmer is not a replacement for ARM - think of it as a "preprocessor", or a build step, which can be used to quickly knock up an ARM template - so you can continue to make use of all the great things about ARM, but save time when creating the templates themselves. You can use Farmer for:
- As a way to quickly generate your ARM template, which is then committed into source control and deployed as normal by e.g. Azure Dev Ops.
- Creating a basic ARM template which generates 90% of what you need, after which you will then manually make further changes to the template, and deploy or commit into source control as normal.
- As a build step in your CD process to generate and deploy your ARM template. In this model, you commit your Farmer code into source control; the ARM template is a transient file that is generated during the build process and deployed into Azure, similar to the relationship between e.g. Typescript and Javascript or C# and a DLL.
The choice is yours. However you're looking to use Farmer, we're delighted to announce that we have now published a preview release of the Farmer project onto NuGet, ready for you to try out.
Your first Farmer template
Farmer is a free-to-use and open source NuGet package that comes with a simple lightweight API for authoring, creating and deploying templates, so you can easily integrate Farmer into any other dotnet codebase - or run it as a standardalone application (which is what we're about to do).
- Let's start by by creating a new console application:
dotnet new console -lang f#
Yep, Farmer is an F#-only library - but don't let that scare you! Farmer applications are just .NET Core projects that happen to be written in F# - they can also be referenced from C#, if needed.
- Now, add the Farmer NuGet package to the console app:
dotnet add package Farmer
- Open the project in Visual Studio and open
Program.fs
. - Delete any code in the file and enter the following two lines to open up the Farmer namespaces that we need.
open Farmer
open Farmer.Resources
- Now let's create an Azure web application with Application Insights in the North Europe region that has a custom app setting.
let myWebApp = webApp {
name "isaacswonderfulapp"
setting "mysetting" "FarmerIsGreat!"
}
Make sure you change the
name
to something unique for yourself, as this evaluates to URL that Azure will allocate to the webapp e.g."scottswonderfulapp"
.
That's all you need to get going. However, Farmer comes with an extensible set of custom operations for each Azure resource type that allow you to configure the resource as you see fit, such as the number of workers, SKU or operating system.
- Now we'll create the overall ARM template that includes this webapp:
let deployment = arm {
add_resource myWebApp
location NorthEurope
}
This adds the web application resource to the overall template, and sets the default location to North Europe for all resources.
- Almost there. Now we can generate the JSON from our .NET object model:
let filename =
deployment.Template
|> Writer.toJson
|> Writer.toFile "myfirstdeploy"
printfn "Template is generated! See %s." (System.IO.Path.GetFullPath filename)
The
|>
operator in F# is called pipeline and is commonly used to create a pipeline of operations on data. If you're used to C#, think of this as a kind of fluent API.
Alternatively, you can deploy the template directly into Azure if you have the Azure CLI installed (note: this support is currently Windows only)
deployment
|> Writer.quickDeploy "my-resource-group"
- Now run the application using normal dotnet CLI (or hit F5 in Visual Studio):
dotnet run
- That's it! You'll now have a full ARM template that contains:
- An App Service Host
- An App Service
- An Application Insights instance
All are fully configured and connected together, ready to be deployed. The template, of course, will contain the custom setting we created in our little program as well.
Why F#?
There are several reasons for our choice here.
Firstly, using a programming language as opposed to JSON gives you all the standard benefits, such as a compiler, rich type checking and, of course, access to the full .NET Core ecosystem. In addition, with a full programming language you can arbitrarily mix code and ARM without the need to use strings that ARM has to parse as expressions:
/// A function that creates web apps that start with "web-app-", parameterised the postfix
let createWebApp postfix = webApp {
name (sprintf "web-app-%s" postfix)
}
/// Create three web apps using the createWebApp function above
let webApps = [ "foo"; "bar"; "baz" ] |> List.map createWebApp
/// Add each of them to the template
let myTemplate = arm {
add_resource webApps.[0]
add_resource webApps.[1]
add_resource webApps.[2]
}
Also, F# has some excellent support for abstractions and modelling which are particuarly well suited for this kind of problem, and the ability to create custom operations is also especially useful. C# is a fine language, but we felt in this case that F# was a much more natural fit.
Supported resources
Farmer includes support for some of the most commonly used resources, including Storage, Search, CosmosDB, SQL Azure, Virtual Machines and more.
Summary
It's still early days for Farmer, but we think it's a great way to start moving away from manually creating infrastructure and starting to use ARM templates - and if you're already using ARM, to lower the operational cost of maintaining them.
We're really looking forward to your feedback - so please do try out Farmer today and let us know what you think of it.
The ARM template
Just in case you're curious, this is what the generated ARM template looks like from the sample above:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {},
"resources": [
{
"apiVersion": "2016-08-01",
"dependsOn": [
"isaacswonderfulapp-plan",
"isaacswonderfulapp-ai"
],
"kind": "app",
"location": "northeurope",
"name": "isaacswonderfulapp",
"properties": {
"serverFarmId": "isaacswonderfulapp-plan",
"siteConfig": {
"alwaysOn": false,
"appSettings": [
{
"name": "mysetting",
"value": "FarmerIsGreat!"
},
{
"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
"value": "[reference('Microsoft.Insights/components/isaacswonderfulapp-ai').InstrumentationKey]"
},
{
"name": "APPINSIGHTS_PROFILERFEATURE_VERSION",
"value": "1.0.0"
},
{
"name": "APPINSIGHTS_SNAPSHOTFEATURE_VERSION",
"value": "1.0.0"
},
{
"name": "ApplicationInsightsAgent_EXTENSION_VERSION",
"value": "~2"
},
{
"name": "DiagnosticServices_EXTENSION_VERSION",
"value": "~3"
},
{
"name": "InstrumentationEngine_EXTENSION_VERSION",
"value": "~1"
},
{
"name": "SnapshotDebugger_EXTENSION_VERSION",
"value": "~1"
},
{
"name": "XDT_MicrosoftApplicationInsights_BaseExtensions",
"value": "~1"
},
{
"name": "XDT_MicrosoftApplicationInsights_Mode",
"value": "recommended"
}
],
"metadata": [
{
"name": "CURRENT_STACK",
"value": "dotnetcore"
}
]
}
},
"resources": [
{
"apiVersion": "2016-08-01",
"dependsOn": [
"isaacswonderfulapp"
],
"name": "Microsoft.ApplicationInsights.AzureWebSites",
"properties": {},
"type": "siteextensions"
}
],
"type": "Microsoft.Web/sites"
},
{
"apiVersion": "2018-02-01",
"kind": "app",
"location": "northeurope",
"name": "isaacswonderfulapp-plan",
"properties": {
"name": "isaacswonderfulapp-plan",
"perSiteScaling": false,
"reserved": false
},
"sku": {
"capacity": 1,
"name": "F1",
"size": "0",
"tier": "Free"
},
"type": "Microsoft.Web/serverfarms"
},
{
"apiVersion": "2014-04-01",
"kind": "web",
"location": "northeurope",
"name": "isaacswonderfulapp-ai",
"properties": {
"ApplicationId": "isaacswonderfulapp",
"Application_Type": "web",
"name": "isaacswonderfulapp-ai"
},
"tags": {
"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', 'isaacswonderfulapp')]": "Resource",
"displayName": "AppInsightsComponent"
},
"type": "Microsoft.Insights/components"
}
]
}