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": { ... } | |
} |
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}" | |
} | |
} | |
} |
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": {} | |
} |
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.