Posts GitHub Actions: How to pull a Docker Image from Azure Container Registry and deploy to Azure App Service in multiple environments
Post
Cancel

GitHub Actions: How to pull a Docker Image from Azure Container Registry and deploy to Azure App Service in multiple environments

Desktop View

In this article, we’ll see how we can pull a Docker image from an Azure Container Registry (ACR) using GitHub Actions Worflows and deploy it 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 build and push a Docker image to an ACR using GitHub Actions.

Therefore, we’re assuming that you already have an image in an Azure Container Registry (in our example from the previous article, we had an ASP.Net Core app) and that you have your project stored in a GitHub repository. If that’s not the case, please refer to the previous article for more background about what was done.

Pre-Requisites

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

1. Create the Resource Groups

First, we’ll want to create the 2 resource groups that will represent our 2 environments. There are multiple ways we can achieve this but, for our example, we’ll use the Azure CLI (you can run these commands directly in the Azure Cloud Shell):

1
2
3
4
5
6
7
8
9
10
11
#Optional - This command is only necessary if you're using a local terminal
az login

DEV_RG_NAME="DEV-ACRCD-RG" #use your own name for this variable
PRD_RG_NAME="PRD-ACRCD-RG" #use your own name for this variable

LOCATION="canadaeast" #use the location that makes the most sense for you

#Creates the resource groups 
az group create --name $DEV_RG_NAME --location $LOCATION
az group create --name $PRD_RG_NAME --location $LOCATION

2. Create an Azure Service Principal

In order for GitHub Actions to be able to deploy to our RGs, we’ll need to create service principal with Contributor access on our RGs that will represent GitHub Actions in Azure. In the Azure Cloud Shell (or on a local terminal), run the following command for all your your RGs:

1
2
3
4
SP_Name="GitHubActions-SP"

az ad sp create-for-rbac --name $SP_Name --sdk-auth --role contributor \
    --scopes /subscriptions/{YOUR-SUBSCRIPTION-ID}/resourceGroups/{YOU-RG-NAME}

In the example above, replace the placeholders with your subscription ID and resource group name.

Note: for the purpose of simplicity for this demo, we’ll use the same Service Principal for all RGs, but in a real-life scenario, you might want to have a dedicated Service Principal per environment or realm for better security management.

Once you run the above command, you should get a JSON response in the terminal. You will only need the sections with the clientId, clientSecret, subscriptionId, and tenantId values. Take note of this JSON object as you will need it in a later section:

1
2
3
4
5
6
7
{
    "clientId": "<GUID>",
    "clientSecret": "<GUID>",
    "subscriptionId": "<GUID>",
    "tenantId": "<GUID>",
    (...)
  }

3. Create the App Service ARM Template

Now that we have our RGs setup, let’s create the ARM template that will deploy our ASP.Net app on a Web App for Containers:

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
{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "subscriptionId": {
            "type": "string"
        },
        "name": {
            "type": "string"
        },
        "location": {
            "type": "string"
        },
        "hostingPlanName": {
            "type": "string"
        },
        "serverFarmResourceGroup": {
            "type": "string"
        },
        "alwaysOn": {
            "type": "bool"
        },
        "sku": {
            "type": "string"
        },
        "skuCode": {
            "type": "string"
        },
        "workerSize": {
            "type": "string"
        },
        "workerSizeId": {
            "type": "string"
        },
        "numberOfWorkers": {
            "type": "string"
        },
        "linuxFxVersion": {
            "type": "string"
        },
        "dockerRegistryUrl": {
            "type": "string"
        },
        "dockerRegistryUsername": {
            "type": "string"
        },
        "dockerRegistryPassword": {
            "type": "securestring"
        },
        "dockerRegistryStartupCommand": {
            "type": "string",
            "defaultValue": ""
        },
        "tag_environment": {
            "type": "string"
        }
    },
    "variables": {},
    "resources": [
        {
            "apiVersion": "2018-11-01",
            "name": "[parameters('name')]",
            "type": "Microsoft.Web/sites",
            "location": "[parameters('location')]",
            "tags": {
                "ENVIRONMENT": "[parameters('tag_environment')]"
            },
            "dependsOn": [
                "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
            ],
            "properties": {
                "name": "[parameters('name')]",
                "siteConfig": {
                    "appSettings": [
                        {
                            "name": "DOCKER_REGISTRY_SERVER_URL",
                            "value": "[parameters('dockerRegistryUrl')]"
                        },
                        {
                            "name": "DOCKER_REGISTRY_SERVER_USERNAME",
                            "value": "[parameters('dockerRegistryUsername')]"
                        },
                        {
                            "name": "DOCKER_REGISTRY_SERVER_PASSWORD",
                            "value": "[parameters('dockerRegistryPassword')]"
                        },
                        {
                            "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
                            "value": "false"
                        }
                    ],
                    "linuxFxVersion": "[parameters('linuxFxVersion')]",
                    "appCommandLine": "[parameters('dockerRegistryStartupCommand')]",
                    "alwaysOn": "[parameters('alwaysOn')]"
                },
                "serverFarmId": "[concat('/subscriptions/', parameters('subscriptionId'),'/resourcegroups/', parameters('serverFarmResourceGroup'), '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
                "clientAffinityEnabled": false,
                "virtualNetworkSubnetId": null
            }
        },
        {
            "apiVersion": "2018-11-01",
            "name": "[parameters('hostingPlanName')]",
            "type": "Microsoft.Web/serverfarms",
            "location": "[parameters('location')]",
            "kind": "linux",
            "tags": {
                "ENVIRONMENT": "[parameters('tag_environment')]"
            },
            "dependsOn": [],
            "properties": {
                "name": "[parameters('hostingPlanName')]",
                "workerSize": "[parameters('workerSize')]",
                "workerSizeId": "[parameters('workerSizeId')]",
                "numberOfWorkers": "[parameters('numberOfWorkers')]",
                "reserved": true,
                "zoneRedundant": false
            },
            "sku": {
                "Tier": "[parameters('sku')]",
                "Name": "[parameters('skuCode')]"
            }
        }
    ]
}

