Deploy Single Tenant (Standard) Azure Logic Apps via Azure DevOps CI/CD
More organisations are trending towards using low code/ no code cloud-native apps than traditional development. Among many choices, LogicApp has become the go-to choice for many integration workflows due to the large eco-system of configurable connectors to various systems. The ability to do the development through interactive designer UI through ClickOps has made it even more popular among business users. However, like any critical business application, we must use proper DevOps CI/CD practices to maintain consistency across environments by deploying these workflows faster and confidently.
This blog will give an overview to understand What LogicApps are, what flavours it offers and step through package deployment steps using Azure DevOps pipelines.
Single-Tenant (Standard) Logic Apps
Compared to Multi-Tenant (Consumption) Azure Logic Apps, Standard Logic Apps are suitable when it demands better isolation, performance and control over the executing environment. Unlike Logic App per workflow in the Consumption model, Standard Logic App can host unlimited workflows. It also means rather than combining one deployment for the application infrastructure and the workflows, the Standard Logic Apps model separates the concerns of deploying infrastructure and workflows into two different flows.
Logic App Project Structure
Standard Logic Apps require maintaining separate Infrastructure as Code (IaC) which I usually maintain under env/main.bicep
. The root src/MyStandardLogicApp
of the logic app source contains a host configuration file, a connection definition file, parameter definitions and a subfolder for each workflow containing the workflow definition.
I have structured my code example related to this blog as below. When you follow through with my example code snippets, I will refer to files relative to this structure.
root/
|-- .azuredevops/
| |-- pipelines/
|-- |-- |-- azure-pipelines.yml
|-- env/
| |-- main.bicep
|-- src/
| |-- MyStandardLogicApp/
|-- |-- |-- WorkflowName1/
|-- |-- |-- |-- workflow.json
|-- |-- |-- WorkflowName2/
|-- |-- |-- |-- workflow.json
|-- |-- |-- .funcignore
|-- |-- |-- connections.json
|-- |-- |-- host.json
|-- |-- |-- parameters.json
Infrastructure as Code (IaC)
A Standard Logic App requires an App Service Plan or Environment, a storage account to host the logic app code package and an Azure Function base website resource at minimum. I am extending this further by having a User Managed Identity, a Key Vault, AppInsights and Log Analytics Workspace. But this is entirely optional.
Note: Through extended exercises, I demonstrate how sensitive information such as Storage connection string, AppInsights instrument key and connection strings are securely retrieved from Key Vault in Logic App Configuration. Though it is not necessary, developers often take security lightly.
/*
------------------------------------------------
Parameters
------------------------------------------------
*/
@allowed([ 'dev', 'test', 'prod' ])
@description('Required. Environment short name.')
param environmentShortName string
@description('Required. Resource deployment location.')
param location string = resourceGroup().location
@description('Optional. Tag values for the resource.')
param resourceTags object = {
Environment: toLower(environmentShortName)
Solution: 'Logic App DevOps Demo'
}
/*
------------------------------------------------
Variables
------------------------------------------------
*/
var appStackName = 'rkt-lapp-devops'
var managedIdentityName = 'mid-${appStackName}-${environmentShortName}'
var logAnalyticsWorkspaceName = 'log-${appStackName}-${environmentShortName}'
var appInsightsName = 'appi-${appStackName}-${environmentShortName}'
var keyVaultName = 'kv-${appStackName}-${environmentShortName}'
var appServicePlanName = 'asp-${appStackName}-lapp-${environmentShortName}'
var lappName = 'lapp-${appStackName}-${environmentShortName}'
var storageAccountName = 'stgrktlogicappdevops${environmentShortName}'
var appiInstrumentationKeyName = 'appinsights-instrumentation-key'
var appiConnectionStringKeyName = 'appinsights-connection-string'
var storageSecretKeyName = '${appStackName}-stg-connection-string'
var storageSku = 'Standard_LRS'
var logicAppSku = 'WS1'
var fileShares = [
lappName
]
/*
------------------------------------------------
Managed Identity
------------------------------------------------
*/
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: managedIdentityName
location: location
tags: resourceTags
}
/*
------------------------------------------------
Log Analytics Workspace
------------------------------------------------
*/
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: logAnalyticsWorkspaceName
location: location
tags: resourceTags
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 120
features: {
searchVersion: 1
legacy: 0
enableLogAccessUsingOnlyResourcePermissions: true
}
}
}
/*
------------------------------------------------
Application Insights
------------------------------------------------
*/
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: appInsightsName
location: location
kind: 'web'
tags: resourceTags
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalyticsWorkspace.id
}
}
/*
------------------------------------------------
Storage Account and Containers
------------------------------------------------
*/
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
name: storageAccountName
location: location
sku: {
name: storageSku
}
kind: 'StorageV2'
tags: resourceTags
properties: {
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
networkAcls: {
bypass: 'AzureServices'
defaultAction: 'Allow'
}
}
resource fileService 'fileServices@2022-09-01' = {
name: 'default'
resource fileShare 'shares@2022-09-01' = [for fileShareName in fileShares: {
name: fileShareName
}]
}
}
/*
------------------------------------------------
Key Vault
------------------------------------------------
*/
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: keyVaultName
location: location
tags: resourceTags
properties: {
tenantId: subscription().tenantId
sku: {
family: 'A'
name: 'standard'
}
enabledForTemplateDeployment: true
accessPolicies: [
{
objectId: managedIdentity.properties.principalId
tenantId: subscription().tenantId
permissions: {
secrets: [
'all'
'purge'
]
}
}
{
objectId: logicApp.identity.principalId
tenantId: tenant().tenantId
permissions: {
secrets: [
'Get'
'List'
]
}
}
]
}
resource kvsAppInsightsInstrumentationKey 'secrets@2023-02-01' = {
name: appiInstrumentationKeyName
properties: {
value: appInsights.properties.InstrumentationKey
}
}
resource kvsAppInsightsConnectionStringKey 'secrets@2023-02-01' = {
name: appiConnectionStringKeyName
properties: {
value: appInsights.properties.ConnectionString
}
}
resource kvsStorageSecret 'secrets@2023-02-01' = {
name: storageSecretKeyName
properties: {
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
}
}
}
/*
------------------------------------------------
App Service Plans
------------------------------------------------
*/
resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = {
name: appServicePlanName
location: location
kind: 'windows'
sku: {
name: logicAppSku
tier: 'WorkflowStandard'
}
tags: resourceTags
}
/*
------------------------------------------------
Logic App
------------------------------------------------
*/
resource logicApp 'Microsoft.Web/sites@2022-09-01' = {
name: lappName
location: location
kind: 'functionapp,workflowapp'
tags: union(resourceTags, {
'hidden-link: /app-insights-resource-id': appInsights.id
})
identity: {
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
type: 'SystemAssigned, UserAssigned'
}
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
netFrameworkVersion: 'v4.6'
functionsRuntimeScaleMonitoringEnabled: false
}
}
}
/*
------------------------------------------------
App Settings of logic app
------------------------------------------------
*/
var storageAccountKvReference = '@Microsoft.KeyVault(vaultName=${keyVault.name};SecretName=${storageSecretKeyName})'
var appInsightsInstrumentationKeyKvReference = '@Microsoft.KeyVault(vaultName=${keyVault.name};SecretName=${appiInstrumentationKeyName})'
var appInsightsConnectionStringKvReference = '@Microsoft.KeyVault(vaultName=${keyVault.name};SecretName=${appiConnectionStringKeyName})'
resource logicAppSettings 'Microsoft.Web/sites/config@2022-03-01' = {
parent: logicApp
name: 'appsettings'
properties: {
APP_KIND: 'workflowApp'
AzureFunctionsJobHost__extensionBundle__id: 'Microsoft.Azure.Functions.ExtensionBundle.Workflows'
AzureFunctionsJobHost__extensionBundle__version: '[1.*, 2.0.0)'
AzureWebJobsSecretStorageType: 'Files'
AzureWebJobsStorage: storageAccountKvReference
APPINSIGHTS_INSTRUMENTATIONKEY: appInsightsInstrumentationKeyKvReference
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionStringKvReference
FUNCTIONS_EXTENSION_VERSION: '~4'
FUNCTIONS_V2_COMPATIBILITY_MODE: 'true'
FUNCTIONS_WORKER_RUNTIME: 'node'
WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: storageAccountKvReference
WEBSITE_CONTENTSHARE: lappName
WEBSITE_CONTENTOVERVNET: '1'
WEBSITE_NODE_DEFAULT_VERSION: '~16'
}
}
output keyVaultName string = keyVault.name
output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.name
output appInsightsName string = appInsights.name
output managedIdentityName string = managedIdentity.name
output storageAccountName string = storageAccount.name
output appServicePlanName string = appServicePlan.name
output logicAppName string = logicApp.name
CI/CD Pipeline
Package Logic App
For Logic App, there is no build step. We need to zip-package the target folder containing definitions and workflows and upload it as a build artifact to use in the release stage.
Important: When packaging the logic app folder, ensure that files such as local.settings.json
is not included with any sensitive credentials used during local development. In fact, any settings in that file will not be honoured by the logic app runtime. Hence, that file has no value.
- job: BuildLogicApp
displayName: 'Build LogicApp'
steps:
- task: ArchiveFiles@2
displayName: 'Package Logic App'
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/src/MyStandardLogicApp'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/lapp/MyStandardLogicApp.zip'
replaceExistingArchive: true
- task: PublishPipelineArtifact@1
displayName: 'Publish Logic App Artifacts'
inputs:
path: '$(Build.ArtifactStagingDirectory)/lapp'
artifact: lapp
publishLocation: pipeline
Deploy Logic App
The deployment is straightforward as deploying an Azure Function App. It is the same pipeline task. The appName
need to be set to the logic app name; in this case, the value has been returned by infrastructure deployment output.
- task: AzureFunctionApp@1
displayName: 'Deploy Logic App'
inputs:
azureSubscription: $(azureServiceConnection)
appType: 'functionApp'
appName: '$(logicAppName)'
package: '$(Pipeline.Workspace)/lapp/MyStandardLogicApp.zip'
deploymentMethod: 'zipDeploy'
Final Thoughts
Microsoft provides excellent documentation and use cases. However, almost all projects I dealt with Logic App presented various challenges in using different connections and parameters. Especially with Bicep, some types are missing, and I had to find the information with some R&D and sleepless nights.
We just briefly started, and more work needs to be done to make the logic app for production ready. I wanted to keep this blog post focused only on deploying the Logic App, and in future posts, I will develop more topics based on this foundation and keep updating this post.
Leave a comment