Posts GitHub Actions: How to use reusable workflows to avoid code duplication when deploying to Azure App Service in multiple environments
Post
Cancel

GitHub Actions: How to use reusable workflows to avoid code duplication when deploying to Azure App Service in multiple environments

In this article, we’ll see how we can use reusable workflows to avoid code duplication when deploying a Docker image (an ASP.Net Core app) to an Azure App Service - Web App for Container in 2 different envrionments (development and production).

This article is a follow-up of what was done previously in the article where we saw how to pull a Docker Image from Azure Container Registry and deploy to Azure App Service in multiple environments.

Desktop View

The main focus of this article will be how to create and use reusable workflows. For the whole setup of the environment, I’d strongly suggest that you first refer to the previous article.

Pre-Requisites

  • Basic knowledge of Azure
  • Basic knowledge of Git
  • Basic knowledge of GitHub Actions
  • Basic knowledge of YAML syntax
  • An Azure subscription (you can get one free here)
  • A GitHub account (you can sign up here)

1. Assess The Current Workflow

First, let’s do a quick review of our current worflow and see what could be improved. As a quick reminder, we’re building a Docker image of an ASP.Net Core app, push that image to an Azure Container Registry and then pull that image to deploy it to 2 environments (Development and Production):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
name: SampleWebApp CICD

on: 
  push:
    branches: [master]
  pull_request:
    branches: [master]