This ARM template will deploy an App Service Plan as well as the Web App for Container itself from a specific ACR. I’ve also added a parameter “tag_environment” for us to tag our resources to later identiy in which environment they reside. We’ll save this template in the same folder as our .csproj file with the name “azuredeploy.json”.

4. Get the ACR Credentials

Note: if you’re coming from the previous article, you can skip this section

Navigate to your ACR in the Azure portal, go to the “Access Keys”, make sure to enable “Admin user” and take note of the username and password :

Desktop View

Also, you’ll want to take note of your subscription ID as well:

Desktop View

5. Add our Secrets in GitHub

Next, we’ll need to store the ACR’s credentials, subscription ID and the Service Principal info (noted earlier) as secrets in GitHub. To do so, go to the “Settings” tab and select Secrets > Actions as shown below and “New repository secret”:

Desktop View

Once added, they should appear like this and be ready to be used:

Desktop View

Note: Another way we could’ve done this is to put the secrets in an Azure Keyvault and refer to them from our GitHub Workflow

6. Create Environments in GitHub

We’ll now setup 2 environments in GitHub that will represent our development and production environments. Also, by creating environments in GitHub, it will also give us the chance to setup pre-deployment manual approvers for a high environment such as production (like we would want to have in a real-life scenario).

Note: As of May 2022, environments, environment protection rules, and environment secrets are available in public repositories for all products. For access to environments in private repositories, you must use GitHub Enterprise

To set the environments, go to “Settings” tab and select Environments and “New environments”. For the fictional “production” environments, I’ll add myself as a pre-deployment reviewer and for all environments, let’s just specify the deployment branch as “master” (again, this is for the purpose of simplicity).

Desktop View

Finally, it’s also possible to add environment secrets if let’s say you had a different subscription id for each environment, but we’ll ignore that for this example.

Desktop View

We now have our 2 environments ready to be used by our workflow:

Desktop View

7. Create the GitHub Workflow

Now it’s time to create our workflow in GitHub. To do this, we’ll build over the workflow that we’ve built in the previous article. As a reminder, this is the workflow we have so far that basically builds and pushes an ASP.Net app Docker image to an ACR:

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
name: SampleWebApp CICD

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

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: eastcanadadevtestacr.azurecr.io
        # 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 eastcanadadevtestacr.azurecr.io/samplewebapp:${{ github.run_id }}
       docker push eastcanadadevtestacr.azurecr.io/samplewebapp:${{ github.run_id }} 

We’ll put our ACR url in an environment variable that will be scoped to the whole workflow (since we’ll use it in multiple places) and add the “dev” and “prd” stages where we’ll deploy the ARM template. The workflow should then look like the below:

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 }}" 

Note: You’ll notice that the “dev” and “prd” stages have the same steps and that only the parameters that are provided to the ARM template differ. We’ll have a look in another article (Coming Soon!) on how we can leverage reusable workflows to avoid code duplication but for now, let’s keep the workflow as simple as possible to focus on the deployment part.

You’ll notice that our 2 stages have the following elements:

1
2
needs: <build/dev>
environment: <development/production>

The “needs” key specify a job that must be successfully completed before the current one can be executed.

The “environment” key links our stage to one of the environment we’ve created earlier and the stage will be subject to any rules configured for that environment. For example, if the environment requires reviewers (such as our production environment), the job will pause until one of the reviewers approves the job.

For the ARM template deployment step, we’re using the GitHub Action for ARM deployment.

8. Demo of the Whole Flow

Once we’re done editing our workflow, let’s push it to our repository and it should trigger our 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

Next Steps

Whew, we’ve made it! We were able to deploy a simple ASP.Net Web App from an Azure Container Registry to an App Service in Azure in 2 different environments using GitHub Actions! As mentionned previously, as a next step, something we could do to improve our workflow readability and maintainability would be to build a reusable workflow for all of our deployment stages. We’ll tackle this in the next article.

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.

GitHub Actions: How to build and push a Docker image to Azure Container Registry

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

Comments powered by Disqus.