7 min read

Separation of Concerns: Logic App from ARM Template

Justin Yoo

Azure Logic App is a set of workflow definitions, which is written in JSON format. The nature of JSON object results in this being tightly bound with ARM template. In other words, the Logic App has a dependency on an ARM template. Due to this characteristics, when any update is made on the workflow, the entire ARM template should be deployed over and over again. As we all know, an ARM template defines Azure resources as infrastructure, while the Logic App is an application defines a set of workflow.

One of software design principles, Separation of Concerns (SoC), depicts that individual software components take one responsiblity (or concern) so that each component should not impact on each other. From this point of view, a Logic App workflow definition and a Logic App instance defined by an ARM template are two different concerns. Therefore, they should be separated. In this post, I am going to show how to separate Logic App workflow from ARM template and deploy them respectively.

The sample Logic App used for this post can be found at here.

Anatomy of Logic App

A Logic App and its wrapping ARM template looks like this:

{
"$schema": "...",
"contentVersion": "...",
"parameters": { ... },
"variables": { ... },
"resources": [
{
"apiVersion": "...",
"type": "Microsoft.Logic/workflows",
"name": "...",
"location": "...",
"tags": { ... },
"properties": {
"parameters": { ... },
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"parameters": { ... },
"triggers": { ... },
"actions": { ... },
"outputs": { ... }
}
}
}
],
"outputs": { ... }
}
view raw logic-app.json hosted with ❤ by GitHub

The big JSON object mapped to the properties attribute is the very Logic App. It contains two major fields – parameters and definition. The parameter field works as a gateway to pass values from ARM template to Logic App. The definition field defines the actual workflow. Therefore, if we can extract these two fields from the ARM template, the problem will be solved. Let's do it.

Writing Logic App

Here's the scenario. This Logic App is triggered by an HTTP request, then gets all the list of ARM template deployment history of the given resource group and returns the result.

The ARM template including the Logic App workflow might look like this:

{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": { ... },
"variables": { ... },
"resources": [
{
"type": "Microsoft.Logic/workflows",
...
"properties": {
"parameters": {
"$connections": {
"value": {
"arm": {
"connectionId": "/subscriptions/{SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.Web/connections/{CONNECTOR_NAME}",
"id": "/subscriptions/{SUBSCRIPTION_ID}/providers/Microsoft.Web/locations/australiasoutheast/managedApis/arm"
}
}
},
"arm": {
"value": {
"subscriptionId": "{SUBSCRIPTION_ID}",
"resourceGroup": "{RESOURCE_GROUP}",
"apiVersion": "{API_VERSION}"
}
}
},
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"$connections": {
"type": "object",
"defaultValue": {}
},
"arm": {
"type": "object",
"defaultValue": {}
}
},
"triggers": {
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {}
}
}
},
"actions": {
"GetDeploymentHistories": {
"type": "ApiConnection",
"runAfter": {},
"description": "Gets the all deployment history.",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['arm']['connectionId']"
}
},
"method": "get",
"path": "/subscriptions/@{encodeURIComponent(parameters('arm')['subscriptionId'])}/resourcegroups/@{encodeURIComponent(parameters('arm')['resourceGroup'])}/providers/Microsoft.Resources/deployments",
"queries": {
"x-ms-api-version": "@parameters('arm')['apiVersion']"
}
}
},
"Response": {
"type": "Response",
"runAfter": {
"GetDeploymentHistories": [
"Succeeded"
]
},
"description": "Returns the successful response.",
"inputs": {
"statusCode": 200,
"body": "@body('GetDeploymentHistories')?.value"
}
}
},
"outputs": {}
}
}
}
],
"outputs": { ... }
}

Separating Logic App

From the ARM template above, extract the parameters and definition attributes and save them into separate files respectively.

{
"$connections": {
"value": {
"arm": {
"connectionId": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/connections/{connectorName}",
"id": "/subscriptions/{subscriptionId}/providers/Microsoft.Web/locations/australiasoutheast/managedApis/arm"
}
}
},
"arm": {
"value": {
"subscriptionId": "{subscriptionId}",
"resourceGroup": "{resourceGroup}",
"apiVersion": "{apiVersion}"
}
}
}
view raw parameters.json hosted with ❤ by GitHub