env:
  DOCKER_REGISTRY_URL: eastcanadadevtestacr.azurecr.io

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Azure Container Registry Login
        uses: Azure/docker-login@v1
        with:
          # Container registry server url
          login-server: ${{ env.DOCKER_REGISTRY_URL }}
          # Container registry username
          username: ${{ secrets.ACR_USERNAME }}
          # Container registry password
          password: ${{ secrets.ACR_PASSWORD }}

      - name: Build and Push the Docker image
        run: |
          docker build ./SampleWebApplication -t ${{ env.DOCKER_REGISTRY_URL }}/samplewebapp:${{ github.run_id }}
          docker push ${{ env.DOCKER_REGISTRY_URL }}/samplewebapp:${{ github.run_id }}

      - name: Package ARM Template
        uses: actions/upload-artifact@v2
        with: 
          name: arm-templates
          path: ${{ github.workspace }}/SampleWebApplication/azuredeploy.json
          retention-days: 5

  dev:
    runs-on: ubuntu-latest

    needs: build
    environment: development

    env:
      ENVIRONMENT_PREFIX: dev

      SERVER_FARM_RESOURCE_GROUP_NAME: DEV-ACRCD-RG #insert the name of your own RG here
      WEBAPP_LOCATION: canadacentral #insert your desired location here
      WEBAPP_SKU: Basic
      WEBAPP_SKU_CODE: B1

      TAG_ENVIRONMENT: DEVELOPMENT

    steps:

      - name: Download ARM Templates
        uses: actions/download-artifact@v3
        with:
          name: arm-templates

      - name: "Login via Azure CLI"
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy ARM Template
        uses: azure/arm-deploy@v1
        with:
          subscriptionId: ${{ secrets.SUBSCRIPTION_ID }}
          resourceGroupName: ${{ env.SERVER_FARM_RESOURCE_GROUP_NAME }}
          deploymentMode: Incremental
          template: azuredeploy.json
          parameters: > 
            subscriptionId="${{ secrets.SUBSCRIPTION_ID }}"
            name="${{ env.ENVIRONMENT_PREFIX }}-devops-samplewebapp"
            location="${{ env.WEBAPP_LOCATION }}"
            hostingPlanName="${{ env.ENVIRONMENT_PREFIX }}-asp"
            serverFarmResourceGroup="${{ env.SERVER_FARM_RESOURCE_GROUP_NAME }}"
            sku="${{ env.WEBAPP_SKU }}"
            skuCode="${{ env.WEBAPP_SKU_CODE }}"
            linuxFxVersion="DOCKER|${{ env.DOCKER_REGISTRY_URL }}/samplewebapp:${{ github.run_id }}"
            dockerRegistryUrl="${{ env.DOCKER_REGISTRY_URL }}"
            dockerRegistryUsername="${{ secrets.ACR_USERNAME }}"
            dockerRegistryPassword="${{ secrets.ACR_PASSWORD }}"
            tag_environment="${{ env.TAG_ENVIRONMENT }}"

  prd:
    runs-on: ubuntu-latest

    needs: dev
    environment: production

    env:
      ENVIRONMENT_PREFIX: prd

      SERVER_FARM_RESOURCE_GROUP_NAME: PRD-ACRCD-RG #insert the name of your own RG here
      WEBAPP_LOCATION: canadacentral #insert your desired location here
      WEBAPP_SKU: Basic
      WEBAPP_SKU_CODE: B1

      TAG_ENVIRONMENT: PRODUCTION

    steps:

      - name: Download ARM Templates
        uses: actions/download-artifact@v3
        with:
          name: arm-templates

      - name: "Login via Azure CLI"
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy ARM Template
        uses: azure/arm-deploy@v1
        with:
          subscriptionId: ${{ secrets.SUBSCRIPTION_ID }}
          resourceGroupName: ${{ env.SERVER_FARM_RESOURCE_GROUP_NAME }}
          deploymentMode: Incremental
          template: azuredeploy.json
          parameters: > 
            subscriptionId="${{ secrets.SUBSCRIPTION_ID }}"
            name="${{ env.ENVIRONMENT_PREFIX }}-devops-samplewebapp"
            location="${{ env.WEBAPP_LOCATION }}"
            hostingPlanName="${{ env.ENVIRONMENT_PREFIX }}-asp"
            serverFarmResourceGroup="${{ env.SERVER_FARM_RESOURCE_GROUP_NAME }}"
            sku="${{ env.WEBAPP_SKU }}"
            skuCode="${{ env.WEBAPP_SKU_CODE }}"
            linuxFxVersion="DOCKER|${{ env.DOCKER_REGISTRY_URL }}/samplewebapp:${{ github.run_id }}"
            dockerRegistryUrl="${{ env.DOCKER_REGISTRY_URL }}"
            dockerRegistryUsername="${{ secrets.ACR_USERNAME }}"
            dockerRegistryPassword="${{ secrets.ACR_PASSWORD }}"
            tag_environment="${{ env.TAG_ENVIRONMENT }}" 

As you can see, the “dev” and “prd” stages have the exact common set of steps:

  1. Download ARM Template
  2. Login via Azure CLI
  3. Deploy ARM Template

The only difference between the 2 stages is the parameters that we provide to the ARM template during the deployment.

2. Create a Reusable Workflow

To avoid code duplication, let’s put these 3 common steps in a reusable workflow. To do so, let’s create a new file under the path “.github\workflows” and name it “reusable-web-app-container-deploy.yml”. Our file structure should now look like this:

Desktop View

In the newly created file, let’s add the name and inputs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name: Web App for Containers Deployment

on: 
  workflow_call:
    inputs:
      ENVIRONMENT_PREFIX:
        required: true
        type: string
      SERVER_FARM_RESOURCE_GROUP_NAME:
        required: true
        type: string
      WEBAPP_LOCATION:
        required: true
        type: string
      WEBAPP_SKU:
        required: true
        type: string
      WEBAPP_SKU_CODE:
        required: true
        type: string
      ENVIRONMENT:
        required: true
        type: string 

For the workflow to be reusable, notice how we have to have the value “workflow_call” in “on”:

1
2
on:
  workflow_call:

