Preparing Azure Logic Apps for CI/CD to Multiple Environments

 Introduction

Logic Apps can be created from the Azure Portal, or using Visual Studio. This works well if you want to create one Logic App at a time. However, if you want to deploy the same Logic App in multiple environments, e.g. Dev, Test, or Production, you want to do it in an automated way. Azure Resource Manager (ARM) Templates allow you to define Azure Resources, including Logic Apps, for automated deployment to multiple environments in a consistent and repeatedly way. ARM Templates can be tailored for each environment using a Parameters file.

The deployment of Logic Apps using ARM Templates and Parameters can be automated with different tools, such as, PowerShell, Azure CLI, or VSTS. In my projects, I normally use a VSTS release definition for this.

You probably have noticed that the Logic App Workflow Definition Language (the JSON code behind) has many similarities with the ARM Templates structure, including the use of expressions and functions, variables, and parameters.

ARM Template expressions and functions are written within JSON string literals wrapped with square brackets []. ARM expressions and functions can appear in different sections of the ARM template, including the resources member, which might contain Logic Apps. The value of these expressions is evaluated at deployment time. More information here.

Logic App expressions and functions are defined within the Logic App definition and might appear anywhere in a JSON string value. Logic Apps expressions and functions are evaluated at execution time. These are declared using the @ sign. More information here.

These similarities can be confusing by themselves. I’ve seen that it’s a quite common practice in ARM Templates with Logic Apps, to use ARM template expressions inside the Logic App definition. For example, using ARM parameters, ARM variables or ARM functions (like concat), within the definition of a Logic App. This might seem OK, as this is what you would normally do to tailor your deployment for any other Azure resources. However, in Logic Apps, this can be quite cumbersome. If you’ve done it, I’m almost sure that you know what I’m talking about.

In this post, I’ll share some practices that I use to ease the preparation of Logic Apps for Continuous Integration / Continuous Delivery (CI/CD) to multiple environments using ARM Templates, when values inside the Logic App definition have to be customised per environment. If you don’t have to change values within the Logic App definition, then you might not need to follow every step of this post.

Why it’s not a good idea to use ARM template expressions inside a Logic App definition?

As I mentioned above, if when preparing you Logic Apps for CI/CD with ARM Templates, you have used ARM template expressions or functions inside a Logic App definition, you most probably have realised that it’s quite troublesome. I personally don’t like to do it that way for two reasons:

  1. Editing the Logic App definition to include ARM Template expressions or functions is not intuitive. Adding ARM Template expressions and functions to be resolved at deployment time in a way that results in Logic Apps expressions and functions to be evaluated at execution time can be messy. Things can become harder when you have string functions in a Logic Apps, like @concat() that accept values that are to be obtained from ARM template expressions, like [parameters()] or [variables()]. I’ve heard and read of many people complaining about it.
  2. Updating your Logic App after you have your ARM Template ready, requires more work. It’s not unlikely that you would need to update your Logic App after you’ve prepared the ARM Template for it. Whether you need to fix a little bug found at testing, or you are required to change or add some functionality, the chances are that you would need to update the ARM template without the help of the Logic App Editor; and if you are unlucky, changes would touch those complex ARM template expressions inside your Logic App definition. Not very fun!

So, the question is, is it possible to create ARM Templates for Logic Apps that can be parameterised for multiple environments while avoiding using ARM template expressions inside the Logic App definition? Fortunately, it is :). Below, I describe how.

Scenario

For this post, I will work with a rather simple scenario: A Logic App that is triggered when a message in a Service Bus queue is received and posts the message to an https endpoint using basic auth. The endpoint url, the username and password will be different for each environment. Additionally, the Service Bus API Connection will have to be defined per environment.

This very simple workflow created using the Logic App editor is shown below:

And the code behind this workflow is as follows:


