Simplifying Microsoft Logic App Exports: A Powerful Tool for DevOps Teams
π Export Logic Apps with Ease: One Script to Rule All Environments! π
Published on 10 March 2025
Introduction
For organisations working with Microsoft Azure, Logic Apps have become an essential component for workflow automation. Whether you're orchestrating business processes, automating Sentinel security responses, or integrating disparate systems, Logic Apps provide the glue that binds these processes together. However, when it comes to managing these workflows across environmentsβfrom development to testing and productionβthings can get complicated quickly.
I have created a new open-source PowerShell tool that addresses this challenge: theLogic App ARM Template Export Script. This utility enables DevOps teams, cloud architects, and IT professionals to easily export Logic Apps as Azure Resource Manager (ARM) templates, streamlining the deployment process and ensuring consistency across environments.
The Challenge of Logic App Management
Before diving into the solution, let's examine the common challenges faced when managing Logic Apps:
- Manual export processes: Using the Azure Portal to individually export Logic Apps is time-consuming
- Connection handling: Managing API connections across environments often requires tedious reconfiguration
- Gallery-compatible templates: Creating templates suitable for Microsoft Sentinel's Content Hub Gallery requires specific formatting
- Multi-environment consistency: Ensuring identical workflows across development, testing, and production environments
The Logic App ARM Template Export Script
This PowerShell-based solution provides an elegant approach to exporting Logic Apps as ARM templates. Crafted with cross-platform compatibility in mind, the script runs on PowerShell 7 across Windows, macOS, and Linux, making it accessible to all Azure administrators regardless of their operating system preference.
Key Features
The script offers several powerful features:
- Interactive selection: Easily navigate through tenants, subscriptions, resource groups, and Logic Apps via an intuitive console interface
- Gallery template generation: Optional configuration to generate templates suitable for the Microsoft Sentinel Content Hub Gallery
- API connection handling: Automatic detection and configuration of API connections within the templates
- Managed identity support: Seamless handling of connections that use managed service identities
- Customisable export location: Flexibility to choose where your exported templates are saved
- Comprehensive logging: Detailed activity tracking to facilitate troubleshooting
How It Works
The script follows a logical, user-friendly workflow:
- Environment setup: Verifies and installs required PowerShell modules (Az.Accounts and Microsoft.PowerShell.ConsoleGuiTools)
- User choices: Prompts for gallery template generation and Az module updates
- Azure navigation: Guides you through selecting the tenant, subscription, and resource group
- Logic App selection: Presents available Logic Apps for multi-selection
- Template generation: Builds ARM templates with proper parameter handling and connection references
- Export: Saves the ARM templates to your specified location
Implementation Deep Dive
The script is remarkably thorough in handling the nuances of Logic App exports. Let's examine some of its key components:
Interactive Module Verification
The script begins with robust module verification, intelligently checking for required modules and guiding the user through installation if they're missing:
Function Ensure-Module {
param (
[string]$ModuleName,
[string]$InstallMessage,
[string]$InstallCommand
)
Write-Log -Message "Checking if module '$ModuleName' is installed..." -Severity "Information"
if (-not (Get-Module -ListAvailable -Name $ModuleName)) {
Write-Log -Message "Module '$ModuleName' is not installed. Prompting user for installation." -Severity "Warning"
$InstallQuestion = "The $ModuleName module is required. Do you want to install it now?"
$InstallChoices = New-Object System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]
$InstallChoices.Add((New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes', "Install the $ModuleName module"))
$InstallChoices.Add((New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList '&No', "Do not install and exit"))
$InstallDecision = $Host.UI.PromptForChoice(
"Install $ModuleName",
$InstallQuestion,
$InstallChoices,
1
)
if ($InstallDecision -eq 0) {
try {
Invoke-Expression $InstallCommand
Import-Module $ModuleName -ErrorAction Stop
Write-Log -Message "Module '$ModuleName' installed and imported successfully." -Severity "Information"
} catch {
Write-Log -Message "Failed to install module '$ModuleName': $($_.Exception.Message)" -Severity "Error"
Write-Log -Message "Exiting script due to missing required module." -Severity "Error"
exit 1
}
} else {
Write-Log -Message "User chose not to install module '$ModuleName'. Exiting..." -Severity "Error"
exit 1
}
}
}
This attention to detail ensures a smooth user experience even when prerequisites aren't met initially.
Comprehensive Logging System
The script implements a sophisticated logging system that tracks operations at multiple severity levels:
Function Write-Log {
param(
[Parameter(Mandatory=$true)][string]$Message,
[ValidateSet("Information", "Warning", "Error", "Debug")][string]$Severity = 'Information'
)
# Get the script root directory
$ScriptRoot = $PSScriptRoot
if (-not $ScriptRoot) {
$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition
if (-not $ScriptRoot) {
$ScriptRoot = (Get-Location).Path
}
}
# Define the log file path
$LogFile = Join-Path -Path $ScriptRoot -ChildPath "ScriptLog.txt"
# Build the log entry with timestamp and severity
$TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$LogEntry = "[$TimeStamp] [$Severity] $Message"
# Write to the console with appropriate color
switch ($Severity) {
"Warning" { Write-Host $LogEntry -ForegroundColor Yellow }
"Error" { Write-Host $LogEntry -ForegroundColor Red }
"Debug" { Write-Host $LogEntry -ForegroundColor Cyan }
}
# Append to the log file
try {
Add-Content -Path $LogFile -Value $LogEntry
} catch {
Write-Host "Failed to write log to file: $($_.Exception.Message)" -ForegroundColor Red
}
}
This dual logging approachβto both console and fileβensures administrators have a clear record of actions for troubleshooting and audit purposes.
Flexible Export Location Selection
Rather than forcing a fixed export location, the script offers a user-friendly way to select the destination folder:
Function Initialize-ExportFolder {
param (
[string]$DefaultExportFolder
)
$ChangeLocationQuestion = "Would you like to change the default export location?"
$ChangeLocationChoices = New-Object System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]
$ChangeLocationChoices.Add((New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes', 'Change the export folder location'))
$ChangeLocationChoices.Add((New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList '&No', 'Use the default export folder location'))
$ChangeLocationDecision = $Host.UI.PromptForChoice(
"Change Export Location",
$ChangeLocationQuestion,
$ChangeLocationChoices,
1
)
if ($ChangeLocationDecision -eq 0) {
# Present directories in a grid view for selection
$directories = Get-ChildItem -Directory -Path $HOME | Select-Object Name,FullName
$selectedDirectory = $directories | Out-ConsoleGridView -Title "Select Export Folder (Press ENTER when done)" -OutputMode Single
if ($selectedDirectory) {
$ExportFolder = $selectedDirectory.FullName
} else {
$ExportFolder = $DefaultExportFolder
}
} else {
$ExportFolder = $DefaultExportFolder
}
# Ensure folder exists
if (-not (Test-Path -Path $ExportFolder)) {
New-Item -Path $ExportFolder -ItemType Directory -Force | Out-Null
}
return $ExportFolder
}
This visual directory selection approach leverages the Out-ConsoleGridView
cmdlet for a more intuitive experience than traditional command-line parameter passing.
API Connection Handling
One of the most impressive aspects is how the script manages API connections. It meticulously extracts and reconfigures connection information to work in any target environment:
Function HandlePlaybookApiConnectionReference($apiConnectionReference, $playbookResource) {
# Get connection name
$connectionName = $apiConnectionReference.Name
$connectionName = $connectionName.Split('_')[0].ToString().Trim()
$connectionName = (Get-Culture).TextInfo.ToTitleCase($connectionName)
# Create variable for the connection
if ($connectionName -ieq "azuresentinel") {
$connectionVariableName = "MicrosoftSentinelConnectionName"
$templateVariables.Add($connectionVariableName, "[concat('MicrosoftSentinel-', parameters('PlaybookName'))]")
} else {
$connectionVariableName = "$($connectionName)ConnectionName"
$templateVariables.Add($connectionVariableName, "[concat('$connectionName-', parameters('PlaybookName'))]")
}
# Determine connector type and authentication method
$connectorType = if ($apiConnectionReference.Value.id.ToLowerInvariant().Contains("/managedapis/")) { "managedApis" } else { "customApis" }
$connectionAuthenticationType = if ($apiConnectionReference.Value.connectionProperties.authentication.type -eq "ManagedServiceIdentity") { "Alternative" } else { $null }
# Special handling for Sentinel connections when generating gallery templates
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 to retrieve existing connection details from ARM
try {
$existingConnectionProperties = SendArmGetCall -relativeUrl "$($apiConnectionReference.Value.connectionId)?api-version=2016-06-01"
}
catch {
$existingConnectionProperties = $null
}
# Create the connection resource for the ARM template
$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')]"
}
}
}
# Add the connection resource and update the playbook to reference it
$apiConnectionResources.Add($apiConnectionResource) | Out-Null
$playbookResource.dependsOn += "[resourceId('Microsoft.Web/connections', variables('$connectionVariableName'))]"
}
This function handles both standard API connections and Microsoft Sentinel-specific connections, with special consideration for Managed Service Identity authentication.
Intelligent ARM Template Construction
The script constructs ARM templates with meticulous attention to detail, removing deployment-specific properties and adapting resources for redeployment:
Function GetPlaybookResource() {
Try {
$playbookArmIdToUse = BuildPlaybookArmId
$playbookResource = SendArmGetCall -relativeUrl "$($playbookArmIdToUse)?api-version=2017-07-01"
# Add a parameter for the playbook name
$PlaybookARMParameters.Add("PlaybookName", [ordered] @{
"defaultValue"= $playbookResource.Name
"type"= "string"
})
# Gallery-specific handling
if ($GenerateForGallery) {
# Add required tags for Content Hub Gallery
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"
}
}
# Ensure SystemAssigned identity for gallery templates
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"
}
}
}
}
# Remove deployment-specific properties
$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 runtime properties
$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")
return $playbookResource
}
Catch {
Write-Log -Message "Error occurred in GetPlaybookResource :$($_)" -Severity Error
}
}
Professional JSON Formatting
The script includes a sophisticated JSON formatting function that ensures the exported templates are professionally structured and human-readable:
Function FixJsonIndentation ($jsonOutput) {
Try {
$currentIndent = 0
$tabSize = 4
$lines = $jsonOutput.Split([Environment]::NewLine)
$newString = ""
foreach ($line in $lines) {
if ($line.Trim() -eq "") {
continue
}
# If line ends with ] or }, reduce indent first
if ($line -match "[\]\}],?\s*$") {
$currentIndent -= 1
}
# Add current 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 line ends with { or [, increase indent
if ($line -match "[\[{]\s*$") {
$currentIndent += 1
}
}
return $newString
}
catch {
Write-Log -Message "Error occurred in FixJsonIndentation :$($_)" -Severity Error
}
}
This attention to detail ensures that the exported templates are not just functional but also easy to read and maintainβa crucial factor for teams that need to review or modify the templates before deployment.
Real-World Applications
This tool excels in several real-world scenarios:
Scenario 1: DevOps Automation
Teams embracing Infrastructure as Code (IaC) can integrate this script into their CI/CD pipelines to automate the export of Logic Apps from development environments. The script's ability to handle tenant and subscription selection makes it particularly valuable for organisations with complex Azure environments.
# Example of how the script navigates Azure environments
if (-not $TenantId) {
$tenants = Get-AzTenant
if ($tenants.Count -gt 1) {
$selectedTenant = $tenants | Out-ConsoleGridView -Title "Select Tenant" -OutputMode Single
if (-not $selectedTenant) {
Write-Log -Message "No tenant selected. Exiting..." -Severity "Error"
exit
}
$TenantId = $selectedTenant.TenantId
} else {
# Only one tenant found, use it automatically
$TenantId = $tenants[0].TenantId
}
}
The exported templates can be version-controlled and deployed to testing and production environments, ensuring consistency across the deployment lifecycle.
Scenario 2: Microsoft Sentinel Content Creators
Security teams developing custom Sentinel playbooks can use this script to prepare their Logic Apps for submission to the Content Hub Gallery. The script automatically handles the specific requirements for gallery templates:
# Logic for gallery-specific template formatting
if ($GenerateForGallery) {
$armTemplate.Insert(2, "metadata", [ordered] @{
"title"= ""
"description"= ""
"prerequisites"= ""
"postDeployment" = @()
"prerequisitesDeployTemplateFile"= ""
"lastUpdateTime"= ""
"entities"= @()
"tags"= @()
"support"= [ordered] @{
"tier"= "community"
"armtemplate" = "Generated"
}
"author"= @{
"name"= ""
}
})
}
This gallery-specific formatting ensures that submissions meet all the requirements for sharing with the wider security community, saving considerable time in the submission process.
Scenario 3: Environment Migration
When organisations need to migrate Logic Apps between Azure environmentsβperhaps during a tenant migration or regional expansionβthis script simplifies the process by generating templates that are environment-agnostic. The script intelligently parameterizes environment-specific values:
# Converting resource-specific references to parameters
$playbookResource.location = "[resourceGroup().location]"
$playbookResource.name = "[parameters('PlaybookName')]"
# Handling API connections with subscription-agnostic references
$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')]"
}
This parameterization ensures that the templates can be deployed to any Azure environment without manual modifications.
Getting Started
To start using this powerful tool:
- Prerequisites:
- PowerShell 7.0 or later installed on your system
- Basic familiarity with Azure and PowerShell
- Setup:
- Clone or download the script from the repository
- Run the script in a PowerShell 7+ terminal with the optional
-TenantId
parameter or let it guide you through tenant selection - Follow the interactive prompts to select your export options
- First Run Experience:
The script offers a guided experience during its first execution:
************************************************************
* Logic App ARM Template Export Script *
************************************************************
This script will help you select and export Logic Apps as ARM templates.
By default, the output will be saved to:
C:\Scripts\LogicAppExport
You can choose to change this location if desired.
Running on PowerShell 7+, multi-platform compatible.
************************************************************
Change Export Location
Would you like to change the default export location?
[Y] Yes [N] No [?] Help (default is "N"):
Install Microsoft.PowerShell.ConsoleGuiTools
The Microsoft.PowerShell.ConsoleGuiTools module is required. Do you want to install it now?
[Y] Yes [N] No [?] Help (default is "N"):
Gallery Template Generation
Generate ARM Template for Gallery?
[Y] Yes [N] No [?] Help (default is "N"):
Update Az Modules
Do you want to update required Az Modules to the latest version?
[Y] Yes [N] No [?] Help (default is "N"):
From there, the script will guide you through:
- Tenant selection (if not provided as a parameter)
- Subscription selection using an interactive grid view
- Resource group selection
- Logic App selection (with multi-select capability)
- Template generation with real-time progress updates
Here's an example of the multi-select grid for Logic Apps:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Select Logic Apps to Export as ARM Templates (Press ENTER when done) β
βββββββββββββββββββββ¬βββββββββββββββββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββ€
β ResourceGroupName β Name β Location β Kind β State β
βββββββββββββββββββββΌβββββββββββββββββββββΌβββββββββββΌβββββββββββΌβββββββββββ€
β Sentinel-Playbook β Alert-TeamChannel β westeu β Playbook β Enabled β
β Sentinel-Playbook β Create-Incident β westeu β Playbook β Enabled β
β Sentinel-Playbook β Email-SOC-Team β westeu β Playbook β Enabled β
β Sentinel-Playbook β EntraID-Monitor β westeu β Playbook β Enabled β
βββββββββββββββββββββ΄βββββββββββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββ
The script will then export the selected Logic Apps as ARM templates to your chosen location, with detailed logging of each action performed.
Security and Best Practices
When using this tool, consider these security best practices:
- Use the script on a secure workstation with appropriate Azure access
- Review exported templates before deployment to ensure sensitive information isn't inadvertently included
- Consider applying additional encryption or secure storage for exported templates
- When defining permanent export locations, ensure appropriate access controls are in place
Technical Highlights
Cross-Platform Compatibility
One of the standout features of this script is its true cross-platform compatibility. Built for PowerShell 7+, it works flawlessly across Windows, macOS, and Linux environments:
#requires -Version 7.0
#requires -Module Az.Accounts
<#
.SYNOPSIS
Logic App ARM Template Export Script
.DESCRIPTION
This script helps you select and export Logic Apps (including Sentinel Playbooks)
as ARM templates. It guides you through selecting a tenant, subscription, resource group,
and one or more Logic Apps. It can optionally generate templates suitable for gallery deployment
and can update the required Az modules.
It also allows you to change the default export location for the generated templates,
and ensures you have both the Az.Accounts module and Microsoft.PowerShell.ConsoleGuiTools module
installed, prompting you to install them if not found.
This script is designed to run on PowerShell 7 or later, and is compatible with
Windows, macOS, and Linux environments.
#>
This cross-platform support is particularly valuable for diverse DevOps teams where engineers might be using different operating systems.
Error Handling
The script is designed with robust error handling throughout, ensuring graceful operation even when encountering issues:
try {
$armTemplateJson | Out-File -FilePath $armFileName -Encoding UTF8
Write-Log "Exported ARM Template for '$name' to '$armFileName'" -Severity Information
} catch {
Write-Log "Failed to export ARM Template for '$name': $($_.Exception.Message)" -Severity Error
}
This comprehensive error handling ensures that even if one Logic App export fails, the script can continue with others, providing a more resilient export experience.
Conclusion
The Logic App ARM Template Export Script represents a significant advancement in Azure Logic App management. By automating the export process with meticulous attention to detail, it solves numerous challenges:
- It eliminates manual work through its guided, interactive approach to Logic App selection and export
- It ensures proper API connection handling by intelligently extracting and parameterizing connection information
- It supports special use cases like Microsoft Sentinel Content Hub Gallery submissions with specialized template formatting
- It maintains full fidelity of Logic App definitions while making them environment-agnostic for redeployment
- It works across platforms with full support for Windows, macOS, and Linux environments
For DevOps teams, cloud architects, and security professionals working with Logic Apps and Sentinel playbooks, this tool should become an essential part of your toolkit. It transforms what was once a tedious, error-prone process into a streamlined, consistent experienceβsaving time, reducing errors, and enabling more robust deployment practices.
The script's sophisticated approach to user interactionβleveraging console grid views rather than traditional command-line parametersβmakes it accessible to both PowerShell experts and those with more limited scripting experience.
Give it a try today, and transform how you manage Logic Apps across your Azure environments!
Have you implemented automation for Logic App deployment in your environment? Share your experiences in the comments below!
Acknowledgements: Special thanks to Sreedhar Ande for the original Playbook-ARM-Template-Generator script that inspired this.