Next, let’s add the common steps for deploying the Web App for Containers. We refer to the inputs by using the “input” context name and secrets are referred by (you’ve guessed it!) the “secrets” context name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
jobs:
  deploy: 
    runs-on: ubuntu-latest
    environment: ${{ inputs.ENVIRONMENT}}

    steps:
      - name: Download ARM Templates
        uses: actions/download-artifact@v3
        with:
          name: arm-templates

      - name: "Login via Azure CLI"
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy ARM Template
        uses: azure/arm-deploy@v1
        with:
          subscriptionId: ${{ secrets.SUBSCRIPTION_ID }}
          resourceGroupName: ${{ inputs.SERVER_FARM_RESOURCE_GROUP_NAME }}
          deploymentMode: Incremental
          template: azuredeploy.json
          parameters: > 
            subscriptionId="${{ secrets.SUBSCRIPTION_ID }}"
            name="${{ inputs.ENVIRONMENT_PREFIX }}-devops-samplewebapp"
            location="${{ inputs.WEBAPP_LOCATION }}"
            hostingPlanName="${{ inputs.ENVIRONMENT_PREFIX }}-asp"
            serverFarmResourceGroup="${{ inputs.SERVER_FARM_RESOURCE_GROUP_NAME }}"
            sku="${{ inputs.WEBAPP_SKU }}"
            skuCode="${{ inputs.WEBAPP_SKU_CODE }}"
            linuxFxVersion="DOCKER|${{ secrets.ACR_URL }}/samplewebapp:${{ github.run_id }}"
            dockerRegistryUrl="${{ secrets.ACR_URL }}"
            dockerRegistryUsername="${{ secrets.ACR_USERNAME }}"
            dockerRegistryPassword="${{ secrets.ACR_PASSWORD }}"
            tag_environment="${{ inputs.ENVIRONMENT }}" 

Here’s how the whole reusable workflow looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
name: Web App for Containers Deployment

on: 
  workflow_call:
    inputs:
      ENVIRONMENT_PREFIX:
        required: true
        type: string
      SERVER_FARM_RESOURCE_GROUP_NAME:
        required: true
        type: string
      WEBAPP_LOCATION:
        required: true
        type: string
      WEBAPP_SKU:
        required: true
        type: string
      WEBAPP_SKU_CODE:
        required: true
        type: string
      ENVIRONMENT:
        required: true
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.ENVIRONMENT}}

    steps:
      - name: Download ARM Templates
        uses: actions/download-artifact@v3
        with:
          name: arm-templates

      - name: "Login via Azure CLI"
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy ARM Template
        uses: azure/arm-deploy@v1
        with:
          subscriptionId: ${{ secrets.SUBSCRIPTION_ID }}
          resourceGroupName: ${{ inputs.SERVER_FARM_RESOURCE_GROUP_NAME }}
          deploymentMode: Incremental
          template: azuredeploy.json
          parameters: > 
            subscriptionId="${{ secrets.SUBSCRIPTION_ID }}"
            name="${{ inputs.ENVIRONMENT_PREFIX }}-devops-samplewebapp"
            location="${{ inputs.WEBAPP_LOCATION }}"
            hostingPlanName="${{ inputs.ENVIRONMENT_PREFIX }}-asp"
            serverFarmResourceGroup="${{ inputs.SERVER_FARM_RESOURCE_GROUP_NAME }}"
            sku="${{ inputs.WEBAPP_SKU }}"
            skuCode="${{ inputs.WEBAPP_SKU_CODE }}"
            linuxFxVersion="DOCKER|${{ secrets.ACR_URL }}/samplewebapp:${{ github.run_id }}"
            dockerRegistryUrl="${{ secrets.ACR_URL }}"
            dockerRegistryUsername="${{ secrets.ACR_USERNAME }}"
            dockerRegistryPassword="${{ secrets.ACR_PASSWORD }}"
            tag_environment="${{ inputs.ENVIRONMENT }}" 

Now that our reusable workflow is done, the next step would be to rework our “caller” workflow a bit.

3. Rework of The "Caller" Workflow

