Cars Island - DevOps practices for the Azure infrastructure - part 13

Cars Island - DevOps practices for the Azure infrastructure - part 13

Introduction

In my first article, I introduced you to Cars Island car rental on the Azure cloud. I created this fake project to present how to use different Microsoft Azure cloud services and how to their SDKs. I also presented what will be covered in the next articles. Here is the next article from the series where I would like to discuss how to use some DevOps practices to manage Azure infrastructure.

Below I present solution architecture:

Image not found

Cars Island project is available on my GitHub

Azure Resource Manager Templates (ARM)

From my previous article you know that I use Azure Resource Manager Templates (ARM) to create and manage resources created on the Azure cloud. I keep these ARM templates in the separate repository in the Azure DevOps:

Image not found

Now let’s talk first about the changes that are applied to ARM templates and what is then happening in the Azure DevOps.

Branch policies

Before I continue, a small disclaimer. You can use different branching strategies (like Git flow, GitHub flow, etc.) and still apply the below recommendations to make sure that changes are not directly committed to specific branches and that they are reviewed before the merge is done.

In the Cars Island GIT repository for ARM templates I have one branch: master. This branch has policies enabled so direct commits are not allowed:

Image not found

For the master branch I applied the below policies:

Require a minimum number of reviewers

First of all - when there are any changes, they cannot be committed directly. A new pull request should be created to discuss what was changed. At least one person from the team has to review the changes and either approve them or reject them. Once changes are approved but someone will add anything new to the source branch (feature branch), approval will be reset and the reviewer has to review the changes again:

Check for linked work items

As you remember from my previous article, I created a backlog in Azure DevOps to control changes. In this backlog, there was dedicated Epic for the infrastructure. Each time there is a new pull request created, work items from this backlog have to be attached. Why? Because it is much easier to discover what kind of changes are applied.

Check for comment resolution

During the code review process, the reviewer can comment on some changes. The creator of the pull request has resolved these comments before the merge is possible. This prevents omitting the comments with important notes and requirements.

Limit merge types

We can control how commits history is preserved in the GIT repository. In this case, I applied Basic merge (no fast-forward) type because I want to keep each commit in the history tree.

Image not found

Now let’s talk about the release pipeline and variables.

Azure DevOps Pipeline Variable Groups

Azure DevOps Pipeline Variable Groups can be used to store variables used in the different pipelines. As you can see I have three variable groups created - each one keeps variables for different environment - DEV, TEST, and PROD:

Image not found

Let’s talk about variable group created for DEV environment. As you can see I declared many different variables:

Image not found

We can mark some variable values to be secrets - then they will not appear in the pipeline’s logs and they will be hidden:

Image not found

Important

As you can see in the picture above, there is an option to link the secret from the Azure Key Vault. This option can be helpful when you have a separate, initial Key Vault, where you store values for the infrastructure that will be created. Example? You can store the username and password for the Azure SQL database there. Then this information can be injected into the ARM templates during the execution of the release pipeline. In Cars Island the process is simplified. I store secrets directly in the Azure DevOps variable groups.

Azure DevOps Environments

An environment in the Azure DevOps is a collection of resources, that can be targeted by deployments from a pipeline. Typical examples of environment names are Dev, Test, QA, Staging, and Production. I created three environments that I will reference in the release pipeline:

Image not found

I encourage you to read this documentation to learn more about declaring environments.

Azure DevOps Pipeline Templates

Before I continue I would like to recommend watching my course called Microsoft DevOps Solutions: Implementing Orchestration Automation Solutions where I described in details some aspects mentioned in this article.

In the GIT repository for ARM templates, I created a separate folder called pipelines where I store YAML files with a declaration for the infrastructure release. In the Azure DevOps you can of course use visual designer but recommended approach is to use YAML files to declare builds and releases as a code:

Image not found

arm-deployment-template.yml