{
"$connections": {
"value": {
"servicebus": {
"connectionId": "/subscriptions/94c0de1a-1234-4854-8b7e-c8ff07fc8888/resourceGroups/my-rg/providers/Microsoft.Web/connections/servicebus",
"connectionName": "servicebus",
"id": "/subscriptions/94c0de1a-1234-4854-8b7e-c8ff07fc8888/providers/Microsoft.Web/locations/australiasoutheast/managedApis/servicebus"
}
}
},
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"HTTP": {
"inputs": {
"authentication": {
"password": "u1tr@53cr37+dev",
"type": "Basic",
"username": "username-dev"
},
"body": "@base64ToString(triggerBody()?['ContentData'])",
"method": "POST",
"uri": "https://mysupercoolendpoint-dev.com.au/service/"
},
"runAfter": {},
"type": "Http"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {},
"triggers": {
"When_a_message_is_received_in_a_queue_(auto-complete)": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['servicebus']['connectionId']"
}
},
"method": "get",
"path": "/@{encodeURIComponent('myqueue')}/messages/head",
"queries": {
"queueType": "Main"
}
},
"recurrence": {
"frequency": "Minute",
"interval": 3
},
"type": "ApiConnection"
}
}
}
}

The code is very straight forward, but the endpoint, username and password are yet static. Not ideal for CI/CD!

Preparing the Logic App for CI/CD to be deployed to multiple environments

In this section, I’ll show how you can prepare your Logic App for CI/CD to be deployed to multiple environments using ARM Templates, without having to use any ARM Template expressions or functions inside a Logic App definition.

1. Add Logic Apps parameters to the workflow for every value that is to be changed for each environment.

Similarly to ARM Templates, the Logic App workflow definition language accepts parameters. We can use these Logic Apps parameters to prepare our Logic App definition for CI/CD. We need to add a Logic App parameter for every value that is to be tailored for each environment. Unfortunately, at the time of writing, adding Logic App parameters can only be done via the code view.

Using the code view, we need to:

  • Add the parameters definition with a default value, you should follow the same principles of parameters for ARM templates, but in this case, they are defined within the Logic App definition. The default value is the one you would use otherwise as static value at development time.
  • Update the workflow definition to use those parameters instead of the fixed values.

I’ve done this using the code view of the workflow shown above. The updated workflow definition is as follows.


{
"$connections": {
"value": {
"servicebus": {
"connectionId": "/subscriptions/94c0de1a-1234-4854-8b7e-c8ff07fc8888/resourceGroups/my-rg/providers/Microsoft.Web/connections/servicebus",
"connectionName": "servicebus",
"id": "/subscriptions/94c0de1a-1234-4854-8b7e-c8ff07fc8888/providers/Microsoft.Web/locations/australiasoutheast/managedApis/servicebus"
}
}
},
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"HTTP": {
// Fixed values are replaced by Logic Apps parameters, i.e. @parameters()
"inputs": {
"authentication": {
"password": "@{parameters('endpointPassword')}",
"type": "Basic",
"username": "@{parameters('endpointUsername')}"
},
"body": "@base64ToString(triggerBody()?['ContentData'])",
"method": "POST",
"uri": "@{parameters('endpointUrl')}"
},
"runAfter": {},
"type": "Http"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
// Logic App Parameters Definition
// Default values are only required when defining them at Development time.
// If deployed from an ARM Template, default values are not required.
// When defining Logic Apps parameters, we use the same syntax as the one used for ARM Templates,
// but within the Logic App definition.
"parameters": {
"endpointPassword": {
"defaultValue": "u1tr@53cr37+dev",
"type": "string"
},
"endpointUrl": {
"defaultValue": "https://mysupercoolendpoint-dev.com.au/service/",
"type": "string"
},
"endpointUsername": {
"defaultValue": "username-dev",
"type": "string"
}
},
"triggers": {
"When_a_message_is_received_in_a_queue_(auto-complete)": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['servicebus']['connectionId']"
}
},
"method": "get",
"path": "/@{encodeURIComponent('myqueue')}/messages/head",
"queries": {
"queueType": "Main"
}
},
"recurrence": {
"frequency": "Minute",
"interval": 3
},
"type": "ApiConnection"
}
}
}
}

After this update, at this point in time, the workflow should work just as before, but now, instead of having fixed values, you are using Logic Apps parameters with default values. If you are doing it for yours, you can test it yourself 🙂

2. Get the Logic App ARM Template for CI/CD.

