← Back to Tutorials
Advanced⏱️ 45 min

CI/CD for Logic Apps Standard with Azure DevOps

CI/CD for Logic Apps Standard with Azure DevOps

In this tutorial, you'll build a complete CI/CD pipeline for Logic Apps Standard using Azure DevOps. You'll learn how to structure your project, automate builds and deployments, manage environment-specific configuration, and achieve zero-downtime deployments with slots.

Prerequisites

  • Azure subscription with Logic Apps Standard resource
  • Azure DevOps organization and project
  • VS Code with Azure Logic Apps (Standard) extension
  • Azure CLI and Bicep CLI installed
  • Basic familiarity with YAML pipelines

Pipeline Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                        Azure DevOps Pipeline                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌───────────┐    ┌───────────┐    ┌────────────┐    ┌──────────────┐  │
│  │  Trigger  │───▶│   Build   │───▶│  Deploy to │───▶│  Swap Slot   │  │
│  │  (main)   │    │  & Test   │    │  Staging   │    │  to Prod     │  │
│  └───────────┘    └───────────┘    └────────────┘    └──────────────┘  │
│                         │                                               │
│                         ▼                                               │
│               ┌───────────────────┐                                     │
│               │  Publish Artifact │                                     │
│               │  (zip package)    │                                     │
│               └───────────────────┘                                     │
│                                                                         │
├─────────────────────────────────────────────────────────────────────────┤
│  Environments:  DEV ──────▶ STAGING ──────▶ PRODUCTION                  │
│                 (auto)      (auto)          (manual approval)           │
└─────────────────────────────────────────────────────────────────────────┘

Logic Apps Standard Project Structure

A Logic Apps Standard project follows a specific folder structure that maps directly to how workflows are deployed:

my-logic-app/
├── host.json                    # Runtime configuration
├── connections.json             # API connections and managed connectors
├── parameters.json              # Workflow parameters
├── local.settings.json          # Local dev settings (not deployed)
├── Artifacts/
│   └── Maps/                    # XSLT maps, Liquid templates
├── workflow-order-processing/
│   └── workflow.json            # Workflow definition
├── workflow-notification/
│   └── workflow.json            # Another workflow
└── .vscode/
    ├── extensions.json
    ├── launch.json
    └── settings.json

host.json

The host.json file configures the Logic Apps runtime:

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
    "version": "[1.*, 2.0.0)"
  },
  "extensions": {
    "workflow": {
      "settings": {
        "Runtime.FlowRunRetryableActionJobCallback.ActionJobExecutionTimeout": "00:10:00"
      }
    }
  }
}

connections.json

Defines managed API connections and service provider connections:

{
  "managedApiConnections": {
    "office365": {
      "api": {
        "id": "/subscriptions/{sub}/providers/Microsoft.Web/locations/{region}/managedApis/office365"
      },
      "connection": {
        "id": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/connections/office365"
      },
      "connectionRuntimeUrl": "https://{connection-runtime-url}",
      "authentication": {
        "type": "ManagedServiceIdentity"
      }
    }
  },
  "serviceProviderConnections": {
    "serviceBus": {
      "parameterValues": {
        "connectionString": "@appsetting('ServiceBus_ConnectionString')"
      },
      "serviceProvider": {
        "id": "/serviceProviders/serviceBus"
      }
    }
  }
}

workflow.json

Each workflow lives in its own folder with a workflow.json definition:

{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "actions": {
      "Send_message": {
        "type": "ServiceProvider",
        "inputs": {
          "parameters": {
            "entityName": "orders",
            "message": {
              "contentData": "@triggerBody()"
            }
          },
          "serviceProviderConfiguration": {
            "connectionName": "serviceBus",
            "operationId": "sendMessage",
            "serviceProviderId": "/serviceProviders/serviceBus"
          }
        }
      }
    },
    "triggers": {
      "When_a_HTTP_request_is_received": {
        "type": "Request",
        "kind": "Http",
        "inputs": {
          "method": "POST"
        }
      }
    },
    "contentVersion": "1.0.0.0"
  },
  "kind": "Stateful"
}

