Logic Apps deployment template extractor

When you have developed your Logic App and it works well in the development envrionemnt it’s time to bring it to test and later on production. If you have devloped it in Visual Studion you will find this easy as long as you don’t use any linked resources like another Logic App, Azure Function, API Management or Integration Account, cause then the the next phase starts, the phase where you manually break down the resource id’s of these actions into concat statements with parameters and arm template functions.

So how does it look like? Let’s start at an innocent Workflow action, a call to another Logic App, in the designer it looks like this (both web designer and VS)

If we take a look at this from the code view, it looks as following:

"INT001_Work_Order": {
    "inputs": {
        "body": "@triggerBody()",
        "host": {
            "triggerName": "manual",
            "workflow": {
                "id": "/subscriptions/fake89f1-660a-4585-b4d8-bf5172cb7f70/resourceGroups/integration-we/providers/Microsoft.Logic/workflows/INT001_Work_Order"
            }
        },
        "retryPolicy": {
            "type": "None"
        }
    },
    "runAfter": {},
    "type": "Workflow"
}

Id is a reference to the Logic App via it’s resource id, so the id in the field bellow is pointing exactly to that other Logic App. Let’s look in to the field:

"/subscriptions/fake89f1-660a-4585-b4d8-bf5172cb7f70/resourceGroups/integration-we/providers/Microsoft.Logic/workflows/INT001_Work_Order"

The harcoded parts is the id of the subscription fake89f1-660a-4585-b4d8-bf5172cb7f70, the resource group name integration-weand the Logic App name INT001_Work_Order. In order to make this possible to relate to in other subscriptions we need to remove the “hard codings” of subscription and resrource group, using arm template language read more at: https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-authoring-templates We will use the subscription object to get the current subscription we are executing and it’s id via the subscriptionId property and the same for the resource group name via the resourceGroup object and the name property when then get the values for the current subscription and resource group we are deploying to.

It will look like this (I assume the logic app name will be same in the next environment).

"[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', resourceGroup().name,'/providers/Microsoft.Logic/workflows/INT001_Work_Order')]"

This can now be deployed to a new subscription and if the INT001_Work_Order Logic App exists it will be referenced automatically, otherwise the deloyment will fail.

Same goes for functions, if we add them alot of information is put in to this id property and put there “hard coded”.

"TransformKNABContactToGeneric": {
    "inputs": {
        "body": "@body('HTTP_Webhook')",
        "function": {
            "id": "/subscriptions/fake89f1-660a-4585-b4d8-bf5172cb7f70/resourceGroups/integration-we/providers/Microsoft.Web/sites/functionappname/functions/INT004-TransformContactToGeneric"
        }
    },
    "runAfter": {},
    "type": "Function"
}

It’s just as with the Logic App reference…

"/subscriptions/fake89f1-660a-4585-b4d8-bf5172cb7f70/resourceGroups/integration-we/providers/Microsoft.Web/sites/functionappname/functions/INT004-TransformContactToGeneric"

As before we need to update it and it should be to something like this, note I’ve added a parameter for the function app container name, this wwill be diffrent in diffrent environments and to be on the safe side we added a parameter for the function name.

"[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', resourceGroup().name,'/providers/Microsoft.Web/sites/', parameters('FunctionAppName'),'/functions/', parameters('FunctionName'),'')]"

Api Management usage is the same, but here we also have some more things to consider instance name and the subscriptionkey (as experience says it’s easy to forgett adding a parameter as is needed to get a smooth deployment)

"UploadAttachment": {
  "runAfter": {},
  "type": "ApiManagement",
  "inputs": {
    "api": { "id": "/subscriptions/FAKE9f1-660a-4585-b4d8-bf5172cb7f70/resourceGroups/Api-Default-West-Europe/providers/Microsoft.ApiManagement/service/apiminstancename/apis/58985740990a990dd41e5392" },
    "body": {
      "attachment": {
        "data": "@{triggerBody()['data']}",
        "extension": "@{triggerBody()['extension']}",
        "filename": "@{triggerBody()['filename']}",
        "orderNumber": "@{triggerBody()['orderNumber']}"
      }
    },
    "method": "post",
    "pathTemplate": {
      "parameters": {},
      "template": "/api/v1/test/rest/UploadAttachment"
    },
    "subscriptionKey": "keytothesubscription"
  }
} 

so in this case we would need to do something like this to make a smooth deployment, note the paramter for instance name and apiid just to be safe.

 "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', parameters('apimResourceGroup'),'/providers/Microsoft.ApiManagement/service/', parameters('apimInstanceName'),'/apis/', parameters('apimApiId'),'')]"
 ..
 "subscriptionKey": "[parameters('subscriptionkey')]"