Once the Logic App is ready, we can get the ARM Template for CI/CD. One easy way to do it is to use the Visual Studio Tools for Logic Apps. This requires Visual Studio 2015 or 2017, the latest Azure SDK and the Cloud Explorer. You can also use the Logic App Template Creator PowerShell module. More information on how to create ARM Templates for Logic Apps here.

The Cloud Explorer will allow you to log in to your Azure Subscription and see the supported Azure resources, including Logic Apps. When you expand the Logic Apps menu, you will see all the Logic Apps available for that subscription.

Once you’ve found the Logic App you want to export, right click on it, and click on Open with Logic App Editor. This will open the Logic App Editor on Visual Studio.

In addition to allowing to edit Logic Apps on Visual Studio, the Visual Studio Logic App Tools let you to download the ARM Template that includes the Logic App. You just need to click the Download button, and
you will get an almost ready-to-deploy ARM Template. This functionality exports the Logic App API Connections as well.

For this workflow, I got an ARM Template as follows:


{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"servicebus_1_Connection_Name": {
"type": "string",
"defaultValue": "servicebus"
},
"servicebus_1_Connection_DisplayName": {
"type": "string",
"defaultValue": "sbnamespace"
},
"servicebus_1_connectionString": {
"type": "securestring",
"metadata": {
"description": "Azure Service Bus Connection String"
}
},
"LogicAppLocation": {
"type": "string",
"minLength": 1,
"defaultValue": "australiasoutheast"
}
},
"variables": {},
"resources": [
{
"properties": {
"state": "Disabled",
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"HTTP": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "@{parameters('endpointUrl')}",
"body": "@base64ToString(triggerBody()?['ContentData'])",
"authentication": {
"type": "Basic",
"username": "@{parameters('endpointUsername')}",
"password": "@{parameters('endpointPassword')}"
}
},
"runAfter": {}
}
},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
},
"endpointPassword": {
"defaultValue": "u1tr@53cr37+dev",
"type": "String"
},
"endpointUrl": {
"defaultValue": "https://mysupercoolendpoint-dev.com.au/service/",
"type": "String"
},
"endpointUsername": {
"defaultValue": "username-dev",
"type": "String"
}
},
"triggers": {
"When_a_message_is_received_in_a_queue_(auto-complete)": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['servicebus']['connectionId']"
}
},
"method": "get",
"path": "/@{encodeURIComponent('myqueue')}/messages/head",
"queries": {
"queueType": "Main"
}
},
"recurrence": {
"frequency": "Minute",
"interval": 3
}
}
},
"contentVersion": "1.0.0.0",
"outputs": {}
},
"parameters": {
"$connections": {
"value": {
"servicebus": {
"id": "[concat(subscription().id, '/providers/Microsoft.Web/locations/', 'australiasoutheast', '/managedApis/', 'servicebus')]",
"connectionId": "[resourceId('Microsoft.Web/connections', parameters('servicebus_1_Connection_Name'))]",
"connectionName": "[parameters('servicebus_1_Connection_Name')]"
}
}
}
}
},
"name": "pacosandpit-logic-parameters",
"type": "Microsoft.Logic/workflows",
"location": "[parameters('LogicAppLocation')]",
"apiVersion": "2016-06-01",
"dependsOn": [
"[resourceId('Microsoft.Web/connections', parameters('servicebus_1_Connection_Name'))]"
]
},
{
"type": "MICROSOFT.WEB/CONNECTIONS",
"apiVersion": "2016-06-01",
"name": "[parameters('servicebus_1_Connection_Name')]",
"location": "australiasoutheast",
"properties": {
"api": {
"id": "[concat(subscription().id, '/providers/Microsoft.Web/locations/', 'australiasoutheast', '/managedApis/', 'servicebus')]"
},
"displayName": "[parameters('servicebus_1_Connection_DisplayName')]",
"parameterValues": {
"connectionString": "[parameters('servicebus_1_connectionString')]"
}
}
}
],
"outputs": {}
}

As you can see, this ARM Template includes

  • ARM Template parameters definition. This is where we define the ARM Template parameters. We can set a default value. The actual value for each environment is to be set on the ARM Parameters file.
  • Logic App parameters definition: These are declared within the definition of the Logic App. These are the ones we can define using the code view of the Logic App, as we did above.
  • Logic App parameters value set: Here is where we set the values for the parameters for the Logic App. This section is declared outside of the definition property of the Logic Apps.