Screenshot: Logic Apps project structure in VS Code

Local Development with VS Code Extension

Setting Up Your Environment

  1. Install the Azure Logic Apps (Standard) extension for VS Code
  2. Install the Azurite extension for local storage emulation
  3. Install .NET Core SDK 6.0 and Azure Functions Core Tools v4

Creating a New Project

  1. Open the Command Palette (Cmd+Shift+P)
  2. Select Azure Logic Apps: Create New Project
  3. Choose your workspace folder
  4. Select Stateful Workflow or Stateless Workflow

Running Locally

Configure local.settings.json for local development:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "ServiceBus_ConnectionString": "Endpoint=sb://dev-servicebus.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...",
    "WORKFLOWS_SUBSCRIPTION_ID": "your-subscription-id"
  }
}

Start the project:

func host start

The workflow designer opens in VS Code, allowing you to visually edit and test workflows.

Screenshot: VS Code Logic Apps designer

Bicep Template for Logic App Infrastructure

main.bicep

@description('Environment name')
param environmentName string

@description('Azure region')
param location string = resourceGroup().location

@description('Logic App name')
param logicAppName string = 'la-${environmentName}-orderprocessing'

@description('App Service Plan SKU')
param skuName string = 'WS1'

var appServicePlanName = 'asp-${environmentName}-logicapps'
var storageAccountName = 'st${environmentName}la${uniqueString(resourceGroup().id)}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: skuName
    tier: 'WorkflowStandard'
  }
  kind: 'elastic'
  properties: {
    elasticScaleEnabled: true
    maximumElasticWorkerCount: 20
  }
}

resource logicApp 'Microsoft.Web/sites@2023-01-01' = {
  name: logicAppName
  location: location
  kind: 'functionapp,workflowapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      netFrameworkVersion: 'v6.0'
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'node'
        }
        {
          name: 'WEBSITE_NODE_DEFAULT_VERSION'
          value: '~18'
        }
      ]
    }
  }
}

// Deployment slot for zero-downtime deployments
resource stagingSlot 'Microsoft.Web/sites/slots@2023-01-01' = {
  parent: logicApp
  name: 'staging'
  location: location
  kind: 'functionapp,workflowapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      netFrameworkVersion: 'v6.0'
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'node'
        }
      ]
    }
  }
}

output logicAppName string = logicApp.name
output logicAppIdentityPrincipalId string = logicApp.identity.principalId
output stagingSlotName string = stagingSlot.name

Screenshot: Bicep deployment in Azure Portal

Environment-Specific Configuration

App Settings per Environment

Use app settings to externalize environment-specific values. Reference them in connections.json with @appsetting('KEY'):

SettingDevStagingProduction
ServiceBus_ConnectionStringDev namespaceStaging namespaceProd namespace
SQL_ConnectionStringDev databaseStaging databaseProd database
API_BaseUrlhttps://api-dev.example.comhttps://api-staging.example.comhttps://api.example.com
WORKFLOWS_SUBSCRIPTION_IDDev subscriptionStaging subscriptionProd subscription

Parameter Files for Dev/Staging/Prod

Create environment-specific parameter files for Bicep deployments:

parameters/dev.bicepparam

using '../main.bicep'

param environmentName = 'dev'
param location = 'eastus'
param skuName = 'WS1'

parameters/staging.bicepparam

using '../main.bicep'

param environmentName = 'staging'
param location = 'eastus'
param skuName = 'WS1'

parameters/prod.bicepparam

using '../main.bicep'

param environmentName = 'prod'
param location = 'eastus'
param skuName = 'WS2'

App Settings Override Files

Create environment-specific app settings for pipeline deployment:

config/appsettings.dev.json

{
  "ServiceBus_ConnectionString": "$(ServiceBus-Dev-ConnectionString)",
  "SQL_ConnectionString": "$(SQL-Dev-ConnectionString)",
  "API_BaseUrl": "https://api-dev.example.com",
  "APPINSIGHTS_INSTRUMENTATIONKEY": "$(AppInsights-Dev-Key)"
}