When we look at parameters.json above, it contains some braced texts like {subscriptionId}, {resourceGroup}, {connectorName} and {apiVersion}. This is a placeholder for substitution so that each CI/CD pipelines can replace them with their own values.

Here is definition.json that defines the workflow. Mak sure that we cannot use parameters or variables from ARM template because this is no longer depending on the ARM template. But we can refer to the parameters.json.

{
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"$connections": {
"type": "object",
"defaultValue": {}
},
"arm": {
"type": "object",
"defaultValue": {}
}
},
"triggers": {
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {}
}
}
},
"actions": {
"GetDeploymentHistories": {
"type": "ApiConnection",
"runAfter": {},
"description": "Gets the all deployment history.",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['arm']['connectionId']"
}
},
"method": "get",
"path": "/subscriptions/@{encodeURIComponent(parameters('arm')['subscriptionId'])}/resourcegroups/@{encodeURIComponent(parameters('arm')['resourceGroup'])}/providers/Microsoft.Resources/deployments",
"queries": {
"x-ms-api-version": "@parameters('arm')['apiVersion']"
}
}
},
"Response": {
"type": "Response",
"runAfter": {
"GetDeploymentHistories": [
"Succeeded"
]
},
"description": "Returns the successful response.",
"inputs": {
"statusCode": 200,
"body": "@body('GetDeploymentHistories')?.value"
}
}
},
"outputs": {}
}
view raw definition.json hosted with ❤ by GitHub

As a result, the existing ARM template will become like this:

{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": { ... },
"variables": { ... },
"resources": [
{
"type": "Microsoft.Logic/workflows",
...
"properties": {
"parameters": {},
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"triggers": {},
"actions": {},
"outputs": {}
}
}
}
],
"outputs": { ... }
}

Now, the ARM template only takes care of deploying the Logic App instance. After getting this ARM template deployed, it will only have a blank Logic App. In fact, this is the screenshot of this ARM template deployment.

We now have separated Logic App from ARM template.

Deploy Logic App

This time, we need to deploy the Logic App workflow. Set-AzureRmResource is the cmdlet we should use for this. Let's see the PowerShell script below. First of all, it defines all the necessary variables, reads parameters.json and substitutes the placeholders with defined variables, and reads definition.json. Then it gets the existing Logic App details, updates its parametres and definition with values just read from the files, and saves the resource back to Azure.

$subscriptionId = "[SUBSCRIPTION_ID]"
$resourceGroup = "[RESOURCE_GROUP]"
$resourceName = "[RESOURCE_NAME]"
$connectorName = "[CONNECTOR_NAME]"
$apiVersion = "2016-06-01"
# Read parameters.json, replace values, and convert it to PSObject
$param = (Get-Content parameters.json -Encoding UTF8 -Raw) `
-replace "{subscriptionId}", $subscriptionId `
-replace "{resourceGroup}", $resourceGroup `
-replace "{connectorName}", $connectorName `
-replace "{apiVersion}", $apiVersion | `
ConvertFrom-Json
# Read definition.json and convert it to PSObject
$definition = Get-Content definition.json -Encoding UTF8 -Raw | `
ConvertFrom-Json
# Get Logic App
$resource = Get-AzureRmResource `
-ResourceGroupName $resourceGroup `
-ResourceName $resourceName `
-ResourceType Microsoft.Logic/workflows
# Update Logic App
$resource.Properties.parameters = $param
$resource.Properties.definition = $definition
$resource | Set-AzureRmResource -Verbose -Force

Once completed, check the Logic App through the Azure Portal. It has been updated. We can also confirm that the Logic App works as expected.

Considerations

As mentioned above, definition.json only accepts values defined by itself or from parameters.json. However, in most cases, parameters.json should interact with ARM template, but there is no way for now. In order to sort this out, we can use the outputs property from the ARM template. After the ARM template deployment, it returns whatever we defined in the outputs section.


So far, we have walked through how to extract Logic App workflow from ARM template to achieve the SoC design principle. By doing so, the existing ARM template only focuses on deploying infrastructure and we can only focus on the Logic App workflow in a separate file so that CI/CD pipelines can only pick up changes from the extracted ones.

As Logic Apps does not currently support this separating feature out-of-the-box, for now this would be the best approach. I hope this can be done within the Logic App sooner rather than later.

This has been originally posted at Mexia blog.