The structure of the ARM Template can be seen in the picture below.

3. Set the Logic App parameters values with ARM Template expressions and functions.

Once we have the ARM Template, we can set the Logic App parameters values with ARM expressions and functions, including ARM parameters or ARM variables. I’ve done it with my ARM Template as shown below.

Before you check the updated ARM Template, some things to note:

  • I added comments to the ARM Template only to make it easier to read and understand, but I don’t recommend it. Comments are not supposed to be supported in JSON documents, however, Visual Studio and ARM Templates allow it.
  • I used the “-armparam” and “-armvar” suffixes on the ARM Template parameters and variables correspondingly. I did it only to show a clear distinction between ARM Template parameters and variables and Logic Apps parameters and variables. But the notation is sufficient (Using square brackets [] for ARM Template expressions and functions, and @ sign for those of Logic Apps).
  • I just used ARM Template parameters and variables to set the values of Logic App parameters, but you can use any other ARM Template function or expression that you might require to set Logic App parameter values.


{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
// ARM Template Parameters Definition
"parameters": {
"logicAppName": {
"type": "string"
},
"servicebus_1_Connection_Name": {
"type": "string",
"defaultValue": "servicebus"
},
"servicebus_1_Connection_DisplayName": {
"type": "string",
"defaultValue": "sbnamespace"
},
"servicebus_1_connectionString": {
"type": "securestring",
"metadata": {
"description": "Azure Service Bus Connection String"
}
},
"endpointPassword-armparam": {
"type": "securestring"
},
"endpointUrl-armparam": {
"type": "string"
},
"environmentCode": {
"type": "string"
}
},
// ARM Template Variables Definition
"variables": {
"endpointUsername-armvar": "[concat('username-', parameters('environmentCode'))]"
},
"resources": [
{
// Logic App Definition
// The Logic App Workflow Definition Language does not support comments like this.
// These will be removed when deployed by the Azure Resource Manager.
"name": "[parameters('logicAppName')]",
"type": "Microsoft.Logic/workflows",
"location": "[resourceGroup().location]",
"apiVersion": "2016-06-01",
"dependsOn": [
"[resourceId('Microsoft.Web/connections', parameters('servicebus_1_Connection_Name'))]"
],
"properties": {
"state": "Disabled",
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"HTTP": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "@{parameters('endpointUrl')}",
"body": "@base64ToString(triggerBody()?['ContentData'])",
"authentication": {
"type": "Basic",
"username": "@{parameters('endpointUsername')}",
"password": "@{parameters('endpointPassword')}"
}
},
"runAfter": {}
}
},
// Logic App Parameters Definition
// Default values are only required when defining them at Development time via Code View.
// If deployed from an ARM Template, default values are not required.
// I'm removing them so the Logic App definition is cleaner after an automated deployment.
// When defining Logic Apps parameters, we use the same syntax as the one used for ARM Templates,
// but within the Logic App definition.
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
},
"endpointPassword": {
"type": "secureString"
},
"endpointUrl": {
"type": "String"
},
"endpointUsername": {
"type": "String"
}
},
"triggers": {
"When_a_message_is_received_in_a_queue_(auto-complete)": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['servicebus']['connectionId']"
}
},
"method": "get",
"path": "/@{encodeURIComponent('myqueue')}/messages/head",
"queries": {
"queueType": "Main"
}
},
"recurrence": {
"frequency": "Minute",
"interval": 3
}
}
},
"contentVersion": "1.0.0.0",
"outputs": {}
},
// Logic App Parameters Value Set
// In this section we can use ARM expressions and functions, including parameters, variables or any
// expression like concat, coalesce, etc.
"parameters": {
"$connections": {
"value": {
"servicebus": {
"id": "[concat(subscription().id, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/', 'servicebus')]",
"connectionId": "[resourceId('Microsoft.Web/connections', parameters('servicebus_1_Connection_Name'))]",
"connectionName": "[parameters('servicebus_1_Connection_Name')]"
}
}
},
// Set the value of Logic App parameter with an ARM parameter.
"endpointPassword": {
"value": "[parameters('endpointPassword-armparam')]"
},
// Set the value of Logic App parameter with an ARM parameter.
"endpointUrl": {
"value": "[parameters('endpointUrl-armparam')]"
},
// Set the value of Logic App parameter with an ARM variable.
"endpointUsername": {
"value": "[variables('endpointUsername-armvar')]"
}
}
}
},
// API Connections
{
"type": "MICROSOFT.WEB/CONNECTIONS",
"apiVersion": "2016-06-01",
"name": "[parameters('servicebus_1_Connection_Name')]",
"location": "[resourceGroup().location]",
"properties": {
"api": {
"id": "[concat(subscription().id, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/', 'servicebus')]"
},
"displayName": "[parameters('servicebus_1_Connection_DisplayName')]",
"parameterValues": {
"connectionString": "[parameters('servicebus_1_connectionString')]"
}
}
}
],
"outputs": {}
}

