Deploying Logic Apps via Azure DevOps
Deployment Strategies
Logic Apps deployment varies by hosting model:
Consumption
ARM/Bicep template-based deployment
DefinitionARM/Bicep templates
DeploymentARM deployment
ConnectionsAPI connection resources
ParametersARM parameters
Local devNot supported
Standard
File-based deployment (like Azure Functions)
DefinitionWorkflow files + app settings
DeploymentZIP deploy
Connectionsconnections.json + app settings
Parametersparameters.json + app settings
Local devVS Code with Functions runtime
Microsoft Reference: DevOps deployment for Logic Apps
Consumption Logic Apps Pipeline
Project Structure
logic-apps/
├── consumption/
│ ├── templates/
│ │ ├── order-processing.json # ARM template
│ │ ├── order-processing.parameters.dev.json
│ │ └── order-processing.parameters.prod.json
│ └── connections/
│ ├── connections.dev.json
│ └── connections.prod.json
└── pipeline-consumption.yml
ARM Template for Consumption Logic App
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"logicAppName": { "type": "string" },
"location": { "type": "string", "defaultValue": "[resourceGroup().location]" },
"environment": { "type": "string", "allowedValues": ["dev", "test", "prod"] },
"apiBaseUrl": { "type": "string" },
"notificationEmail": { "type": "string" }
},
"resources": [
{
"type": "Microsoft.Logic/workflows",
"apiVersion": "2019-05-01",
"name": "[parameters('logicAppName')]",
"location": "[parameters('location')]",
"identity": {
"type": "SystemAssigned"
},
"properties": {
"state": "Enabled",
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"apiBaseUrl": { "type": "string", "defaultValue": "[parameters('apiBaseUrl')]" },
"notificationEmail": { "type": "string", "defaultValue": "[parameters('notificationEmail')]" }
},
"triggers": {
"When_an_HTTP_request_is_received": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {}
}
}
},
"actions": {}
}
}
}
]
}
Pipeline Definition
trigger:
branches:
include:
- main
paths:
include:
- logic-apps/consumption/*
variables:
- group: logic-apps-settings
stages:
- stage: Validate
displayName: 'Validate Templates'
jobs:
- job: ValidateARM
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
displayName: 'Validate ARM template'
inputs:
azureSubscription: 'dev-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group validate \
--resource-group rg-integration-dev \
--template-file logic-apps/consumption/templates/order-processing.json \
--parameters @logic-apps/consumption/templates/order-processing.parameters.dev.json
- stage: DeployDev
displayName: 'Deploy to Development'
dependsOn: Validate
jobs:
- deployment: DeployLogicApp
environment: 'logic-apps-dev'
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: AzureCLI@2
displayName: 'Deploy Logic App'
inputs:
azureSubscription: 'dev-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group create \
--resource-group rg-integration-dev \
--template-file logic-apps/consumption/templates/order-processing.json \
--parameters @logic-apps/consumption/templates/order-processing.parameters.dev.json
- task: AzureCLI@2
displayName: 'Verify deployment'
inputs:
azureSubscription: 'dev-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
STATE=$(az logic workflow show \
--resource-group rg-integration-dev \
--name la-order-processing-dev \
--query 'state' -o tsv)
if [ "$STATE" != "Enabled" ]; then
echo "Logic App is not enabled. State: $STATE"
exit 1
fi
echo "Logic App deployed and enabled successfully."
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: DeployDev
condition: succeeded()
jobs:
- deployment: DeployLogicApp
environment: 'logic-apps-prod'
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: AzureCLI@2
displayName: 'Deploy Logic App'
inputs:
azureSubscription: 'prod-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group create \
--resource-group rg-integration-prod \
--template-file logic-apps/consumption/templates/order-processing.json \
--parameters @logic-apps/consumption/templates/order-processing.parameters.prod.json
Standard Logic Apps Pipeline
Project Structure
logic-apps/
├── standard/
│ ├── .vscode/
│ ├── host.json
│ ├── connections.json
│ ├── parameters.json
│ ├── order-processing/
│ │ └── workflow.json
│ ├── invoice-sync/
│ │ └── workflow.json
│ └── Artifacts/
│ ├── Maps/
│ └── Schemas/
├── infra/
│ ├── main.bicep
│ ├── modules/
│ │ ├── logic-app.bicep
│ │ ├── storage.bicep
│ │ └── app-service-plan.bicep
│ └── parameters/
│ ├── dev.json
│ └── prod.json
└── pipeline-standard.yml
Pipeline Definition
trigger:
branches:
include:
- main
paths:
include:
- logic-apps/standard/*
- logic-apps/infra/*
variables:
- group: logic-app-standard-settings
- name: workingDirectory
value: 'logic-apps/standard'
stages:
- stage: Build
displayName: 'Build Logic App Package'
jobs:
- job: BuildPackage
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '6.x'
- script: |
cd $(workingDirectory)
mkdir -p $(Build.ArtifactStagingDirectory)/logic-app
cp -r . $(Build.ArtifactStagingDirectory)/logic-app/
# Remove local settings (not for deployment)
rm -f $(Build.ArtifactStagingDirectory)/logic-app/local.settings.json
rm -rf $(Build.ArtifactStagingDirectory)/logic-app/.vscode
cd $(Build.ArtifactStagingDirectory)/logic-app
zip -r $(Build.ArtifactStagingDirectory)/logic-app.zip .
displayName: 'Package Logic App'
- publish: $(Build.ArtifactStagingDirectory)/logic-app.zip
artifact: logic-app-package
- stage: DeployInfra
displayName: 'Deploy Infrastructure'
dependsOn: Build
jobs:
- deployment: DeployInfrastructure
environment: 'logic-app-infra-dev'
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: AzureCLI@2
displayName: 'Deploy Bicep Infrastructure'
inputs:
azureSubscription: 'dev-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group create \
--resource-group rg-integration-dev \
--template-file logic-apps/infra/main.bicep \
--parameters @logic-apps/infra/parameters/dev.json
- stage: DeployApp
displayName: 'Deploy Logic App Code'
dependsOn: DeployInfra
jobs:
- deployment: DeployCode
environment: 'logic-app-dev'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: logic-app-package
- task: AzureFunctionApp@2
displayName: 'Deploy to Logic App'
inputs:
azureSubscription: 'dev-service-connection'
appType: 'functionApp'
appName: 'la-standard-dev'
package: '$(Pipeline.Workspace)/logic-app-package/logic-app.zip'
deploymentMethod: 'zipDeploy'
- task: AzureCLI@2
displayName: 'Configure app settings'
inputs:
azureSubscription: 'dev-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az functionapp config appsettings set \
--resource-group rg-integration-dev \
--name la-standard-dev \
--settings \
"WORKFLOWS_SUBSCRIPTION_ID=$(subscriptionId)" \
"WORKFLOWS_RESOURCE_GROUP_NAME=rg-integration-dev" \
"WORKFLOWS_LOCATION_NAME=uksouth"
- stage: SmokeTest
displayName: 'Smoke Tests'
dependsOn: DeployApp
jobs:
- job: RunSmokeTests
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
# Test HTTP-triggered workflow
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "https://la-standard-dev.azurewebsites.net/api/order-processing/triggers/manual/invoke?api-version=2020-05-01-preview" \
-H "Content-Type: application/json" \
-d '{"orderId": "TEST-001", "test": true}')
if [ "$RESPONSE" -ge 200 ] && [ "$RESPONSE" -lt 300 ]; then
echo "Smoke test passed (HTTP $RESPONSE)"
else
echo "Smoke test failed (HTTP $RESPONSE)"
exit 1
fi
displayName: 'Run smoke tests'
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: SmokeTest
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProd
environment: 'logic-app-prod'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: logic-app-package
- task: AzureFunctionApp@2
displayName: 'Deploy to Production'
inputs:
azureSubscription: 'prod-service-connection'
appType: 'functionApp'
appName: 'la-standard-prod'
package: '$(Pipeline.Workspace)/logic-app-package/logic-app.zip'
deploymentMethod: 'zipDeploy'
Connection Management
Parameterised Connections
Use app settings to manage connection strings across environments:
// connections.json
{
"serviceProviderConnections": {
"serviceBus": {
"parameterValues": {
"connectionString": "@appsetting('ServiceBus_ConnectionString')"
},
"serviceProvider": {
"id": "/serviceProviders/serviceBus"
}
},
"sql": {
"parameterValues": {
"connectionString": "@appsetting('SQL_ConnectionString')"
},
"serviceProvider": {
"id": "/serviceProviders/sql"
}
}
}
}
Managing Connections in Pipelines
- task: AzureCLI@2
displayName: 'Set connection strings from Key Vault'
inputs:
azureSubscription: 'prod-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Retrieve secrets from Key Vault
SB_CONN=$(az keyvault secret show \
--vault-name kv-integration-prod \
--name ServiceBusConnectionString \
--query value -o tsv)
SQL_CONN=$(az keyvault secret show \
--vault-name kv-integration-prod \
--name SqlConnectionString \
--query value -o tsv)
# Set as app settings (or use Key Vault references)
az functionapp config appsettings set \
--resource-group rg-integration-prod \
--name la-standard-prod \
--settings \
"ServiceBus_ConnectionString=$SB_CONN" \
"SQL_ConnectionString=$SQL_CONN"
Key Vault References (Preferred)
- task: AzureCLI@2
displayName: 'Configure Key Vault references'
inputs:
azureSubscription: 'prod-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az functionapp config appsettings set \
--resource-group rg-integration-prod \
--name la-standard-prod \
--settings \
"ServiceBus_ConnectionString=@Microsoft.KeyVault(VaultName=kv-integration-prod;SecretName=ServiceBusConnectionString)" \
"SQL_ConnectionString=@Microsoft.KeyVault(VaultName=kv-integration-prod;SecretName=SqlConnectionString)"
Testing Strategies
Unit Testing Workflow Definitions
Validate workflow JSON structure:
# Validate all workflow.json files
for f in $(find . -name "workflow.json"); do
echo "Validating $f..."
python -c "import json; json.load(open('$f'))" || exit 1
done
Integration Testing
- stage: IntegrationTest
dependsOn: DeployDev
jobs:
- job: RunIntegrationTests
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
# Test each workflow endpoint
for workflow in order-processing invoice-sync; do
echo "Testing $workflow..."
STATUS=$(curl -s -o response.json -w "%{http_code}" \
-X POST "https://la-standard-dev.azurewebsites.net/api/$workflow/triggers/manual/invoke?api-version=2020-05-01-preview" \
-H "Content-Type: application/json" \
-d @test-data/$workflow-payload.json)
if [ "$STATUS" -lt 200 ] || [ "$STATUS" -ge 300 ]; then
echo "FAILED: $workflow returned HTTP $STATUS"
cat response.json
exit 1
fi
echo "PASSED: $workflow (HTTP $STATUS)"
done
displayName: 'Run integration tests'
Rollback Strategy
Deployment Slots (Standard)
resource stagingSlot 'Microsoft.Web/sites/slots@2022-09-01' = {
parent: logicApp
name: 'staging'
location: location
kind: 'functionapp,workflowapp'
properties: {
serverFarmId: appServicePlan.id
}
}
# Deploy to staging slot, then swap
- task: AzureFunctionApp@2
inputs:
appName: 'la-standard-prod'
slotName: 'staging'
package: '$(Pipeline.Workspace)/logic-app-package/logic-app.zip'
- task: AzureAppServiceManage@0
inputs:
azureSubscription: 'prod-service-connection'
action: 'Swap Slots'
webAppName: 'la-standard-prod'
sourceSlot: 'staging'
targetSlot: 'production'
Best Practices
- Separate infrastructure from application code — deploy Bicep/Terraform independently
- Use Key Vault references for connection strings and secrets
- Implement deployment slots for zero-downtime production deployments
- Add smoke tests after each deployment stage
- Store workflow definitions in source control alongside application code
- Use environment-specific parameters via variable groups
- Validate templates before deployment to catch errors early
- Implement rollback using deployment slots or previous successful artifacts
- Monitor deployments with Application Insights deployment annotations
- Use approval gates on production environments