jobs:

  - deployment: DeployRG
    environment: $
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
            clean: true 
            fetchDepth: 5
            lfs: true
          - task: AzureResourceManagerTemplateDeployment@3
            displayName: Deploy Resource Group
            inputs:
              deploymentScope: 'Subscription'
              azureResourceManagerConnection: $
              subscriptionId: $
              location: 'West Europe'
              templateLocation: 'Linked artifact'
              csmFile: '$(System.DefaultWorkingDirectory)/arm/rg-azure-deploy.json'
              overrideParameters: '-rgName $ -location $'
              deploymentMode: 'Incremental'
  
  - deployment: DeployResourcesIntoRG
    condition: succeeded()
    dependsOn: DeployRG
    environment: $
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
            clean: true 
            fetchDepth: 5
            lfs: true         
          - task: AzureResourceManagerTemplateDeployment@3
            displayName: Deploy resources into resource group
            inputs:
              deploymentScope: 'Resource Group'
              azureResourceManagerConnection: $
              subscriptionId: $
              action: 'Create Or Update Resource Group'
              resourceGroupName: $
              location: 'West Europe'
              templateLocation: 'Linked artifact'
              csmFile: '$(System.DefaultWorkingDirectory)/arm/azure-deploy.json'
              csmParametersFile: '$(System.DefaultWorkingDirectory)/arm/azure-deploy.parameters.$.json'
              overrideParameters: '-sendgrid_name "$" -sendgrid_password "$" -sendgrid_email "$" -sendgrid_firstName "$" -sendgrid_lastName "$" -sendgrid_company "$" -sendgrid_website "$"'
              deploymentMode: 'Incremental'

  - deployment: ApiManagementDeployment
    environment: $
    dependsOn: DeployResourcesIntoRG
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
            clean: true 
            fetchDepth: 5
            lfs: true
          - task: AzureResourceManagerTemplateDeployment@3
            displayName: Deploy Api Management resource
            inputs:
              deploymentScope: 'Resource Group'
              azureResourceManagerConnection:  $
              subscriptionId: $
              action: 'Create Or Update Resource Group'
              resourceGroupName: $
              location: $
              templateLocation: 'Linked artifact'
              csmFile: '$(System.DefaultWorkingDirectory)/arm/apim-azure-deploy.json'
              overrideParameters: '-api_management_name "$" -api_management_publisher_email "$" -api_management_publisher_name "$" -sku {"name": "$", "capacity": 1} -resourceTags {"Environment": "$", "Project": "$"}'
              deploymentMode: 'Incremental'

The template above contains three deployments:

DeployRG - to create new resource group (if one does not already exist):

  - deployment: DeployRG
    environment: $
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
            clean: true 
            fetchDepth: 5
            lfs: true
          - task: AzureResourceManagerTemplateDeployment@3
            displayName: Deploy Resource Group
            inputs:
              deploymentScope: 'Subscription'
              azureResourceManagerConnection: $
              subscriptionId: $
              location: 'West Europe'
              templateLocation: 'Linked artifact'
              csmFile: '$(System.DefaultWorkingDirectory)/arm/rg-azure-deploy.json'
              overrideParameters: '-rgName $ -location $'
              deploymentMode: 'Incremental'

Please note that deploymentMode is set to Incremental. With this configuration, we can avoid re-creating resource groups (or other Azure resources if they already exist - names are verified first).

There is also overrideParameters section where we can pass the parameters to override the one used in the parameter files (like azure-deploy.parameters.dev.json). Above parameters are taken from the variable group (connection to variable groups will be mentioned below).

For the csmFile section we have to provide the path to the ARM json file (for instance for the resource group this one: rg-azure-deploy.json).