As you can see, now we are only using ARM Template expressions and functions outside the Logic App definition. This is much easier to read and maintain. Don’t you think?

4. Prepare your ARM Parameters file for each environment.

Now that we have the ARM Template ready, we can prepare an ARM Parameters file for our deployment to each environment. Below I show an example of this.


{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"logicAppName": {
"value": "{logicAppName}"
},
"servicebus_1_Connection_DisplayName": {
"value": "{displayName}"
},
"servicebus_1_connectionString": {
"value": "Endpoint=sb://{namespace}.servicebus.windows.net/;SharedAccessKeyName={keyName};SharedAccessKey={key}"
},
"endpointPassword-armparam": {
"value": "{password}"
},
"endpointUrl-armparam": {
"value": "{url}"
},
"environmentCode": {
"value": "{environmentCode}"
}
}
}

5. Work on your CI/CD Pipeline.

Once we have the ARM Template and the ARM Parameter files, we can automate the deployment using our preferred tool. If you want to use VSTS, this is a good video that shows you how.

6. Deploy and enjoy.

Once you have deployed the ARM Template, you will be able to see the deployed Logic App. The Logic App parameters value set section is hidden, but if you execute it, you will see how the values have been set accordingly.

Do you want this to be easier?

You might be thinking, just as I am, that this process is not as intuitive as it should be, and is a bit time consuming. If you wish to ask the product team to improve this, you might want to vote for these user voice requests on the links below:

Wrapping Up.

In this post, I’ve shown how to prepare your Logic Apps for CI/CD to multiple environments using ARM Templates in a more convenient way, i.e. without using ARM Template expressions or functions inside the Logic App definition. I believe that this approach makes the ARM Template of a Logic App much easier to read and to maintain.

This method not only avoids the need of writing complex ARM Template expressions inside a Logic App definition, but also allows you to update your Logic App in the Designer, after this has been deployed using ARM Templates, and later update the ARM Template by simply updating the Logic App definition section. That’s much better, isn’t it?

I hope you’ve found this post handy, and it has helped you to streamline the configuration of your CI/CD pipelines when using Logic Apps.

Do you have a different preferred way of preparing your Logic Apps for CI/CD? Feel free to leave your comments or questions below,

Happy clouding and automating!

P.S. And remember: “I will never use ARM Template expressions inside a Logic App definition” 😉

Follow me on @pacodelacruz

Cross-posted on Deloitte Platform Engineering Blog

4 thoughts on “Preparing Azure Logic Apps for CI/CD to Multiple Environments

  1. […] Paco de la Cruz – Preparing Azure Logic Apps for CI/CD This blog provides some simple, but very useful, guidance on how to deal with ARM template expressions inside Logic Apps, to enable a smooth CI/CD..  To avoid problems like escaping characters or invalid Logic Apps, it’s highly advised to put ARM template expressions only inside Logic Apps parameters.  Read the complete article here. […]

    Like

  2. HI,
    can we have parameters in parameter.json file that are not used in main logic app definition file.
    I want to have single parameter file for multiple logic app arm template.

    Like

    • Hi Rakesh, the parameters in the parameters file have to match the parameters in the ARM template. The only way to reuse the same parameters file in multiple ARM Templates is to add all parameters in all ARM Templates, and use the parameters are required.

      Like

Leave a comment