So as you can see there are som things to do here, and if you are as me a fan of the Web desginer after the development the next step in order to get this into VSTS would be to start building the arm template by hand.. I’ve found this very time consuming and error prone, so easy to make misstakes and it takes time to test the deployment. Also this often changes the Logic App abit and that might not be so good if test’s has been done on the implementation.

So in order to automate all this and more, i.e. pushing parameters in the logic app to the ARM template making it possible to change the parameters with a parameter file between environments. We are using the Logic App Template Creator created by Jeff Holan https://github.com/jeffhollan/LogicAppTemplateCreator to automate this, it’s even possible to create the parameter file. With this we can extract the Logic App, setup parameters for TEST/PROD and add it to VSTS and start deploying directly. Note that Gateway’s and their deployment are not supported yet and if you find bugs, help out or report them so we can fix them togheter.

The Template Generator will automate this and do the following:

Logic App (workflow)

In addition to fix the id to a generic concat path it also adds a parameters at the top if the referenced Logic App is in another resource group.

Logic App in the same resource group (assumes that in the next enviornment it will be the same)

"INT001_Work_Order": {
  "runAfter": {},
  "type": "Workflow",
  "inputs": {
    "body": "@triggerBody()",
    "host": {
      "triggerName": "manual",
      "workflow": {
        "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', resourceGroup().name,'/providers/Microsoft.Logic/workflows/INT001_Work_Order')]"
      }
    },
    "retryPolicy": {
      "type": "None"
    }
  }
}

Logic app not in same resource group, a parameter is added for the resource group name

"parameters": {
...
"INT0014-NewHires-ResourceGroup": {
      "type": "string",
      "defaultValue": "resourcegroupname2"
    }
...
}
...
"INT0014-NewHires": {
  "runAfter": {
    "Parse_JSON": [
      "Succeeded"
    ]
  },
  "type": "Workflow",
  "inputs": {
    "body": {
      "Completed_Date_On_or_After": "@{body('Parse_JSON')?['Completed_Date_On_or_After']}",
      "Completed_Date_On_or_Before": "@{body('Parse_JSON')?['Completed_Date_On_or_Before']}",
      "Event_Effective_Date_On_or_After": "@{body('Parse_JSON')?['Event_Effective_Date_On_or_After']}",
      "Event_Effective_Date_On_or_Before": "@{body('Parse_JSON')?['Event_Effective_Date_On_or_Before']}"
    },
    "host": {
      "triggerName": "manual",
      "workflow": {
        "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', parameters('INT0014-NewHires-ResourceGroup'),'/providers/Microsoft.Logic/workflows/INT0014-NewHires')]"
      }
    }
  }
}

Azure Function

Azure function are referenced via a function app and at the last the part the function name so all these parameters need’s to be parameters, also here we have the same function as with the Logic around the resource group.