DeployResourcesIntoRG - to create resources in the resouce group (if these resources do not already exist):

  - deployment: DeployResourcesIntoRG
    condition: succeeded()
    dependsOn: DeployRG
    environment: $
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
            clean: true 
            fetchDepth: 5
            lfs: true         
          - task: AzureResourceManagerTemplateDeployment@3
            displayName: Deploy resources into resource group
            inputs:
              deploymentScope: 'Resource Group'
              azureResourceManagerConnection: $
              subscriptionId: $
              action: 'Create Or Update Resource Group'
              resourceGroupName: $
              location: 'West Europe'
              templateLocation: 'Linked artifact'
              csmFile: '$(System.DefaultWorkingDirectory)/arm/azure-deploy.json'
              csmParametersFile: '$(System.DefaultWorkingDirectory)/arm/azure-deploy.parameters.$.json'
              overrideParameters: '-sendgrid_name "$" -sendgrid_password "$" -sendgrid_email "$" -sendgrid_firstName "$" -sendgrid_lastName "$" -sendgrid_company "$" -sendgrid_website "$"'
              deploymentMode: 'Incremental'

Once the resource group is created, the next deployment can be run - DeployResourcesIntoRG. Please note that there is a dependsOn: DeployRG section. With the conditions in Azure DevOps we can decide whether next deployment should be executed or not basing on the previous one’s status (if it is successfull, next deployment can be executed).

ApiManagementDeployment - to create Azure API Management service (if it does not already exist)

  - deployment: ApiManagementDeployment
    environment: $
    dependsOn: DeployResourcesIntoRG
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
            clean: true 
            fetchDepth: 5
            lfs: true
          - task: AzureResourceManagerTemplateDeployment@3
            displayName: Deploy Api Management resource
            inputs:
              deploymentScope: 'Resource Group'
              azureResourceManagerConnection:  $
              subscriptionId: $
              action: 'Create Or Update Resource Group'
              resourceGroupName: $
              location: $
              templateLocation: 'Linked artifact'
              csmFile: '$(System.DefaultWorkingDirectory)/arm/apim-azure-deploy.json'
              overrideParameters: '-api_management_name "$" -api_management_publisher_email "$" -api_management_publisher_name "$" -sku {"name": "$", "capacity": 1} -resourceTags {"Environment": "$", "Project": "$"}'
              deploymentMode: 'Incremental'

The last deployment is created separately for the API Management deployment. Once we have the deployment YAML file defined, we can move to the azure-pipelines.yml file structure below.

arm-deployment-template.yml

This file contains stages which uses the above deployment template to create resources for DEV, TEST, and PROD environment:

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

stages:
- stage: Deploy_DEV_environment_infrastructure
  displayName: 'Create Cars Island Azure resources for DEV environment'
  variables:
  - group: 'cars-island-infrastructure-dev-env-variable-group'
  jobs:
  - template: arm-deployment-template.yml
    parameters:
      azureResourceManagerConnectionName: '$(azureResourceManagerConnectionName)'
      targetSubscriptionId: '$(targetSubscriptionId)'
      resourceGroupName: '$(resourceGroupName)'
      resourceGroupLocation: '$(resourceGroupLocation)'
      apiManagementName: '$(apiManagementName)'
      apiManagementPublisherEmail: '$(apiManagementPublisherEmail)'
      apiManagementPublisherName: '$(apiManagementPublisherName)'
      apiManagementSku: '$(apiManagementSku)'
      apiManagementEnvironmentTag: '$(apiManagementEnvironmentTag)'
      apiManagementProjectTag: '$(apiManagementProjectTag)'
      sendGridName: '$(sendGridName)'
      sendGridPublisherFirstName: '$(sendGridPublisherFirstName)'
      sendGridPublisherLastName: '$(sendGridPublisherLastName)'
      sendGridPublisherEmail: '$(sendGridPublisherEmail)'
      sendGridPublisherPassword: '$(sendGridPublisherPassword)'
      sendGridCompanyName: '$(sendGridCompanyName)'
      sendGridWebsite: '$(sendGridWebsite)'
      env: '$(env)'
      environment: 'DEV'

