In this blog post I will show how we reduced our 1000+ lines of Azure Service Bus arm-template configuration to just a few lines of F#. And then some…

Background

Although I do value what arm-templates bring to the table with repeatable infrastructure deployments, it does not really deliver on the promise of infrastructure as code. Mostly because the arm-template json is not a real programming language but rather a pretty horrible language to do any actual coding in.

In our current Azure Service Bus setup we are using quite a lot of topics and subscriptions. We have adopted a one-to-one relationship between our messages and topics. A topic can then have one or more subscribers (in our case an Azure Function). For example a BlogPostCreated message is sent to a topic with the same name and will be consumed by the subscribers UpdateBlogPostSearchIndex and NotifyBlogSubscribers.

The json for creating a service bus namespace with a single topic and two subscriptions in an arm-template is quite verbose, and comes to about 50 lines of json:

{
    "apiVersion": "2017-04-01",
    "dependsOn": [],
    "location": "westeurope",
    "name": "my-service-bus",
    "sku": {
        "name": "Standard",
        "tier": "Standard"
    },
    "tags": {},
    "type": "Microsoft.ServiceBus/namespaces"
},
{
    "apiVersion": "2017-04-01",
    "dependsOn": [
        "[resourceId('Microsoft.ServiceBus/namespaces', 'my-service-bus')]"
    ],
    "name": "my-service-bus/BlogPostCreated",
    "properties": {
        "defaultMessageTimeToLive": "P14D",
        "duplicateDetectionHistoryTimeWindow": "PT10M",
        "enablePartitioning": true,
        "requiresDuplicateDetection": true
    },
    "type": "Microsoft.ServiceBus/namespaces/topics"
},
{
    "apiVersion": "2017-04-01",
    "dependsOn": [
        "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-service-bus', 'BlogPostCreated')]"
    ],
    "name": "my-service-bus/BlogPostCreated/UpdateBlogPostSearchIndex",
    "properties": {
        "deadLetteringOnMessageExpiration": true,
        "duplicateDetectionHistoryTimeWindow": "PT10M",
        "lockDuration": "PT5M",
        "maxDeliveryCount": 10,
        "requiresDuplicateDetection": true
    },
    "resources": [],
    "type": "Microsoft.ServiceBus/namespaces/topics/subscriptions"
},
{
    "apiVersion": "2017-04-01",
    "dependsOn": [
        "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-service-bus', 'BlogPostCreated')]"
    ],
    "name": "my-service-bus/BlogPostCreated/NotifyBlogSubscribers",
    "properties": {
        "deadLetteringOnMessageExpiration": true,
        "duplicateDetectionHistoryTimeWindow": "PT10M",
        "lockDuration": "PT5M",
        "maxDeliveryCount": 10,
        "requiresDuplicateDetection": true
    },
    "resources": [],
    "type": "Microsoft.ServiceBus/namespaces/topics/subscriptions"
}

Enter Farmer

Farmer, according to its own description, is “a .NET domain-specific-language (DSL) for rapidly generating Azure Resource Manager (ARM) templates.”

With Farmer’s DSL we can re-write the json above to

serviceBus {
    name "my-service-bus"
    sku ServiceBus.Sku.Standard
    add_topics [
        topic {
            name topicName
            message_ttl (14<Days>)
            duplicate_detection_minutes 10
            enable_partition
            add_subscriptions [
                subscription {
                    name "UpdateBlogPostSearchIndex"
                    lock_duration_minutes 5
                    enable_dead_letter_on_message_expiration
                    max_delivery_count 10
                    duplicate_detection_minutes 10
                }
                subscription {
                    name "NotifyBlogSubscribers"
                    lock_duration_minutes 5
                    enable_dead_letter_on_message_expiration
                    max_delivery_count 10
                    duplicate_detection_minutes 10
                }
            ]
        }
    ]
}

Since we know there’s going to be many more topics and subscriptions, let’s extract that part to an array where we can add all our topics and subscriptions:

module Topics =
    let all = 
        [|
            {| Topic = "BlogPostCreated"
               Subscriptions = [| "UpdateBlogPostSearchIndex"; "NotifyBlogSubscribers" |] |}
            // ... and many more
        |]

serviceBus {
    name "my-service-bus"
    sku ServiceBus.Sku.Standard
    add_topics 
        [ for ts in Topics.all do
            topic {
                name ts.Topic
                message_ttl (14<Days>)
                duplicate_detection_minutes 10
                enable_partition
                add_subscriptions 
                    [ for subscriptionName in ts.Subscriptions do
                        subscription {
                            name subscriptionName
                            lock_duration_minutes 5
                            enable_dead_letter_on_message_expiration
                            max_delivery_count 10
                            duplicate_detection_minutes 10
                        }]
            }]
}

With the move from json to Farmer above we managed to reduce the amount of code for our full service bus configuration (~30 topics) with ~1000 lines.

Another problem of using json arm-templates

Both the infrastructure code (the json template) and our Azure Functions application need to know about the topic and subscription names. Additionally, the ServiceBusTrigger attribute we use for the azure function input binding, requires the topic and subscription name to be known at compile time:

[Function("UpdateBlogPostSearchIndex")]
public async Task RunUpdateBlogPostSearchIndex(
    [ServiceBusTrigger("BlogPostCreated", "UpdateBlogPostSearchIndex", Connection = "<connection-string>")]Message message, FunctionContext executionContext)
    {
        // code, code, code...
    }

Previously we didn’t really solve this in a nice way, and ended up having the topic and subscription names duplicated in the arm-template parameters and the application code in the Azure Function App. If you needed to change the name of a topic or subscription you had to remember to do so in both places.

The benefits of using a ‘real’ programming language

When you realise that Farmer is just .NET code and that you can have your infrastructure code interop with your application code this opens up a whole new can of worms. So when our Farmer code needs to add all the topics and subscriptions it just need to find all the usages of the ServiceBusTrigger attribute, and that’s not too hard to do with a bit of reflection:

open System.Reflection
open Microsoft.Azure.Functions.Worker

module Topics =
    let getAzureFunctions (t: Type) = 
        let hasFunctionAttribute (m: MethodInfo) = m.GetCustomAttribute<FunctionAttribute>() |> isNull |> not
        t.GetMethods()
        |> Array.filter hasFunctionAttribute
    let getServiceBusTriggerAttributes (m: MethodInfo) =
        m.GetParameters()
        |> Array.choose (fun p -> p.GetCustomAttribute<ServiceBusTriggerAttribute>() |> Option.ofObj)

    typeof<MyFunctionApp>.Assembly.GetTypes()
    |> Array.collect getAzureFunctions
    |> Array.collect getServiceBusTriggerAttributes
    |> Array.map (fun attribute -> {| Topic = attribute.TopicName; Subscription = attribute.SubscriptionName |})
    |> Array.groupBy (fun st -> st.Topic)
    |> Array.map (fun (topic, grouping) -> topic, grouping |> Array.map (fun st -> st.Subscription))

So now we have, not only reduced our arm-template configuration by 1000+ lines of code, we have also the added benefit of always having the infrastructure deployment code being in sync with the application code. If you make a change to a topic or subscription in the application code the Farmer deployment will find out about it automagically!