Overview
Azure Logic Apps are a vital part of many cloud-based automations and workflows. However, exporting these Logic Apps—including their connections—for reuse or deployment in another environment can be a tedious manual process. This blog post introduces a PowerShell script designed to simplify that task by generating an ARM (Azure Resource Manager) template that includes both the Logic App and its required connections.
The script is particularly useful for DevOps engineers, cloud architects, and administrators who frequently manage Infrastructure as Code (IaC) or need to migrate Logic Apps across environments.
Script Synopsis
This PowerShell script allows users to:
- Connect to an Azure tenant
- Select subscriptions and Logic Apps interactively
- Automatically generate ARM templates including:
- The Logic App definition
- All associated API connections
- Output a clean and formatted ARM template JSON file
- Optionally prepare the template for publishing to the Azure Sentinel Playbook Gallery
The script is authored by Sreedhar Ande and Itai Yankelevsky and is distributed under an open disclaimer (“AS IS” without warranty or liability).
Script
THE SCRIPT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SCRIPT OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
.SYNOPSIS
This PowerShell script generates ARM Template for Azure LogicApp with all the connections
.DESCRIPTION
Generates ARM Template for Azure LogicApp with all the connections
.PARAMETER TenantID
Enter the TenantID (required)
.PARAMETER GenerateForGallery
Enter the boolean - True or False
.NOTES
AUTHOR: Sreedhar Ande, Itai Yankelevsky
LASTEDIT: 4-15-2022
.EXAMPLE
.\GenerateARMTemplate_V2 -TenantID xxxx -GenerateForGallery true
#>
param(
[parameter(Mandatory = $true, HelpMessage = "Enter the Tenant Id")]
[string]$TenantID
)
#region HelperFunctions
Function Write-Log {
<#
.DESCRIPTION
Write-Log is used to write information to a log file and to the console.
.PARAMETER Severity
parameter specifies the severity of the log message. Values can be: Information, Warning, or Error.
#>
[CmdletBinding()]
param(
[parameter()]
[ValidateNotNullOrEmpty()]
[string]$Message,
[string]$LogFileName,
[parameter()]
[ValidateNotNullOrEmpty()]
[ValidateSet('Information', 'Warning', 'Error')]
[string]$Severity = 'Information'
)
# Write the message out to the correct channel
switch ($Severity) {
"Information" { Write-Host $Message -ForegroundColor Green }
"Warning" { Write-Host $Message -ForegroundColor Yellow }
"Error" { Write-Host $Message -ForegroundColor Red }
}
try {
[PSCustomObject] [ordered] @{
Time = (Get-Date -f g)
Message = $Message
Severity = $Severity
} | Export-Csv -Path "$PSScriptRoot\$LogFileName" -Append -NoTypeInformation -Force
}
catch {
Write-Error "An error occurred in Write-Log() method" -ErrorAction SilentlyContinue
}
}
Function Get-RequiredModules {
<#
.DESCRIPTION
Get-Required is used to install and then import a specified PowerShell module.
.PARAMETER Module
parameter specifices the PowerShell module to install.
#>
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] $Module
)
try {
$installedModule = Get-InstalledModule -Name $Module -ErrorAction SilentlyContinue
if ($null -eq $installedModule) {
Write-Log -Message "The $Module PowerShell module was not found" -LogFileName $LogFileName -Severity Warning
#check for Admin Privleges
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
#Not an Admin, install to current user
Write-Log -Message "Can not install the $Module module. You are not running as Administrator" -LogFileName $LogFileName -Severity Warning
Write-Log -Message "Installing $Module module to current user Scope" -LogFileName $LogFileName -Severity Warning
Install-Module -Name $Module -Scope CurrentUser -Repository PSGallery -Force -AllowClobber
Import-Module -Name $Module -Force
}
else {
#Admin, install to all users
Write-Log -Message "Installing the $Module module to all users" -LogFileName $LogFileName -Severity Warning
Install-Module -Name $Module -Repository PSGallery -Force -AllowClobber
Import-Module -Name $Module -Force
}
}
else {
if ($UpdateAzModules) {
Write-Log -Message "Checking updates for module $Module" -LogFileName $LogFileName -Severity Information
$currentVersion = [Version](Get-InstalledModule | Where-Object {$_.Name -eq $Module}).Version
# Get latest version from gallery
$latestVersion = [Version](Find-Module -Name $Module).Version
if ($currentVersion -ne $latestVersion) {
#check for Admin Privleges
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
#install to current user
Write-Log -Message "Can not update the $Module module. You are not running as Administrator" -LogFileName $LogFileName -Severity Warning
Write-Log -Message "Updating $Module from [$currentVersion] to [$latestVersion] to current user Scope" -LogFileName $LogFileName -Severity Warning
Update-Module -Name $Module -RequiredVersion $latestVersion -Force
}
else {
#Admin - Install to all users
Write-Log -Message "Updating $Module from [$currentVersion] to [$latestVersion] to all users" -LogFileName $LogFileName -Severity Warning
Update-Module -Name $Module -RequiredVersion $latestVersion -Force
}
}
else {
$latestVersion = [Version](Get-Module -Name $Module).Version
Write-Log -Message "Importing module $Module with version $latestVersion" -LogFileName $LogFileName -Severity Information
Import-Module -Name $Module -RequiredVersion $latestVersion -Force
}
}
else {
# Get latest version
$latestVersion = [Version](Get-Module -Name $Module).Version
Write-Log -Message "Importing module $Module with version $latestVersion" -LogFileName $LogFileName -Severity Information
Import-Module -Name $Module -RequiredVersion $latestVersion -Force
}
}
# Install-Module will obtain the module from the gallery and install it on your local machine, making it available for use.
# Import-Module will bring the module and its functions into your current powershell session, if the module is installed.
}
catch {
Write-Log -Message "An error occurred in Get-RequiredModules() method - $($_)" -LogFileName $LogFileName -Severity Error
}
}
Function Get-FolderName {
Add-Type -AssemblyName System.Windows.Forms
$FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$FolderBrowser.Description = 'Select the folder containing the data'
Try {
$result = $FolderBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true; TopLevel = $true }))
if ($result -eq [Windows.Forms.DialogResult]::OK){
return $FolderBrowser.SelectedPath
}
}
catch {
Write-Log -Message "Error occured in Get-FolderName :$($_)" -LogFileName $LogFileName -Severity Error
exit
}
} #end function Get-FolderName
function Confirmation-Dlg {
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] $DlgMessage
)
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$logselectform = New-Object System.Windows.Forms.Form
$logselectform.Text = 'Confirmation'
$logselectform.AutoSize = $false
$logselectform.Size = New-Object System.Drawing.Size(450,360)
$logselectform.StartPosition = 'CenterScreen'
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(10,20)
$label.Size = New-Object System.Drawing.Size(350,120)
$label.AutoSize = $false
$label.Text = $DlgMessage
$logselectform.Controls.Add($label)
$okb = New-Object System.Windows.Forms.Button
$okb.Location = New-Object System.Drawing.Point(165,225)
$okb.Size = New-Object System.Drawing.Size(75,25)
$okb.Text = 'Ok'
$okb.DialogResult = [System.Windows.Forms.DialogResult]::OK
$logselectform.AcceptButton = $okb
$logselectform.Controls.Add($okb)
$logselectform.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true; TopLevel = $true }))
}
#endregion
#region MainFunctions
# Taken from https://stackoverflow.com/questions/56322993/proper-formating-of-json-using-powershell/56324939
Function FixJsonIndentation ($jsonOutput) {
Try {
$currentIndent = 0
$tabSize = 4
$lines = $jsonOutput.Split([Environment]::NewLine)
$newString = ""
foreach ($line in $lines)
{
# skip empty line
if ($line.Trim() -eq "") {
continue
}
# if the line with ], or }, reduce indent
if ($line -match "[\]\}]+\,?\s*$") {
$currentIndent -= 1
}
# add the line with the right indent
if ($newString -eq "") {
$newString = $line
} else {
$spaces = ""
$matchFirstChar = [regex]::Match($line, '[^\s]+')
$totalSpaces = $currentIndent * $tabSize
if ($totalSpaces -gt 0) {
$spaces = " " * $totalSpaces
}
$newString += [Environment]::NewLine + $spaces + $line.Substring($matchFirstChar.Index)
}
# if the line with { or [ increase indent
if ($line -match "[\[\{]+\s*$") {
$currentIndent += 1
}
}
return $newString
}
catch {
Write-Log -Message "Error occured in FixJsonIndentation :$($_)" -LogFileName $LogFileName -Severity Error
}
}
Function BuildPlaybookArmId() {
Try {
if ($PlaybookSubscriptionId -and $PlaybookResourceGroupName -and $PlaybookResourceName) {
return "/subscriptions/$PlaybookSubscriptionId/resourceGroups/$PlaybookResourceGroupName/providers/Microsoft.Logic/workflows/$PlaybookResourceName"
}
}
catch {
Write-Log -Message "Playbook full ARM id, or subscription, resource group and resource name are required: $($_)" -LogFileName $LogFileName -Severity Error
}
}
Function SendArmGetCall($relativeUrl) {
$authHeader = @{
'Authorization'='Bearer ' + $tokenToUse
}
$absoluteUrl = $armHostUrl+$relativeUrl
Try {
$result = Invoke-RestMethod -Uri $absoluteUrl -Method Get -Headers $authHeader
Write-Log -Message $result -LogFileName $LogFileName -Severity Information
return $result
}
catch {
Write-Log -Message $($_.Exception.Response.StatusCode.value__) -LogFileName $LogFileName -Severity Error
Write-Log -Message $($_.Exception.Response.StatusDescription) -LogFileName $LogFileName -Severity Error
}
}
Function GetPlaybookResource() {
Try {
$playbookArmIdToUse = BuildPlaybookArmId
$playbookResource = SendArmGetCall -relativeUrl "$($playbookArmIdToUse)?api-version=2017-07-01"
$PlaybookARMParameters.Add("PlaybookName", [ordered] @{
"defaultValue"= $playbookResource.Name
"type"= "string"
})
# Update properties to fit ARM template structure
if ($GenerateForGallery) {
if (!("tags" -in $playbookResource.PSobject.Properties.Name)) {
Add-Member -InputObject $playbookResource -Name "tags" -Value @() -MemberType NoteProperty -Force
}
if (!$playbookResource.tags) {
$playbookResource.tags = [ordered] @{
"hidden-SentinelTemplateName"= $playbookResource.name
"hidden-SentinelTemplateVersion"= "1.0"
}
}
else {
if (!$playbookResource.tags["hidden-SentinelTemplateName"]) {
Add-Member -InputObject $playbookResource.tags -Name "hidden-SentinelTemplateName" -Value $playbookResource.name -MemberType NoteProperty -Force
}
if (!$playbookResource.tags["hidden-SentinelTemplateVersion"]) {
Add-Member -InputObject $playbookResource.tags -Name "hidden-SentinelTemplateVersion" -Value "1.0" -MemberType NoteProperty -Force
}
}
# The azuresentinel connection will use MSI when exported for the gallery, so the playbook must support it too
if ($playbookResource.identity.type -ne "SystemAssigned") {
if (!$playbookResource.identity) {
Add-Member -InputObject $playbookResource -Name "identity" -Value @{
"type"= "SystemAssigned"
} -MemberType NoteProperty -Force
}
else {
$playbookResource.identity = @{
"type"= "SystemAssigned"
}
}
}
}
$playbookResource.PSObject.Properties.remove("id")
$playbookResource.location = "[resourceGroup().location]"
$playbookResource.name = "[parameters('PlaybookName')]"
Add-Member -InputObject $playbookResource -Name "apiVersion" -Value "2017-07-01" -MemberType NoteProperty
Add-Member -InputObject $playbookResource -Name "dependsOn" -Value @() -MemberType NoteProperty
# Remove properties specific to an instance of a deployed playbook
$playbookResource.properties.PSObject.Properties.remove("createdTime")
$playbookResource.properties.PSObject.Properties.remove("changedTime")
$playbookResource.properties.PSObject.Properties.remove("version")
$playbookResource.properties.PSObject.Properties.remove("accessEndpoint")
$playbookResource.properties.PSObject.Properties.remove("endpointsConfiguration")
if ($playbookResource.identity) {
$playbookResource.identity.PSObject.Properties.remove("principalId")
$playbookResource.identity.PSObject.Properties.remove("tenantId")
}
return $playbookResource
}
Catch {
Write-Log -Message "Error occured in GetPlaybookResource :$($_)" -LogFileName $LogFileName -Severity Error
}
}
Function HandlePlaybookApiConnectionReference($apiConnectionReference, $playbookResource) {
Try {
$connectionName = $apiConnectionReference.Name
$connectionName = $connectionName.Split('_')[0].ToString().Trim()
$connectionName = (Get-Culture).TextInfo.ToTitleCase($connectionName)
if ($connectionName -ieq "azuresentinel") {
$connectionVariableName = "MicrosoftSentinelConnectionName"
$templateVariables.Add($connectionVariableName, "[concat('MicrosoftSentinel-', parameters('PlaybookName'))]")
} else {
$connectionVariableName = "$($connectionName)ConnectionName"
$templateVariables.Add($connectionVariableName, "[concat('$connectionName-', parameters('PlaybookName'))]")
}
$connectorType = if ($apiConnectionReference.Value.id.ToLowerInvariant().Contains("/managedapis/")) { "managedApis" } else { "customApis" }
$connectionAuthenticationType = if ($apiConnectionReference.Value.connectionProperties.authentication.type -eq "ManagedServiceIdentity") { "Alternative" } else { $null }
# We always convert azuresentinel connections to MSI during export
if ($GenerateForGallery -and $connectionName -eq "azuresentinel" -and !$connectionAuthenticationType) {
$connectionAuthenticationType = "Alternative"
if (!$apiConnectionReference.Value.ConnectionProperties) {
Add-Member -InputObject $apiConnectionReference.Value -Name "ConnectionProperties" -Value @{} -MemberType NoteProperty
}
$apiConnectionReference.Value.connectionProperties = @{
"authentication"= @{
"type"= "ManagedServiceIdentity"
}
}
}
try {
$existingConnectionProperties = SendArmGetCall -relativeUrl "$($apiConnectionReference.Value.connectionId)?api-version=2016-06-01"
}
catch {
$existingConnectionProperties = $null
}
$existingConnectorProperties = SendArmGetCall -relativeUrl "$($apiConnectionReference.Value.id)?api-version=2016-06-01"
# Create API connection resource
$apiConnectionResource = [ordered] @{
"type"= "Microsoft.Web/connections"
"apiVersion"= "2016-06-01"
"name"= "[variables('$connectionVariableName')]"
"location"= "[resourceGroup().location]"
"kind"= "V1"
"properties"= [ordered] @{
"displayName"= "[variables('$connectionVariableName')]"
"customParameterValues"= [ordered] @{}
"parameterValueType"= $connectionAuthenticationType
"api"= [ordered] @{
"id"= "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/$connectorType/$connectionName')]"
}
}
}
if (!$apiConnectionResource.properties.parameterValueType) {
$apiConnectionResource.properties.Remove("parameterValueType")
}
$apiConnectionResources.Add($apiConnectionResource) | Out-Null
# Update API connection reference in the playbook resource
$apiConnectionReference.Value = [ordered] @{
"connectionId"= "[resourceId('Microsoft.Web/connections', variables('$connectionVariableName'))]"
"connectionName" = "[variables('$connectionVariableName')]"
"id" = "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/$connectorType/$connectionName')]"
"connectionProperties" = $apiConnectionReference.Value.connectionProperties
}
if (!$apiConnectionReference.Value.connectionProperties) {
$apiConnectionReference.Value.Remove("connectionProperties")
}
$playbookResource.dependsOn += "[resourceId('Microsoft.Web/connections', variables('$connectionVariableName'))]"
# Evaluate and add connection-specific parameters
Foreach ($connectorParameter in $existingConnectorProperties.properties.connectionAlternativeParameters.PSObject.Properties) {
if ($connectorParameter.Name -eq "authentication" -or $connectorParameter -match "token:") {
continue
}
$matchingConnectionValue = $existingConnectionProperties.properties.alternativeParameterValues.$($connectorParameter.Name)
$templateParameters.Add($connectorParameter.Name, [ordered] @{
"defaultValue"= $matchingConnectionValue
"type"= $connectorParameter.Value.type
})
}
}
Catch {
Write-Log -Message "Error occured in HandlePlaybookApiConnectionReference :$($_)" -LogFileName $LogFileName -Severity Error
}
}
Function BuildArmTemplate($playbookResource) {
Try {
$armTemplate = [ordered] @{
'$schema'= "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"
"contentVersion"= "1.0.0.0"
"parameters"= $PlaybookARMParameters
"variables"= $templateVariables
"resources"= @($playbookResource)+$apiConnectionResources
}
if ($GenerateForGallery) {
$armTemplate.Insert(2, "metadata", [ordered] @{
"title"= ""
"description"= ""
"prerequisites"= ""
"postDeployment" = @()
"prerequisitesDeployTemplateFile"= ""
"lastUpdateTime"= ""
"entities"= @()
"tags"= @()
"support"= [ordered] @{
"tier"= "community"
"armtemplate" = "Generated from https://github.com/Azure/Azure-Sentinel/tree/master/Tools/Playbook-ARM-Template-Generator"
}
"author"= @{
"name"= ""
}
})
}
return $armTemplate
}
Catch {
Write-Log -Message "Error occured in BuildArmTemplate :$($_)" -LogFileName $LogFileName -Severity Error
}
}
#endregion
#region DriverProgram
$TemplateGalleryQuestion = "Generate ARM Template for Gallery?"
$TemplateGalleryQuestionChoices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$TemplateGalleryQuestionChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$TemplateGalleryQuestionChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$TemplateGalleryQuestionDecision = $Host.UI.PromptForChoice($title, $TemplateGalleryQuestion, $TemplateGalleryQuestionChoices, 0)
if ($TemplateGalleryQuestionDecision -eq 0) {
$GenerateForGallery = $true
}
else {
$GenerateForGallery = $false
}
$AzModulesQuestion = "Do you want to update required Az Modules to latest version?"
$AzModulesQuestionChoices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$AzModulesQuestionChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$AzModulesQuestionChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$AzModulesQuestionDecision = $Host.UI.PromptForChoice($title, $AzModulesQuestion, $AzModulesQuestionChoices, 1)
if ($AzModulesQuestionDecision -eq 0) {
$UpdateAzModules = $true
}
else {
$UpdateAzModules = $false
}
Get-RequiredModules("Az.Accounts")
Get-RequiredModules("Az.Resources")
Get-RequiredModules("Az.OperationalInsights")
# Check Powershell version, needs to be 5 or higher
if ($host.Version.Major -lt 5) {
Write-Log -Message "Supported PowerShell version for this script is 5 or above" -LogFileName $LogFileName -Severity Error
exit
}
$TimeStamp = Get-Date -Format yyyyMMdd_HHmmss
$LogFileName = '{0}_{1}.csv' -f "ARMTemplateGenerator", $TimeStamp
# Load Assembly
Add-Type -AssemblyName System.Windows.Forms
#disconnect exiting connections and clearing contexts.
Write-Log -Message "Clearing existing Azure connection" -LogFileName $LogFileName -Severity Information
$null = Disconnect-AzAccount -ContextName 'MyAzContext' -ErrorAction SilentlyContinue
Write-Log -Message "Clearing existing Azure context `n" -LogFileName $LogFileName -Severity Information
get-azcontext -ListAvailable | ForEach-Object{$_ | remove-azcontext -Force -Verbose | Out-Null} #remove all connected content
Write-Log -Message "Clearing of existing connection and context completed." -LogFileName $LogFileName -Severity Information
Try {
#Connect to tenant with context name and save it to variable
Connect-AzAccount -Tenant $TenantID -ContextName 'MyAzContext' -Force -ErrorAction Stop
#Select subscription to build
$GetSubscriptions = Get-AzSubscription -TenantId $TenantID | Where-Object {($_.state -eq 'enabled') } | Out-GridView -Title "Select Subscription to Use" -PassThru
}
catch {
Write-Log -Message "Error When trying to connect to tenant : $($_)" -LogFileName $LogFileName -Severity Error
exit
}
foreach($GetSubscription in $GetSubscriptions) {
Try {
#Set context for subscription being built
$azContext = Set-AzContext -Subscription $GetSubscription.id
Write-Log -Message "`nWorking in Subscription: $($GetSubscription.Name)" -LogFileName $LogFileName -Severity Information
Write-Log -Message "Listing Azure Logic Apps workspace from $($GetSubscription.Name)" -LogFileName $LogFileName -Severity Information
$AzureLogicApps = Get-AzResource -ResourceType "Microsoft.Logic/workflows" | Out-GridView -Title "Select Playbook to generate ARM Template" -PassThru
if($null -eq $AzureLogicApps){
Write-Log -Message "No Azure Logic Apps workspace found in $($GetSubscription.Name)" -LogFileName $LogFileName -Severity Error
}
else {
Write-Log -Message "Creating ARM Template" -LogFileName $LogFileName -Severity Information
$FolderName = Get-FolderName -initialDirectory $PSScriptRoot
foreach($LogicApp in $AzureLogicApps){
$tokenToUse = (Get-AzAccessToken).Token
$armHostUrl = $azContext.Environment.ResourceManagerUrl
$apiConnectionResources = [System.Collections.ArrayList]@()
$templateParameters = [ordered] @{}
$PlaybookARMParameters = [ordered] @{}
$templateVariables = [ordered] @{}
$PlaybookSubscriptionId = $GetSubscription.id
$PlaybookResourceName = $LogicApp.Name
$PlaybookResourceGroupName = $LogicApp.ResourceGroupName
$playbookResource = GetPlaybookResource
$null = MkDir "$($FolderName)\$($PlaybookResourceName)" -Force
# Remove Parameter default values
foreach($PlaybookParameter in $playbookResource.properties.definition.parameters.PSObject.Properties) {
if ($PlaybookParameter.Name -ne '$connections') {
$playbookResource.properties.definition.parameters.PSObject.Properties.Remove($PlaybookParameter.Name)
$playbookResource.properties.definition.parameters | Add-Member -MemberType NoteProperty -Name $($PlaybookParameter.Name) -Value @{"defaultValue"="[parameters('$($PlaybookParameter.Name)')]"
"type"= "string" }
$PlaybookARMParameters.Add($($PlaybookParameter.Name), [ordered] @{
"type"= "string"
"metadata"= @{
"description"="Enter value for $($PlaybookParameter.Name)"
}
})
}
}
# Add changes for API connection resources
Foreach ($apiConnectionReference in $playbookResource.properties.parameters.'$connections'.value.PsObject.Properties) {
HandlePlaybookApiConnectionReference -apiConnectionReference $apiConnectionReference -playbookResource $playbookResource
}
# Create and export the ARM template
$armTemplateOutput = BuildArmTemplate -playbookResource $playbookResource | ConvertTo-Json -Depth 100
$armTemplateOutput = $armTemplateOutput -replace "\\u0027", "'" # ConvertTo-Json escapes quotes, which we don't want
FixJsonIndentation -jsonOutput $armTemplateOutput | Set-Content "$($FolderName)\$($PlaybookResourceName)\azuredeploy.json"
Write-Log -Message "ARM Template created successfully at $($FolderName)\$($PlaybookResourceName)\azuredeploy.json" -LogFileName $LogFileName -Severity Information
}
Confirmation-Dlg -DlgMessage "ARM Template created successfully at $($FolderName)"
}
}
catch {
Write-Log -Message "Error When trying to connect to Subscription : $($_)" -LogFileName $LogFileName -Severity Error
exit
}
}
#endregion
Key Features
- 🔐 Secure Authentication using
Connect-AzAccount
- 🔎 Interactive GUI Selection for subscriptions and Logic Apps via
Out-GridView
- 🔁 Handles Both Managed and Custom API Connections
- 🧩 Automatic Parameter Extraction for dynamic fields
- 📦 Modular Design with helper functions like
Get-RequiredModules
,HandlePlaybookApiConnectionReference
, andBuildArmTemplate
- 🛠️ Built-in Logging with color-coded messages
- 🧪 Environment Clean-Up with context disconnection before each new session
- 📁 Export Templates in a structured folder format
Requirements
Before running the script, make sure:
- You are using PowerShell version 5.0 or higher
- You have Az PowerShell modules installed (specifically
Az.Accounts
,Az.Resources
, andAz.OperationalInsights
) - You have sufficient permissions in the Azure subscription and resource group
How to Use
- Open PowerShell as an Administrator.
- Save the script locally as
GenerateARMTemplate_V2.ps1
. - Run the script using the command:
.\GenerateARMTemplate_V2.ps1 -TenantID <Your-Tenant-ID>
- Answer the prompts to:
- Choose whether to generate a gallery-ready template
- Decide on updating Az modules
- Log in to Azure and select the appropriate subscription
- Select Logic Apps to export
- Choose a folder to save the exported templates.
- The script will:
- Fetch the Logic App and connections
- Clean up the JSON definition
- Export a formatted
azuredeploy.json
file per Logic App
Example Output Path
The script will create a sub-folder for each Logic App, such as:
plaintextCopyEditC:\Users\You\Documents\ARMTemplates\MyLogicApp\azuredeploy.json
This file is ready to be deployed using tools like Azure CLI, Azure Portal, or as part of a DevOps pipeline.
Notes and Best Practices
- Version Control: Store generated templates in Git or Azure Repos for better CI/CD integration.
- Security: Always review and sanitize connection strings and secrets before sharing or publishing templates.
Summary
If you’re working extensively with Azure Logic Apps, this script is a powerful tool to include in your automation toolkit. It not only saves time but also ensures your Logic Apps and their connections are properly preserved across environments or published in a compliant, structured way.
Whether you’re deploying internal workflows or creating public templates for the community, this script does the heavy lifting for you—efficiently and cleanly.