Create an Azure Pipelines to deploy Docker image for Azure App Service
Tags: devops, docker, azure, appservice
Published: 2021-08-06 20:16:00

This article details how to containerize an app, create an App Service in Azure and configure it to pull the Docker image from the Azure Container Registry (ACR). How the Docker image gets deployed from our code to ACR is a result of us creating a deployment pipeline using Azure Pipelines. 

The benefit is instead of manually pushing the locally-built Docker image to ACR after some code changes, we now have an automated Continuous Integration/Continuous Deployment (CI/CD) process. 

The workflow can be described as follow: We write the code on our Dev computer, build and run the Docker image locally. Once we do a `git` push of the code to a `git` repo, Azure Pipelines performs the build on some agent machine and deploys the Docker image to ACR. Since the App Service has also already been configured for Continuous Deployment, it will pull the image from the ACR automatically. Essentially we have a pipeline from code to repo to Docker image to ACR to App Service.
 
1) We start with writing an app. What app to write is not the focal point of this article. But to have something to start with, we will create an out-of-the-box NextJS app using `npx create-next-app`. We could have done with an ASP.Net core app and everything will be just the same. So let's start:

```
npx create-next-app mysample
cd mysample
npm run build
```
2) We will create a Docker image for this and run in a local Docker container. To do so, we will create a `Dockerfile` and a `docker-compose.yml` file.

Here's the `Dockerfile`

```
FROM node:14 as BUILD_IMAGE
WORKDIR /var/www/app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN yarn build

# remove dev dependencies
RUN npm prune --production

FROM node:14
WORKDIR /var/www/app
COPY --from=BUILD_IMAGE /var/www/app/package.json ./package.json
COPY --from=BUILD_IMAGE /var/www/app/node_modules ./node_modules
COPY --from=BUILD_IMAGE /var/www/app/.next ./.next
COPY --from=BUILD_IMAGE /var/www/app/public ./public

EXPOSE 3000
CMD ["yarn", "start"]
```

And `docker-compose.yml` file
```
version: '3.4'

services:
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - 80:3000
    env_file: .env
    command:
      sh -c 'yarn start'
```
3) Now we can run the Docker locally with the command
```docker-compose up --build```
It should work if you point the browser to `http://localhost` (that's port 80 not 3000)

4) All the next steps that we're about to do can be done with the UI in [Azure Portal](portal.azure.com), but we will use [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) as much as possible. Also, I will name my `Resource Group` and `App Service Plan` as `MyResourceGroup` and `MyLinuxPlan` respectively. If you already have a Resource Group and App Service plan, you can continue using them. Or you can follow next step and substitute with names of your own choosing.

Let's create a Resource Group called `MyResourceGroup`
```
az group create -l westus -n MyResourceGroup
```
Now we create a Linux App Service Plan called `MyLinuxPlan` with 2 workers
```
az appservice plan create -g MyResourceGroup -n MyLinuxPlan \
    --is-linux --number-of-workers 2 --sku S1
```
To create a Resource Group, refer to this [link](https://docs.microsoft.com/en-us/cli/azure/group?view=azure-cli-latest#az_group_create).
To create an App Service Plan refer to this [link](https://docs.microsoft.com/en-us/cli/azure/appservice/plan?view=azure-cli-latest#az_appservice_plan_create)

5) Now we create an Azure Container Registry called `mysampleacr`
```
az acr create --name mysampleacr --resource-group \
    MyResourceGroup --sku Basic --admin-enabled true
```
At this point, we now have an ACR but no image has been pushed to it and no App Service has been created. This is where my approach starts to differ from that in Azure documentation. 

This is OK because we don't want to push the Docker image to ACR manually. Instead we will set up the Azure Pipeline now.

6) Let's go to [https://dev.azure.com](https://dev.azure.com), navigate to the `Pipeline` page and create a new Pipeline as shown in the next picture:
![Azure Pipeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98vdhl0x90nigi8sgojb.png)

7) Then specify `Where is you code?` when prompted.

8) At the page `Configure your pipeline`, scroll down and click on "Show more". Then select `Docker Build and push an image to Azure Container Registry`

9) Select your Subscription and `Continue`. You might be prompted to login again.

10) Now select your Container Registry. If you follow this example, earlier we name it `mysampleacr`. For image name, let's name it `mysampleimg`. Then click on `Validate and configure`

