Build And Deploy Your .NET Core Web App As Docker Container Using Microsoft Azure - Part Two

Introduction

 
This is the second article in the series and here, we are going to set up a Microsoft Azure DevOps build pipeline to automate the tasks. We did that manually in the first article of the series. Each time we push a change to the master branch, the build will be triggered to build our application, then build a Docker image, and push it to the Docker Hub.
 
If you have been following along, you must have -
  • a GitHub repository
  • a working web application in ASP .Net Core (or something similar)
  • a Docker Image for the application
  • a running container to host your application locally
There are tree parts in this series,

Docker Registry

 
First, you need to set up an account at Docker Hub. After that, create a repository where you can keep Docker images for your application.
 
You already have a Docker image built in the previous steps, that works well. So now, you can tag that image with the repository name. However, the Docker client running on your local machine needs to connect with the Docker Hub account, before you can push the image.
 
The following commands tag the local image for Docker Hub repository, authorize the Docker client and finally push the image to Docker Hub.
  1. $ docker tag <image-name> <namespace>/<repository>:<tag>   
  2.   
  3. // example  
  4. $ docker tag webapp quickdevnotes/webapp:v1  
  5.   
  6. // login to your Docker Hub account  
  7. $ docker login   
  8. username: quickdevnotes 
  9. password:  

  10. // push the to Docker Hub  
  11. $ docker push quickdevnotes/webapp:v1  
Note that I'm using a public repository at Docker Hub. If you are using a private repository, then you will have to first login using the docker login command and then push the image. Otherwise, the push will fail with authorization error.
 

Azure DevOps - Project 

 
Now comes the interesting part. To start using the Azure DevOps pipelines, you first need to have a user account. If you already have an account, great, you are good to start or you can register here
 
Once you are in, it asks you to create an organization and where you want to host your project. Name it anything you like and select a region suitable to your need:
 

 
Next, we have to create a project under the organization. Select +Create Project from the top right, and provide the required details.
 
From the "Advanced" section we don't need anything for this series. So, I leave the choice up to you.
 

Continuous Integration (Build Pipeline)

 
It's finally time to automate the manual steps. From Pipelines > Builds select New Pipeline.
 
Where is your code?
 
The first thing Azure pipeline needs is to connect with your application code repository. So, on the Connect tab, select GitHub a connection to your GitHub.
 

 
After that, you will be prompted with OAuth authentication to authorize Azure Pipelines for accessing the GitHub repository. Grant the authorization and provide credentials as required.
 
 
Select a repository
 
Once the authorization completes, you can see a list of repositories from the GitHub account. From the list of repositories select your application repository, which then prompts you to Install Azure Pipelines. Use the "only select repositories" option and from the drop-down select your application repository.
 

 
If you want to install Azure Pipelines for all current and future repositories select "All repositories" and click install.
 
 
azure-pipelines.yaml
 
The Azure pipeline is smart enough to analyze your application repository and provide a basic azure-pipelines.yml file. This YAML file lies at the root of your GitHub repository and is used by the Azure build pipeline to perform certain tasks like building the application, executing tests, building Docker Image and many more.
 
Here is the azure-pipelines.yml file for my build pipeline. I know, it can be overwhelming; but everything will be clear after we talk about each task in detail.
  1. trigger:  
  2. - master  
  3.   
  4. pool:  
  5.   vmImage: 'Ubuntu-16.04'  
  6.   
  7. variables:  
  8.   imageName: 'quickdevnotes/webapp:$(build.buildNumber)'  
  9.   
  10. steps:  
  11. - script: dotnet test WebApp.Tests/WebApp.Tests.csproj --logger trx  
  12.   displayName: Run unit tests  
  13. - task: PublishTestResults@2  
  14.   condition: succeededOrFailed()  
  15.   inputs:  
  16.     testRunner: VSTest  
  17.     testResultsFiles: '**/*.trx'  
  18. - task: Docker@1  
  19.   displayName: Build an image  
  20.   inputs:  
  21.     command: Build an image  
  22.     containerregistrytype: Container Registry  
  23.     dockerRegistryEndpoint: DockerHub  
  24.     dockerFile: Dockerfile  
  25.     imageName: $(imageName)  
  26.     imageNamesPath:   
  27.     restartPolicy: always  
  28. - task: Docker@1  
  29.   displayName: Push an image  
  30.   inputs:  
  31.     command: Push an image  
  32.     containerregistrytype: Container Registry  
  33.     dockerRegistryEndpoint: DockerHub  
  34.     dockerFile: Dockerfile  
  35.     imageName: $(imageName)  
  36.     imageNamesPath:   
  37.     restartPolicy: always  