Now, let’s go back our original “caller” workflow (samplewebapp-cicd.yml).

First, let’s put our registry url as a GitHub Actions secret:

Desktop View

Note: Even though the registry url is not technically sensitive data, for simplicity purposes of this article and to easily access that value, let’s do that for now. Personally, I would’ve prefer to declare that url as an environment variable in the caller workflow and pass it to the reusable workflow but at the time of writing this article, which is June 2022, environment variables are not supported as inputs to reusable workflows in GitHub Actions.

Once that’s done, let’s remove the below environment variable:

1
2
env:
  DOCKER_REGISTRY_URL: eastcanadadevtestacr.azurecr.io

And replace all references to that registry url by using the “secrets” context

1
"${{ secrets.ACR_URL }}"

Now, for the most satisfying part (at least for me), let’s remove the big chunks of duplicated code in our “caller” workflow at our “dev” and “prd” stages and call the reusable workflow instead. As a result, the “dev” and “prd” stages will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  dev:
    needs: build
    uses: ./.github/workflows/reusable-web-app-container-deploy.yml
    with:
      ENVIRONMENT_PREFIX: dev
      SERVER_FARM_RESOURCE_GROUP_NAME: DEV-ACRCD-RG #insert the name of your own RG here
      WEBAPP_LOCATION: canadacentral #insert your desired location here
      WEBAPP_SKU: Basic
      WEBAPP_SKU_CODE: B1
      ENVIRONMENT: development
    secrets: inherit

  prd:
    needs: dev
    uses: ./.github/workflows/reusable-web-app-container-deploy.yml
    with:
      ENVIRONMENT_PREFIX: prd
      SERVER_FARM_RESOURCE_GROUP_NAME: PRD-ACRCD-RG #insert the name of your own RG here
      WEBAPP_LOCATION: canadacentral #insert your desired location here
      WEBAPP_SKU: Basic
      WEBAPP_SKU_CODE: B1
      ENVIRONMENT: production
    secrets: inherit

The value of the key “uses:” represents the path of the reusable workflow and “with:” has all the inputs we want to provide to the reusable workflow. For inputs, the data type of the input value must match the type specified in the called workflow (either boolean, number, or string).

Finally, for the “secrets:” key, the value “inherit” means that we’re implicitly passing all the secrets to the reusable workflow. Something worth mentioning and noticing is that because the secrets are inherited, we don’t have to specify them in the “on” key in the reusable workflow!

4. Demo of the Whole Flow

Once we’re done editing our workflows, let’s push them to our repository and it should trigger our “samplewebapp-cicd.yml” workflow (under the “Actions” tab in GitHub):

Desktop View

Once the “build” stage is done, the “dev” stage automatically starts and will deploy the web app to the development RG:

Desktop View

You can click on the stage to see the progress:

Desktop View

Once the deployment is successful, you can validate the web app in the portal in the development RG:

Desktop View

You can navigate to the web app’s url as well to see that the web app is up and running:

Desktop View

Now that we’ve validated the “dev” stage, we can go back to our workflow in GitHub. Notice that the “prd” stage is pending the review of an approver:

Desktop View

To review and approve the deployment, click on “Review deployments”:

Desktop View

Upon approval, the “prd” job starts:

Desktop View

Once successful, we can validate that the resources have been correctly deployed in the portal like we did for “dev”:

Desktop View Desktop View

Conclusion

There you have it! We’ve been able to leverage reusable workflows in GitHub Actions to avoid code duplication in the deployment stages of our workflow and therefore, making them cleaner and easier to maintain in the long run.

I hope this article has been useful to you! Again, and as always, I’d love to hear your feedback and if you have any questions or something you’d like to add based on your experience, please let me know in the comments section below!

If you’d like to see more content like this in the future and keep in touch, feel free to follow me on Twitter and LinkedIn!

This post is licensed under CC BY 4.0 by the author.