- stage: Deploy_TEST_environment_infrastructure
  displayName: 'Create Cars Island Azure resources for TEST environment'
  variables:
  - group: 'cars-island-infrastructure-test-env-variable-group'
  jobs:
  - template: arm-deployment-template.yml
    parameters:
      azureResourceManagerConnectionName: '$(azureResourceManagerConnectionName)'
      targetSubscriptionId: '$(targetSubscriptionId)'
      resourceGroupName: '$(resourceGroupName)'
      apiManagementName: '$(apiManagementName)'
      apiManagementPublisherEmail: '$(apiManagementPublisherEmail)'
      apiManagementPublisherName: '$(apiManagementPublisherName)'
      apiManagementSku: '$(apiManagementSku)'
      apiManagementEnvironmentTag: '$(apiManagementEnvironmentTag)'
      apiManagementProjectTag: '$(apiManagementProjectTag)'
      sendGridName: '$(sendGridName)'
      sendGridPublisherFirstName: '$(sendGridPublisherFirstName)'
      sendGridPublisherLastName: '$(sendGridPublisherLastName)'
      sendGridPublisherEmail: '$(sendGridPublisherEmail)'
      sendGridPublisherPassword: '$(sendGridPublisherPassword)'
      sendGridCompanyName: '$(sendGridCompanyName)'
      sendGridWebsite: '$(sendGridWebsite)'
      env: '$(env)'
      environment: 'TEST'

- stage: Deploy_PROD_environment_infrastructure
  displayName: 'Create Cars Island Azure resources for PROD environment'
  variables:
  - group: 'cars-island-infrastructure-prod-env-variable-group'
  jobs:
  - template: arm-deployment-template.yml
    parameters:
      azureResourceManagerConnectionName: '$(azureResourceManagerConnectionName)'
      targetSubscriptionId: '$(targetSubscriptionId)'
      resourceGroupName: '$(resourceGroupName)'
      apiManagementName: '$(apiManagementName)'
      apiManagementPublisherEmail: '$(apiManagementPublisherEmail)'
      apiManagementPublisherName: '$(apiManagementPublisherName)'
      apiManagementSku: '$(apiManagementSku)'
      apiManagementEnvironmentTag: '$(apiManagementEnvironmentTag)'
      apiManagementProjectTag: '$(apiManagementProjectTag)'
      sendGridName: '$(sendGridName)'
      sendGridPublisherFirstName: '$(sendGridPublisherFirstName)'
      sendGridPublisherLastName: '$(sendGridPublisherLastName)'
      sendGridPublisherEmail: '$(sendGridPublisherEmail)'
      sendGridPublisherPassword: '$(sendGridPublisherPassword)'
      sendGridCompanyName: '$(sendGridCompanyName)'
      sendGridWebsite: '$(sendGridWebsite)'
      env: '$(env)'
      environment: 'PROD'

As you can see, each stage uses a different variable group (like DEV stage uses cars-island-infrastructure-dev-env-variable-group variable group). Once these variable groups are linked to the pipeline, parameters can be injected and used in the YAML templates. How can we link these variable groups then?

Linking Variable Groups with Pipelines

Once we define new pipeline in the Azure DevOps, we can link variable groups so we can inject values for parameters presented above in the templates. To do it, click Edit button:

Image not found

Then select Triggers:

Image not found

Then switch to the Variables section and select Variable groups. From this place you can link any variable group to use variables in the pipeline:

Image not found

Run release for the infrastructure

Once we have steps completed above, we can run the release pipeline. Please note that in this case all three environments will be created: DEV, TEST, and PROD:

Image not found

What about scenarios in which we want to control when there is a deployment done for a specific environment (like for the production once)? In this case, we have to use gates.

Approvals and checks

As you remember, I defined three environments in the Azure DevOps mentioned above in the article: DEV, TEST, and PROD. Now for each environment, I can define approvals and checks. To do it, we have to open specific environment, like PROD, and select three dots, and then select Approvals and checks:

Image not found

There is a whole list of available checks to select from:

Image not found

You can select Approvals and then decide who is responsible for the final approval before there is deployment executed:

Image not found

Each time there is a new release available, the person indicated above will have to manually approve the execution of the deployment stage.

Summary

In this article, I described how to use Azure DevOps to implement the DevOps process for the Azure infrastructure release. As you can see, there are many great features you can use to make your process more stable. Once again, I encourage you to watch my Pluralsight course called Microsoft DevOps Solutions: Implementing Orchestration Automation Solutions to learn more.

Updated: