Logic Apps deployment from Github with Powershell
Tags: azure, github, powershell
Published: 2024-02-28 06:45:36

## Context 

I was working with a startup that is building software to predict time series in a specific industry.  One of the big challenges for them is to integrate with systems of their customers.  

While they offer an API that can be called by customers to ingest data into their tenant, some of those customers, require that startup to go and fetch the data from one of their systems.  And yes, that smells like customer specific integration.

For that, we have set up several Logic Apps, that we isolate from the central part of the runtime.  And this post describes how we have structured those Logic Apps, and how we automatically deploy them from our automated Github workflow.

### Structure

As written above, the structure of the repo is in such a way that we have put all Logic Apps in a folder, called `Integrations`.  And at deployment time, we automatically take those workflows from the folder, leveraging the names of the sub folders, to build the right resource name (ensuring our naming conventions).  And those Logic Apps get deployed to a consumption Logic App. 

### Code

All code is available on https://github.com/SamVanhoutte/azure-logic-apps-deployment

#### Azure authentication

As with any other Github action, it is important to leverage the right credentials to authenticate against your Azure Subscription.  For that, the following script has to be executed in the Azure CLI, and the resulting lines should be kept as a secret in your Github environment.

```bash
az ad sp create-for-rbac --name "github-deploy-dev" --role contributor --scopes /subscriptions/b73995e3-caad-4882-8644-f2175789c3ff --sdk-auth 
```
The output looks like the following (which should be handled as a secret!). 

```json
{
  "clientId": "a8fa378b....",
  "clientSecret": "cOF8....",
  "subscriptionId": "b73995e3-caad-4882-8644-f2175789c3ff",
  "tenantId": "37e57300...."
}
```

**Note:** you can leave out the urls and just keep the first for relevant parts.

The output has been saved in my Github repo secret (with name `AZURE_CREDENTIALS` in the Development environment on my repo.

#### Github pipeline

To deploy our infrastructure, we default to Github workflows.

The result of our deployment pipeline is shown in the next screenshot:

![Github workflow result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9yrl8yffnp6lkagoe6ic.png) 

The definition of the Github workflow can be easily found in the [Github repo](https://github.com/SamVanhoutte/azure-logic-apps-deployment/blob/main/.github/workflows/deploy-environment.yaml).  But the most relevant sections have been copied here: 

```yaml
name: Deploy the Backend environment

on:
  workflow_call:
    inputs: 
    # Removed for brevity
jobs:
  Deploy-Azure-Backend-Resources:
    runs-on: ubuntu-latest
    if: always()
    environment: ${{ inputs.environment }}
    steps:
    # Log in to Azure
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: '${{ secrets.AZURE_CREDENTIALS }}'
        enable-AzPSSession: true
    # Removed for brevity
  Deploy-Integrations:
    runs-on: ubuntu-latest
    if: always()
    needs : [ Deploy-Azure-Backend-Resources ]
    environment: ${{ inputs.environment }}
    steps:
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: '${{ secrets.AZURE_CREDENTIALS }}'
        enable-AzPSSession: true
    - name: Download powershell files from pipeline artifact
      uses: actions/download-artifact@v3
      with:
        name: powershell
        path: ./artifacts/powershell
    - name: Download integration files
      uses: actions/download-artifact@v3
      with:
        name: integrations
        path: ./artifacts/integrations
    - name: Deploy integrations
      uses: azure/powershell@v1
      with:
        inlineScript: ./artifacts/powershell/deploy-logicapp-folder.ps1 -environmentAcronym ${{ inputs.environmentAcronym }} -location ${{ inputs.region }} -rootFolder './artifacts/integrations/'
        azPSVersion: "latest"
```

Some remarks about the above script:

- `enable-AzPSSession: true` ensures our Powershell script will be running authenticated against our Azure Subscription.
- We download the artifacts that we initially uploaded to the runtime storage.  We do this for our `powershell` folder & our `integrations` folder that contains our Logic Apps definitions.
- And in the last step, we are calling our Powershell script (that is explained in the next step) that will receive a reference to our `integrations` folder from our repo.

#### Powershell script

The following script takes the folder reference, walks the tree iteratively and deploys every Logic App definition to the corresponding resource group, applying the naming conventions, based on the folder structure. 

```powershell
param ($rootFolder, $location, $environmentAcronym)


Function Deploy-LogicAppDefinition($logicAppDefinitionFile, $location, $environmentAcronym, $locationAcronym)
{
    # This function takes a given workflow definition file 
    # and deploys it to the resource group 
    # (based on the folder, location & environment)
    # It will create or update the Logic App

    ### SET UP LOGIC APP NAME
    # Taking the file name and remove the folder
    $logicAppName = Split-Path $logicAppDefinitionFile -leaf

    # and remove the suffix to keep the logical name
    $logicAppName = $logicAppName.Replace('.definition.json', '')

    # and apply naming conventions
    $logicAppName = "$locationAcronym-$environmentAcronym-int-la-$logicAppName"

    ### SET UP RESOURCE GROUP NAME
    # Taking the directory name to which the json file belongs
    $logicAppType = Split-Path (Split-Path $logicAppDefinitionFile -Parent) -Leaf
    # and construct the resource group name
    $resourceGroupName = "$environmentAcronym-$locationAcronym-logapp-rg-int-$logicAppType"

    # Check if the resource group exists and create if not
    $existingRG = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue
    if(-Not $existingRG)
    {
        Write-Host "Creating resource group $resourceGroupName"   
        New-AzResourceGroup -Name $resourceGroupName -Location 'West Europe' -Force
    }

    ### CRUD OF LOGIC APP DEFINITION
    # Check if logic app exists
    $existingLogicApp = Get-AzLogicApp -ResourceGroupName $resourceGroupName -Name $logicAppName -ErrorAction SilentlyContinue
    if($existingLogicApp)
    {
        Write-Host "Update logic app $logicAppName to resource group $resourceGroupName"   
        Set-AzLogicApp -ResourceGroupName $resourceGroupName -Name $logicAppName -DefinitionFilePath $logicAppDefinitionFile -Force
    }
    else 
    {
        Write-Host "Create logic app $logicAppName to resource group $resourceGroupName"
        New-AzLogicApp -ResourceGroupName $resourceGroupName -Name $logicAppName -Location $location -DefinitionFilePath $logicAppDefinitionFile
    }
}

# Apply location acronym, based on location
switch ($location)
{
    'westeurope' { $locationAcronym = 'weu'}
    'northeurope' { $locationAcronym = 'neu'}
    'westus' { $locationAcronym = 'wus'}
    default { $locationAcronym = 'weu'}
}

# Start of logic
# Search through all folders for files ending with definition.json
Get-ChildItem -Path $rootFolder -Recurse -Filter *.definition.json |
    ForEach-Object {
        Deploy-LogicAppDefinition $_.FullName $location $locationAcronym $environmentAcronym
    }
```

## Conclusion

I believe it's crucial to apply naming conventions in every step of your deployment logic.  So, here we have a way to structure our logic apps in our repo and have them deployed, just because the appear in our repo (we don't have to update the pipeline or script for every new workflow).  

In another post, I will describe how a custom connector can be deployed and used in another Logic App.