config/appsettings.prod.json

{
  "ServiceBus_ConnectionString": "$(ServiceBus-Prod-ConnectionString)",
  "SQL_ConnectionString": "$(SQL-Prod-ConnectionString)",
  "API_BaseUrl": "https://api.example.com",
  "APPINSIGHTS_INSTRUMENTATIONKEY": "$(AppInsights-Prod-Key)"
}

Screenshot: Environment configuration in Azure DevOps variable groups

Azure DevOps Pipeline YAML (Build + Deploy)

azure-pipelines.yml

trigger:
  branches:
    include:
      - main
  paths:
    include:
      - src/logic-app/**

pool:
  vmImage: 'ubuntu-latest'

variables:
  - group: 'logic-app-common'
  - name: workingDirectory
    value: 'src/logic-app'
  - name: artifactName
    value: 'logic-app-package'

stages:
  # ─────────────────────────────────────────────
  # BUILD STAGE
  # ─────────────────────────────────────────────
  - stage: Build
    displayName: 'Build Logic App'
    jobs:
      - job: BuildJob
        displayName: 'Build and Package'
        steps:
          - task: UseDotNet@2
            displayName: 'Install .NET SDK'
            inputs:
              packageType: 'sdk'
              version: '6.x'

          - task: CopyFiles@2
            displayName: 'Copy Logic App files'
            inputs:
              SourceFolder: '$(workingDirectory)'
              Contents: |
                host.json
                connections.json
                parameters.json
                Artifacts/**
                workflow-*/**
              TargetFolder: '$(Build.ArtifactStagingDirectory)/app'

          - task: ArchiveFiles@2
            displayName: 'Create deployment package'
            inputs:
              rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/app'
              includeRootFolder: false
              archiveType: 'zip'
              archiveFile: '$(Build.ArtifactStagingDirectory)/$(artifactName).zip'

          - task: PublishBuildArtifacts@1
            displayName: 'Publish artifact'
            inputs:
              PathtoPublish: '$(Build.ArtifactStagingDirectory)/$(artifactName).zip'
              ArtifactName: '$(artifactName)'

  # ─────────────────────────────────────────────
  # DEPLOY TO DEV
  # ─────────────────────────────────────────────
  - stage: DeployDev
    displayName: 'Deploy to Dev'
    dependsOn: Build
    variables:
      - group: 'logic-app-dev'
    jobs:
      - deployment: DeployDev
        displayName: 'Deploy to Dev Environment'
        environment: 'logic-app-dev'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureAppServiceSettings@1
                  displayName: 'Configure app settings'
                  inputs:
                    azureSubscription: '$(azureServiceConnection)'
                    appName: 'la-dev-orderprocessing'
                    appSettings: |
                      [
                        { "name": "ServiceBus_ConnectionString", "value": "$(ServiceBus-Dev-ConnectionString)", "slotSetting": false },
                        { "name": "API_BaseUrl", "value": "https://api-dev.example.com", "slotSetting": false }
                      ]

                - task: AzureFunctionApp@2
                  displayName: 'Deploy Logic App'
                  inputs:
                    azureSubscription: '$(azureServiceConnection)'
                    appType: 'functionApp'
                    appName: 'la-dev-orderprocessing'
                    package: '$(Pipeline.Workspace)/$(artifactName)/$(artifactName).zip'
                    deploymentMethod: 'zipDeploy'

  # ─────────────────────────────────────────────
  # DEPLOY TO STAGING (with slot)
  # ─────────────────────────────────────────────
  - stage: DeployStaging
    displayName: 'Deploy to Staging'
    dependsOn: DeployDev
    variables:
      - group: 'logic-app-staging'
    jobs:
      - deployment: DeployStaging
        displayName: 'Deploy to Staging Slot'
        environment: 'logic-app-staging'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureFunctionApp@2
                  displayName: 'Deploy to staging slot'
                  inputs:
                    azureSubscription: '$(azureServiceConnection)'
                    appType: 'functionApp'
                    appName: 'la-staging-orderprocessing'
                    package: '$(Pipeline.Workspace)/$(artifactName)/$(artifactName).zip'
                    deploymentMethod: 'zipDeploy'
                    deployToSlotOrASE: true
                    slotName: 'staging'

                - task: AzureAppServiceManage@0
                  displayName: 'Swap staging to production'
                  inputs:
                    azureSubscription: '$(azureServiceConnection)'
                    Action: 'Swap Slots'
                    WebAppName: 'la-staging-orderprocessing'
                    ResourceGroupName: '$(resourceGroupName)'
                    SourceSlot: 'staging'

  # ─────────────────────────────────────────────
  # DEPLOY TO PRODUCTION (with approval + slot)
  # ─────────────────────────────────────────────
  - stage: DeployProd
    displayName: 'Deploy to Production'
    dependsOn: DeployStaging
    variables:
      - group: 'logic-app-prod'
    jobs:
      - deployment: DeployProd
        displayName: 'Deploy to Production Slot'
        environment: 'logic-app-prod'  # Configure approval gate on this environment
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureFunctionApp@2
                  displayName: 'Deploy to staging slot'
                  inputs:
                    azureSubscription: '$(azureServiceConnection)'
                    appType: 'functionApp'
                    appName: 'la-prod-orderprocessing'
                    package: '$(Pipeline.Workspace)/$(artifactName)/$(artifactName).zip'
                    deploymentMethod: 'zipDeploy'
                    deployToSlotOrASE: true
                    slotName: 'staging'

                - task: AzureCLI@2
                  displayName: 'Warm up staging slot'
                  inputs:
                    azureSubscription: '$(azureServiceConnection)'
                    scriptType: 'bash'
                    inlineScript: |
                      echo "Warming up staging slot..."
                      curl -s -o /dev/null -w "%{http_code}" https://la-prod-orderprocessing-staging.azurewebsites.net/
                      sleep 30

                - task: AzureAppServiceManage@0
                  displayName: 'Swap staging to production'
                  inputs:
                    azureSubscription: '$(azureServiceConnection)'
                    Action: 'Swap Slots'
                    WebAppName: 'la-prod-orderprocessing'
                    ResourceGroupName: '$(resourceGroupName)'
                    SourceSlot: 'staging'

