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.
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:
- Download ARM Template
- Login via Azure CLI
- 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:
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:
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):
Once the “build” stage is done, the “dev” stage automatically starts and will deploy the web app to the development RG:
You can click on the stage to see the progress:
Once the deployment is successful, you can validate the web app in the portal in the development RG:
You can navigate to the web app’s url as well to see that the web app is up and running:
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:
To review and approve the deployment, click on “Review deployments”:
Upon approval, the “prd” job starts:
Once successful, we can validate that the resources have been correctly deployed in the portal like we did for “dev”:
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!