11) We should get a generated `azure-pipelines.yml` file with contents similar to the one below
```
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker

trigger:
- main

resources:
- repo: self

variables:
  # Container registry service connection established during pipeline creation
  dockerRegistryServiceConnection: 'b0...a4'
  imageRepository: 'mysampleimg'
  containerRegistry: 'mysampleacr.azurecr.io'
  dockerfilePath: '**/Dockerfile'
  tag: '$(Build.BuildId)'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

stages:
- stage: Build
  displayName: Build and push stage
  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)
    steps:
    - task: Docker@2
      displayName: Build and push an image to container registry
      inputs:
        command: buildAndPush
        repository: $(imageRepository)
        dockerfile: $(dockerfilePath)
        containerRegistry: $(dockerRegistryServiceConnection)
        tags: |
          $(tag)
```

We need to make a couple of changes and then we can `Save and run`.

In the above file, look for the line `tag: '$(Build.BuildId)'` and add a line below it. It will now show

```
tag: '$(Build.BuildId)'
latestTag: 'latest'
```
In the last 2 lines of the file, change from 
```
tags: |
  $(tag)
```
to
```
tags: |
  $(latestTag)
```

The resulting/modified `azure-pipelines.yml` file should now look something like
```
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker

trigger:
- main

resources:
- repo: self

variables:
  # Container registry service connection established during pipeline creation
  dockerRegistryServiceConnection: 'b0...a4'
  imageRepository: 'mysampleimg'
  containerRegistry: 'mysampleacr.azurecr.io'
  dockerfilePath: '**/Dockerfile'
  tag: '$(Build.BuildId)'
  latestTag: 'latest'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

stages:
- stage: Build
  displayName: Build and push stage
  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)
    steps:
    - task: Docker@2
      displayName: Build and push an image to container registry
      inputs:
        command: buildAndPush
        repository: $(imageRepository)
        dockerfile: $(dockerfilePath)
        containerRegistry: $(dockerRegistryServiceConnection)
        tags: |
          $(latestTag)
```
Now we can `Save and run`

12) When the run finishes, we can verify there is an image in the ACR by running the following commands (one by one)
```
az acr repository list -n mysampleacr

az acr repository show -n mysampleacr \
  --repository mysampleimg

az acr repository show -n mysampleacr  \
  --repository mysampleimg:latest
```
For more help on `az acr repository` commands, please refer to [https://docs.microsoft.com/en-us/cli/azure/acr/repository?view=azure-cli-latest#az_acr_repository_list](https://docs.microsoft.com/en-us/cli/azure/acr/repository?view=azure-cli-latest#az_acr_repository_list)

13) Now we can create an App Service and tell it to pull from the latest image `mysampleimg:latest` from the ACR `mysampleacr`.
```
az webapp create --resource-group MyResourceGroup --plan MyLinuxPlan --name mysample --deployment-container-image-name mysampleacr.azurecr.io/mysampleimg:latest
```
14) To ensure when a new image is pushed to the ACR, the App Service will automatically pull the latest image, go to [Azure Portal](https://portal.azure.com), navigate to the App Service blade and make sure `Continuous Deployment` is on.
![App Service Continuous Deployment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1iuowr923axifra0g2va.png)

References (A little difficult to read for me, but that's just my opinion):
[https://docs.microsoft.com/en-us/azure/app-service/quickstart-nodejs?pivots=platform-linux](https://docs.microsoft.com/en-us/azure/app-service/quickstart-nodejs?pivots=platform-linux)

[https://docs.microsoft.com/en-us/azure/architecture/example-scenario/apps/devops-dotnet-webapp](https://docs.microsoft.com/en-us/azure/architecture/example-scenario/apps/devops-dotnet-webapp)

[https://docs.microsoft.com/en-us/azure/developer/javascript/tutorial/deploy-nodejs-azure-app-service-with-visual-studio-code?tabs=bash](https://docs.microsoft.com/en-us/azure/developer/javascript/tutorial/deploy-nodejs-azure-app-service-with-visual-studio-code?tabs=bash)

[https://docs.microsoft.com/en-us/azure/app-service/quickstart-custom-container?tabs=node&pivots=container-linux](https://docs.microsoft.com/en-us/azure/app-service/quickstart-custom-container?tabs=node&pivots=container-linux)