Note that the file generated for your build pipeline will be different from what you see here. This is because I have updated the file with the tasks required to achieve our end goal.
 

CI Tasks - a close look

 
Let's take a closer look at the tasks defined in the above build file. We will stay at a high level though, just to keep things simple for this series.
  1. trigger:  
  2.   - master  
The trigger specifies, pushes to which branch will trigger the continuous integration pipeline to run. In my case, it's the master branch.
If you want to use a different branch just change the name of the branch. If we do not specify any branch, pushes to any branch will trigger a build.
  1. pool:  
  2. vmImage: 'Ubuntu-16.04'  
The above lines tell which agent pool to use for a job/task of the pipeline. 
  1. variables:  
  2. imageName: 'quickdevnotes/webapp:$(build.buildNumber)'  
We use the variables section to declare any variables we want to use in our pipeline. For instance, here I'm creating a variable imageName which will be set to name of the Docker image that I want to create. The Continuous Integration best practices recommend using $(build.buildNumber) to tag your Docker images. Because it makes it easy to update or rollback any changes.
  1. - script: dotnet test WebApp.Tests/WebApp.Tests.csproj --logger trx  
  2.   displayName: Run unit tests  
  3. - task: PublishTestResults@2  
  4.   condition: succeededOrFailed()  
  5.   inputs:  
  6.     testRunner: VSTest  
  7.     testResultsFiles: '**/*.trx'  
I have also added some basic unit tests to my application. With the above script, these tests will execute each time the build is run. We can also get test reports out of these test runs.
  1. - task: Docker@1  
  2.   displayName: Build an image  
  3.   inputs:  
  4.     command: Build an image  
  5.     containerregistrytype: Container Registry  
  6.     dockerRegistryEndpoint: DockerHub  
  7.     dockerFile: Dockerfile  
  8.     imageName: $(imageName)  
  9.     imageNamesPath:   
  10.     restartPolicy: always  
  11. - task: Docker@1  
  12.   displayName: Push an image  
  13.   inputs:  
  14.     command: Push an image  
  15.     containerregistrytype: Container Registry  
  16.     dockerRegistryEndpoint: DockerHub  
  17.     dockerFile: Dockerfile  
  18.     imageName: $(imageName)  
  19.     imageNamesPath:   
  20.     restartPolicy: always  
The first in the above two tasks build a Docker image using the Dockerfile available at the root of the repository. After building the image, the second task pushes that image to our repository on Docker Hub. After building the image, the second task pushes that image to our repository on Docker Hub. To read more about the different tasks and available options, please refer to the docs.
 
To push an image to your Docker Hub repository, the Docker client running on the build agent needs to authorize first, similar to what you did while pushing the image manually. 
  1. - task: Docker@1  
  2. ...  
  3.     dockerRegistryEndpoint: DockerHub  
  4. ...  
The dockerRegistryEndpoint is set to a docker registry service connection that holds the credentials for the Docker Hub account and is used for authorization. Let's set up that next. 
 

Docker Registry Service Connection

 
From the bottom left of the navigation pane, select Project Settings. Then under Pipelines, select Service connections and you should see your GitHub connection here. To a Docker Registry connection, click New service connection and from the list select Docker Registry. 
 

 
On the next dialog window, select Docker Hub as the Registry Type and set DockerHub (must be the same as dockerRegistryEndpoint in build task) as the Connection Name. Then, provide the Docker ID and Password of your Docker Hub account. Email is optional. Please ensure "Allow all pipelines to use this connection" is checked.
 
 
You can verify the connection by using the "Verify this connection" link. Click "OK" and we are good to go.
 

Testing the Build Pipeline

 
I believe, by now you have enough information to customize your continuous integration pipeline. Alright then, it's time to test if it does what we expect. Go to Pipelines > Builds and select Queue to queue a build. If you don't get any errors, you must see a build in progress. Here is the output from my build pipeline, after a successful build: 
 

 
The final task in the pipeline has successfully pushed the newly built Docker image to Docker Hub. Notice the image tag, which is equal to the build number in the above image:
 

 
To make a final test, change something in the application code and push it to the master branch. This will trigger the build and you will see the commit message as build title:
 

 
Notice how the build description tells you about the build trigger, and this proves it is not a manual trigger.
 
Congratulations!!
 

Conclusion

 
In this article, we have successfully set up a working continuous integration build pipeline with Microsoft Azure DevOps. Starting from building a .Net Core web application, to setting up a CI pipeline, we are halfway through. Next, we will set up a release pipeline to deploy our application as a Docker container on Azure Web App Service. It's going to be fun.