Basic CI/CD setup for web apps in Azure
You do not have to read all the excellent articles/books of Martin Fowler to understand that the whole CI/CD process is the core of proper software development. After working on many global projects I’ve implemented, many of those setups are the same. They all share a basic ‘hygiene’ in the CI/CD process. I will share my experience on this topic and will include code to make it more appealing.
Tools I’m using
These are the tools I cannot live without at the moment:
Azure Devops
Azure Devops is the current workhorse I’m using the most. After working with Gitlab, I fell in love with the YAML pipelines. Azure Devops fully supports this kind of setup. Before Azure Devops I’ve worked with Octopus Deploy and TeamCity which are excellent tools too.
More info
Editor
Visual Studio Code is the way to go for me. Awesome editor and it’s free!
More info
Version control
I’ve worked with a lot of Git clients, also command line. Bottom line: I dig Gitkraken. This tool is visualizing the Git process in a very friendly way.
More info
Touch Portal
When working in multiple teams and projects, proper navigation between resources, repositories, environments is a basic requirement. I’ve been looking at the StreamDeck but to serve only as an optimized ‘bookmark’ device, was too pricy for me. I’ve discovered Touch Portal and I love it! Now navigation between clients, projects, folders, VS Code editors is a breeze!
More info
Docker
Goodbye to file deployments. Using Docker is deploying a ‘state’ of your application instead of a collection of files.
More info
Architecture
For our setup, I’ll use a basic architecture that will serve as a blueprint for the process.
We’ll start with Environment definitions:
- (Development)
- Test
- UAT
- (Staging)
- Production
This is an extended setup. For some clients, we’ll use a Test, UAT, and Production environment only. Some small clients prefer Test and Production.
The purpose of these environments is important and depends on the branch strategy.
Branching strategy is another topic, I’ll use a lightweight model which is somewhere between the Github model and Gitflow model: Development works on feature branches, these feature branches are merged into the main branch using Pull Requests. A merge in the main branch will trigger a release to the Test environment.
When the Test environment is stable, this version is promoted to the UAT environment and will be promoted to Production when accepted.
Application
For this setup, I’ll use a very simple application that will be used in the setup. This is just a Node app that will run in a Docker container and just serves an ordinary HTML page:
And the Dockerfile
Project Structure
Now the application is set up, let’s take a look at the folder structure. We’ll be using the Azure Devops pipelines for:
- Provisioning
- Building and Testing
- Pull Requests
- Release to Test
- Release to UAT and Production
I’ll store these pipelines in a separate folder which also includes folders for templates, scripts, and variables:
Pipeline structure
To keep the pipelines organized, use templates! Templates can be seen as files that can be included in the pipeline itself. You can pass parameters to the template itself to unleash the full power of the template.
Variables can also be set up in a template. To move variables to templates, it’s easy to maintain the same template with different values across multiple environments.
The pipeline for a build looks like this:
Important to notice:
Line #1 is a reference to the ‘global’ variables. These are required independent of the environment itself.
Line #5 is a line that prevents the pipeline to be triggered on a Pull Request. For Pull requests, a different pipeline is triggered.
Line #8 is a trigger definition that tells the agent to start the pipeline when a change in any feature branch is detected (feature/*). Changes in the pipeline folder are excluded, this will not trigger a run.
Line #23 starts the stages. I’m always using stages to keep the process organized. When the pipelines need to be extended, it’s easier to add a new stage. It’s also possible to run multiple stages in parallel. By default stages run sequentially.
Line #32 is a reference to the template.
Part I — Building and Testing template
The first part will take care of the building and testing of the application itself. That’s why we need the build pipeline. This pipeline just checks out the branch and will run a build command. In this case, it will be:
npm run build
Our build template looks like this:
This template runs on every commit in a feature branch, so it must be fast. It must just Build and Test the application itself. Keep this pipeline small and simple for it will run many times a day. Its main purpose is to validate the code itself and check if the commit won’t break the application.
Part II — Pull Request
When a feature branch is ready, it can be merged into the main branch. This is done by a Pull Request.
In Azure Devops it’s important to prevent direct commits in the main branch. This can be done by selecting the project and repository:
In the Branch Polices, select the branch you want to protect and select ‘Build Validation’. In this section, you can pick the PR pipeline which must be triggered when a PR is created.
When this is set: the PR pipeline can be created. This is essentially the same as a build pipeline but will contain better checks for the code. A PR can, for example, contain additional SonarQube scans. These scans take some time so you don’t want to run this in the ordinary build.
By using parameters in the template, it’s easy to use the same template but extend this with additional tasks:
Line #24 is the condition for this task to run. It corresponds to the added parameters in the pipeline and template itself (line #2).
So, in the Buildpipeline, SonarQube run disabled:
And in the PR pipeline, SonarQube run enabled:
Part III — Release to the Test environment
The previous pipelines take care of building and testing the code. Nothing happens to the code.
But now the PR is approved, the Release to Test pipeline must be triggered. This pipeline is responsible for:
- Building the application
- Dockerizing the application
- Push the Docker image
- Switch image tag in Azure for the WebApp
These are actually 2 stages:
- Stage Building
- Stage Deploying
The pipeline looks like this:
Important to notice:
Line #8 targeting the main branch. This will be triggered on an approved Pull Request only because direct commits in the main branch are prohibited. Can only be done using a PR.
Lin #26 including a variable template that is used on top of the ‘global’ variables defined on #2.
As you can see here, only stages are defined: Build and Deploy stages. Both stages refer to a template.
Build-template for a release
Because the PR pipeline already included SonarQube and Unit testing, it’s not recommended to rerun these time-consuming tasks again. This stage is optimized for the build itself.
The stage only needs to build and push the Dockerimage.
This template uses a Docker step to Dockerize the application. This step takes care of both the Docker Build as the Docker Push action. The image is tagged with:
- BuildID
- A custom image tag
- latest
When the Build stage is done, the Deploy stage will kick in, this stage takes care of ‘telling’ the WebApp to use the new image (tag).
The deploy stage from the pipeline refers to this template. This template is using the task AzureWebAppContainer@1 which contains additional information for WebApps for Containers.
The task itself is just telling the WebApp to use a certain image with a specific tag. In the appSettings section, it’s possible to add application settings to the webapp (appsettings in WebApps are injected in a Docker Container as environment variables during the Docker Run command).
When this step is finished (will only take 10–15 seconds) you’ll find the next log lines in the log stream of the WebApp:
The beauty of this system is the integrated Blue/Green deployment. Azure takes care of the new version and is warming up the application before switching the internal loadbalancer to the new Container.
Great Succes!
And don’t forget to keep the fun in automating!
Here is an additional task for successful Test releases and/or builds:
This task will result in:
Now the application can run on Azure, it’s time to take a look at the provisioning scripts for the Azure environments. This will be covered in the next article.
All files from this article can be found here on Github.