"parameters": {
...
 "TransformContactToGeneric-FunctionApp": {
      "type": "string",
      "defaultValue": "functionAppName"
    },
    "TransformContactToGeneric-FunctionName": {
      "type": "string",
      "defaultValue": "INT004-TransContactToGeneric"
    }
...
 "TransformContactToGeneric": {
  "runAfter": {},
  "type": "Function",
  "inputs": {
    "body": "@body('HTTP_Webhook')",
    "function": {
      "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', resourceGroup().name,'/providers/Microsoft.Web/sites/', parameters('TransformContactToGeneric-FunctionApp'),'/functions/', parameters('TransformContactToGeneric-FunctionName'),'')]"
    }
  }
}

API Managemenmt

API Management and API’s are referenced at the same way as all other so we need the same handling. Here it’s also important to be able to set the instance namen and sunscription key since they will change in the next environment.

"parameters": {
...
"apimResourceGroup": {
      "type": "string",
      "defaultValue": "integration-we"
    },
    "apimInstanceName": {
      "type": "string",
      "defaultValue": "apiminstancename"
    },
    "apimApiId": {
      "type": "string",
      "defaultValue": "58778a86990a990f5c794e48"
    },
    "apimSubscriptionKey": {
      "type": "string",
      "defaultValue": "mysecretsubscriptionkey"
    }
...
"INT005_Add_Attachment": {
  "runAfter": {},
  "type": "ApiManagement",
  "inputs": {
    "api": {
      "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', parameters('apimResourceGroup'),'/providers/Microsoft.ApiManagement/service/', parameters('apimInstanceName'),'/apis/', parameters('apimApiId'),'')]"
    },
    "body": {
      "fileextension": "@{triggerBody()['fileextension']}",
      "filename": "@{triggerBody()?['filename']}",
      "image64": "@triggerBody()?['image64']",
      "workorderno": "@{triggerBody()?['workorderno']}"
    },
    "method": "post",
    "pathTemplate": {
      "parameters": {},
      "template": "/test/addattachment"
    },
    "subscriptionKey": "[parameters('apimSubscriptionKey')]"
  }
}

Integration Account

When working with Integration Account’s these need to be set in the next environment aswell so if they are used there will be some parameters and settings added.

"parameters": {
...
"IntegrationAccountName": {
  "type": "string",
  "metadata": {
    "description": "Name of the Integration Account that should be connected to this Logic App."
  },
  "defaultValue": "myiaaccountname"
},
"IntegrationAccountResourceGroupName": {
  "type": "string",
  "metadata": {
    "description": "The resource group name that the Integration Account are in"
  },
  "defaultValue": "[resourceGroup().name]"
}
...
"integrationAccount": {
  "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourcegroups/',parameters('IntegrationAccountResourceGroupName'),'/providers/Microsoft.Logic/integrationAccounts/',parameters('IntegrationAccountName'))]"
}

Parameters

Parameters added inside the Logic App via for now Code View will be pushed and added as a ARM template parameter, so if you have this parameter inside your Logic App.

"definition": {
  "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "MyURL": {
      "defaultValue": "http://requestb.in",
      "type": "String"
    }
  },

After running the extractor this parameter will be a ARM template parameter and the defaultValue will be set to the value of that ARM template parameter, so now this can be set at deployment time via the parameters file.

"parameters": {
...
"paramMyURL": {
  "type": "string",
  "defaultValue": "http://requestb.in"
},
...

"definition": {
  "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "MyURL": {
      "defaultValue": "[parameters('paramMyURL')]",
      "type": "String"
    }
  },

We are currently working on more, like trigger functionality to get frequency, intervall settings put up. And also functions to handle special triggers like file, where the folder path is base64 encoded and built up in a special way.

Be aware that right now the Gateway Connectors are strangely removing the user/pass if they are not supplied so they will break, remove them manually from the template and create them manually for now. A fix is comming to not include them. But for now hope this helps out!

Here is a sample of a powershell script that will take out the Logic App, generate a template and create a parameter file

#if you have problem with execution policy execute this in a administrator runned powershell window.
#Set-ExecutionPolicy -ExecutionPolicy Unrestricted

#we have th module in a folder structure like this and it's needed to be included
Import-Module ".\LogicAppTemplateCreator\LogicAppTemplate.dll"

#Credentials will "flow" if you are logged in.


#Set the name of the Logic App
$logicapname = 'INT001_Work_Order'
#Set the resource group of the Logic App
$resourcegroupname = 'integration-we'
#Set the subscription id of where the Logic App is
$subscriptionid = 'fakeb73-d0ff-455d-a2bf-eae0b300696d'
#Set the tenant to use with the Logic App, make sure it has the right tennant
$tenant = 'ibiz-solutions.se'
 
#setting the output filename
$filenname = $logicapname + '.json'
$filennameparam = $logicapname + '.parameters.json'


Get-LogicAppTemplate -LogicApp $logicapname -ResourceGroup $resourcegroupname -SubscriptionId $subscriptionid -TenantName $tenant | Out-File $filenname
#$filenname = $PSScriptRoot+"\"+$filenname
Get-ParameterTemplate -TemplateFile $filenname | Out-File $filennameparam

Sample of complete extraction INT001_Work_Order:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "logicAppName": {
      "type": "string",
      "defaultValue": "INT001_Work_Order",
      "metadata": {
        "description": "Name of the Logic App."
      }
    },
    "logicAppLocation": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]",
      "allowedValues": [
        "eastasia",
        "southeastasia",
        "centralus",
        "eastus",
        "eastus2",
        "westus",
        "northcentralus",
        "southcentralus",
        "northeurope",
        "westeurope",
        "japanwest",
        "japaneast",
        "brazilsouth",
        "australiaeast",
        "australiasoutheast",
        "westcentralus",
        "westus2"
      ],
      "metadata": {
        "description": "Location of the Logic App."
      }
    },
    "apimResourceGroup": {
      "type": "string",
      "defaultValue": "integration-we"
    },
    "apimInstanceName": {
      "type": "string",
      "defaultValue": "instancename"
    },
    "apimApiId": {
      "type": "string",
      "defaultValue": "5833f8a9990a991064e0e128"
    },
    "apimSubscriptionKey": {
      "type": "string",
      "defaultValue": "fakekeyis here"
    },
    "apimApiId2": {
      "type": "string",
      "defaultValue": "58778a86990a990f5c794e48"
    }
  },
  "variables": {},
  "resources": [
    {
      "type": "Microsoft.Logic/workflows",
      "apiVersion": "2016-06-01",
      "name": "[parameters('logicAppName')]",
      "location": "[parameters('logicAppLocation')]",
      "dependsOn": [],
      "properties": {
        "definition": {
          "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
          "contentVersion": "1.0.0.0",
          "parameters": {},
          "triggers": {
            "manual": {
              "type": "Request",
              "kind": "Http",
              "inputs": {
                "schema": {}
              }
            }
          },
          "actions": {
            "Check_assigned_to": {
              "actions": {
                "Service_Order": {
                  "runAfter": {},
                  "type": "ApiManagement",
                  "inputs": {
                    "api": {
                      "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', parameters('apimResourceGroup'),'/providers/Microsoft.ApiManagement/service/', parameters('apimInstanceName'),'/apis/', parameters('apimApiId'),'')]"
                    },
                    "body": "@concat(replace(replace(string(triggerBody()),'<?xml version=\"1.0\" encoding=\"utf-8\"?>',''),'<workOrders ','<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:a=\"http://www.w3.org/2005/08/addressing\"><s:Header><a:Action s:mustUnderstand=\"1\">Submit</a:Action></s:Header><s:Body><workOrders xmlns=\"pv-work-order\" '),'</s:Body></s:Envelope>')",
                    "method": "post",
                    "pathTemplate": {
                      "parameters": {},
                      "template": "/external/Relacom/ServiceOrder/"
                    },
                    "subscriptionKey": "[parameters('apimSubscriptionKey')]"
                  }
                }
              },
              "runAfter": {
                "Compose": [
                  "Succeeded"
                ]
              },
              "expression": "@equals(outputs('Compose')['assigned'], 'RELACOM')",
              "type": "If"
            },
            "INT005_Add_Attachment": {
              "runAfter": {
                "INT001_Create_Order": [
                  "Succeeded"
                ]
              },
              "type": "Workflow",
              "inputs": {
                "body": {
                  "company": null,
                  "fileextension": "xml",
                  "filename": "@concat(concat(xpath(xml(triggerbody()), 'string(/*[local-name()=\"workOrders\"]/*[local-name()=\"workOrder\"]/*[local-name()=\"code\"])'),'_'),xpath(xml(triggerbody()), 'string(/*[local-name()=\"workOrders\"]/*[local-name()=\"header\"]/*[local-name()=\"messageDate\"])'))",
                  "id": null,
                  "image64": "@base64(triggerbody())",
                  "workorderno": "@xpath(xml(triggerbody()), 'string(/*[local-name()=\"workOrders\"]/*[local-name()=\"workOrder\"]/*[local-name()=\"code\"])')"
                },
                "host": {
                  "triggerName": "manual",
                  "workflow": {
                    "id": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/', resourceGroup().name,'/providers/Microsoft.Logic/workflows/INT005_Add_Attachment')]"
                  }
                }
              }
            },
            "Response": {
              "runAfter": {
                "INT005_Add_Attachment": [
                  "Succeeded"
                ]
              },
              "type": "Response",
              "inputs": {
                "body": "OK",
                "statusCode": 200
              }
            }
          },
          "outputs": {}
        },
        "parameters": {}
      }
    }
  ],
  "outputs": {}
}