Screenshot: Azure DevOps pipeline run

Step-by-Step: Set Up a Complete CI/CD Pipeline

Step 1: Organize Your Repository

Structure your repository to separate infrastructure from application code:

repo-root/
├── infra/
│   ├── main.bicep
│   └── parameters/
│       ├── dev.bicepparam
│       ├── staging.bicepparam
│       └── prod.bicepparam
├── src/
│   └── logic-app/
│       ├── host.json
│       ├── connections.json
│       ├── parameters.json
│       ├── workflow-order-processing/
│       │   └── workflow.json
│       └── workflow-notification/
│           └── workflow.json
├── config/
│   ├── appsettings.dev.json
│   ├── appsettings.staging.json
│   └── appsettings.prod.json
├── azure-pipelines.yml
└── README.md

Step 2: Create Azure DevOps Service Connection

  1. Navigate to Project Settings > Service connections
  2. Click New service connection > Azure Resource Manager
  3. Select Service principal (automatic)
  4. Choose your subscription and resource group
  5. Name it (e.g., azure-logic-app-connection)

Screenshot: Azure DevOps service connection setup

Step 3: Configure Variable Groups

Create variable groups for each environment in Pipelines > Library:

  • logic-app-common — shared variables (service connection name, artifact name)
  • logic-app-dev — dev connection strings and keys
  • logic-app-staging — staging connection strings and keys
  • logic-app-prod — production connection strings and keys

Link Azure Key Vault for secrets:

  1. Toggle Link secrets from an Azure key vault
  2. Select your Key Vault
  3. Add the secrets you need (connection strings, keys)

