Photo by Adam Sherez

Define branch policies using Terraform inside Azure DevOps

Secure your branches!

Posted by Damien Aicheh on 10/28/2022 · 9 mins

In this tutorial we will configure the policies to apply for each repository inside our Azure DevOps project using Terraform!

This tutorial is part of a full series of tutorials on configuring Azure DevOps using Terraform. You can download the project from the previous part and follow along.

Usecase

In a previous tutorial you created your repositories, but to contribute to it using Pull Requests, you must to setup a list of good practices to avoid having bad code deployed to production. So, to achieve this you can use the concept of policies and apply it to all the branches you need.

For the purpose of this tutorial we will apply the policies only on the default branch, however It’s really recommanded you to apply it on each branch that make sense for your project.

Type of policies

You have multiple policies to set to lock and standardize your Pull Requests:

  • Require a minimum number of reviewers
  • Check for linked work items
  • Check for comment resolution
  • Limit merge types
  • Adding a build validation
  • Status Checks
  • Automatically included reviewers

Let’s see how to setup the first five by creating a new file called repos_policies.tf.

Also notice in the next examples of codes, that you need to add the depends_on property for each of the policies declared, because the default files you added in the repository must be pushed before activating the policies. If you don’t do that, you will get an error telling you that the policies are already set and you are unable to push your default files without passing by a Pull Request, which is not the behavior you want when initializing the repository.

Minimum number of reviewers

As you can see below you define an azuredevops_branch_policy_min_reviewers and iterate over a list of repository like we defined in a previous tutorial.

resource "azuredevops_branch_policy_min_reviewers" "this" {
  count      = length(var.repositories)
  project_id = azuredevops_project.this.id

  enabled  = true
  blocking = true

  settings {
    reviewer_count                         = 1
    submitter_can_vote                     = false
    last_pusher_cannot_approve             = true
    allow_completion_with_rejects_or_waits = false
    on_push_reset_approved_votes           = false
    on_last_iteration_require_vote         = false

    scope {
      repository_id  = azuredevops_git_repository.this[count.index].id
      repository_ref = azuredevops_git_repository.this[count.index].default_branch
      match_type     = "Exact"
    }
  }

  depends_on = [
    azuredevops_git_repository_file.default_pipeline,
    azuredevops_git_repository_file.default_gitignore,
  ]
}

Check for linked work items

resource "azuredevops_branch_policy_work_item_linking" "this" {
  count      = length(var.repositories)
  project_id = azuredevops_project.this.id

  enabled  = true
  blocking = true

  settings {

    scope {
      repository_id  = azuredevops_git_repository.this[count.index].id
      repository_ref = azuredevops_git_repository.this[count.index].default_branch
      match_type     = "Exact"
    }
  }

  depends_on = [
    azuredevops_git_repository_file.default_pipeline,
    azuredevops_git_repository_file.default_gitignore,
  ]
}

Above you ask the user to add the work item linked to the branch for a Pull Request. This is useful to undestand the code added.

Check for comment resolution

Now, something really important regarding Pull Requests, be sure that the comments where all put to resolved before being able to merge it:

resource "azuredevops_branch_policy_comment_resolution" "this" {
  count      = length(var.repositories)
  project_id = azuredevops_project.this.id

  enabled  = true
  blocking = true

  settings {

    scope {
      repository_id  = azuredevops_git_repository.this[count.index].id
      repository_ref = azuredevops_git_repository.this[count.index].default_branch
      match_type     = "Exact"
    }
  }

  depends_on = [
    azuredevops_git_repository_file.default_pipeline,
    azuredevops_git_repository_file.default_gitignore,
  ]
}

Limit merge types

Depending on the rules you fixed with your team, it’s important to define the merge type you want for each branch. For instance the squash can be a good one when you merge a feature on the develop branch. So, you end up with only one commit with a formatted message instead of having all the commits from the feature branch which can be a bit messy.

resource "azuredevops_branch_policy_merge_types" "this" {
  count      = length(var.repositories)
  project_id = azuredevops_project.this.id

  enabled  = true
  blocking = true

  settings {
    allow_squash                  = true
    allow_rebase_and_fast_forward = false
    allow_basic_no_fast_forward   = true
    allow_rebase_with_merge       = false

    scope {
      repository_id  = azuredevops_git_repository.this[count.index].id
      repository_ref = azuredevops_git_repository.this[count.index].default_branch
      match_type     = "Exact"
    }
  }

  depends_on = [
    azuredevops_git_repository_file.default_pipeline,
    azuredevops_git_repository_file.default_gitignore,
  ]
}

Adding a build validation

The last one that you will see is probably the most important one. Adding a build validation ensure that an Azure DevOps pipeline ran your code. This pipeline must have at least, the execution of your Unit Tests, the build of the project and if you can, other scaning tools like Sonar, Checkmarks, etc.. to validate the quality of your code.

resource "azuredevops_branch_policy_build_validation" "this" {
  count      = length(var.repositories)
  project_id = azuredevops_project.this.id

  enabled  = true
  blocking = true

  settings {
    display_name        = "Pull Request Check"
    build_definition_id = azuredevops_build_definition.build_definitions[count.index].id
    valid_duration      = 720 # minutes => 12 hours

    scope {
      repository_id  = azuredevops_git_repository.this[count.index].id
      repository_ref = azuredevops_git_repository.this[count.index].default_branch
      match_type     = "Exact"
    }
  }

  depends_on = [
    azuredevops_git_repository_file.default_pipeline,
    azuredevops_git_repository_file.default_gitignore,
  ]
}

You can find all the policies available in the Terraform documentation.

Run Terraform

Let’s run a new Terraform plan command and apply it:

terraform plan -var-file=env.tfvars --out=plan.out

Then:

terraform apply plan.out

As result, all your repositories will have policies in the default branch and all the new code will be only added to this branch by a Pull Request.

Final touch

You now have your default branches protected by some policies using Terraform. You will find full source code in this Github repository.

Do not hesitate to follow me on to not miss my next tutorial!