Sample of the parameters file generated:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "logicAppName": {
      "value": "INT001_Work_Order"
    },
    "logicAppLocation": {
      "value": "[resourceGroup().location]"
    },
    "apimResourceGroup": {
      "value": "integration-we"
    },
    "apimInstanceName": {
      "value": "instancename"
    },
    "apimApiId": {
      "value": "5833f8a9990a991064e0e128"
    },
    "apimSubscriptionKey": {
      "value": "fakekeyis here"
    },
    "apimApiId2": {
      "value": "58778a86990a990f5c794e48"
    }
  }
}

As you can see this will handle all of the nessisary concat setups, it automatically add’s paramters for the import things like the API Management instance name and subscription key to make sure the parameters are not forgotten. This makes it better and easier to use the Web designer rather than VS since in VS you still need to this work or export it afterwards (but then what is the point in using VS?). With this it’s possible to just develop in the Web designer as I prefer and then extract the template, verify it and update with parameters (prefereable in the Logic App so you can reextract the template to make sure all is working before the deployment) and put into VSTS for deployment.

I strongly recomend you to try it.

https://github.com/jeffhollan/LogicAppTemplateCreator

Posted in: •Logic Apps  •Integration  | Tagged: •DTAP  •Logic Apps  •Deployments  •Azure  •ARM  •ARM Template