Step 4: Deploy Infrastructure with Bicep

Run the Bicep deployment for each environment:

# Deploy dev environment
az deployment group create \
  --resource-group rg-dev-logicapps \
  --template-file infra/main.bicep \
  --parameters infra/parameters/dev.bicepparam

# Deploy staging environment
az deployment group create \
  --resource-group rg-staging-logicapps \
  --template-file infra/main.bicep \
  --parameters infra/parameters/staging.bicepparam

# Deploy production environment
az deployment group create \
  --resource-group rg-prod-logicapps \
  --template-file infra/main.bicep \
  --parameters infra/parameters/prod.bicepparam

Step 5: Configure Environment Approvals

  1. Go to Pipelines > Environments
  2. Select logic-app-prod
  3. Click the menu > Approvals and checks
  4. Add Approvals — specify required approvers
  5. Optionally add Business Hours check to prevent off-hours deployments

Step 6: Create and Run the Pipeline

  1. Go to Pipelines > New Pipeline
  2. Select your repository source
  3. Choose Existing Azure Pipelines YAML file
  4. Select azure-pipelines.yml
  5. Click Run

Screenshot: Pipeline creation wizard

Step 7: Verify Deployment

After the pipeline completes:

# Check Logic App status
az logicapp show --name la-dev-orderprocessing --resource-group rg-dev-logicapps --query "state"

# List workflows
az rest --method get \
  --uri "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/la-dev-orderprocessing/workflows?api-version=2023-01-01"

Deployment Slots for Zero-Downtime

Deployment slots allow you to deploy to a staging environment and swap it into production without downtime.

How Slot Swapping Works

  1. Deploy new code to the staging slot
  2. Warm up the staging slot (pre-load workflows, establish connections)
  3. Validate the staging slot is healthy
  4. Swap staging into production — traffic switches instantly
  5. Rollback if needed by swapping back

Slot-Specific Settings

Mark settings as "slot settings" to keep them tied to a specific slot:

az webapp config appsettings set \
  --name la-prod-orderprocessing \
  --resource-group rg-prod-logicapps \
  --slot-settings "SLOT_NAME=production"

Health Check Configuration

Configure a health check endpoint so Azure can verify the slot is ready before swapping:

az webapp config set \
  --name la-prod-orderprocessing \
  --resource-group rg-prod-logicapps \
  --generic-configurations '{"healthCheckPath": "/api/health"}'

Rollback Strategy

If issues are detected after a swap:

# Swap back immediately
az webapp deployment slot swap \
  --name la-prod-orderprocessing \
  --resource-group rg-prod-logicapps \
  --slot staging \
  --target-slot production

Screenshot: Deployment slots in Azure Portal

Best Practices

  • Never commit local.settings.json — add it to .gitignore
  • Use Managed Identity for connections instead of connection strings where possible
  • Parameterize everything — no hardcoded environment values in workflow definitions
  • Version your connections — track connections.json in source control with @appsetting() references
  • Test workflows locally before pushing to the pipeline
  • Use branch policies — require PR reviews and successful builds before merging to main
  • Monitor deployments — integrate Application Insights and set up alerts for failed runs

Troubleshooting

IssueSolution
Workflows not appearing after deployEnsure folder structure is correct; each workflow needs its own folder with workflow.json
Connection auth failuresVerify Managed Identity has correct RBAC roles on target resources
Slot swap failsCheck health check endpoint returns 200; review staging slot logs
Pipeline timeoutIncrease timeout on deployment tasks; check if Logic App is stuck starting

Summary

You've built a complete CI/CD pipeline for Logic Apps Standard that includes:

  • ✅ Structured project with proper separation of concerns
  • ✅ Infrastructure as Code with Bicep
  • ✅ Multi-stage pipeline with dev, staging, and production
  • ✅ Environment-specific configuration via variable groups and Key Vault
  • ✅ Zero-downtime deployments using slot swapping
  • ✅ Manual approval gates for production

← Previous: Stateful vs Stateless Workflows | Next: VNet & Private Endpoints →