Upcoming book:

Modern .NET Development with Azure and DevOps

Governance As Code with Azure Bicep

Introduction

Azure Governance refers to the practice of defining and enforcing business policies to your Azure workloads. Governance is an important part of Microsoft's Cloud Adoption Framework.

Governance consists of many disciplines, as we can see in the Microsoft documentation:

  • Cost management
  • Security
  • Resource structure/naming
  • Policies
  • Etc.

Governance as Code (GaC) as presented in this post, refers to the idea of going a step further than just Infrastructure as Code (IaC), and having as much of the governance as possible written in code.

I must note that this post will not attempt to define a governance strategy, since that varies greatly from company to company, and the official Microsoft documentation has plenty written about the subject.

Instead, we'll skip the initial step of defining the governance, and take a look at how we can achieve GaC by using Azure Resource Manager (ARM) through Bicep templates. Please note that I will not try to cover all the aspects of governance in one go, but rather focus on establishing a baseline.

Example governance structure

The structure we will be building is more or less simple and by no means attempts to represent a full structure.

Governance structure

The structure above follows Microsoft recommendations for using Management Groups and, while simple, is extensible and scalable. We have:

  • A root Management Group (which is required).
    • An IT Management group (for platform/network/identity subscriptions) with policies attached.
      • A Networking subscription.
    • A Development Management Group (for teams to run their development and test subscriptions) with policies attached.
      • Two subscriptions, each with its set of policies.
    • A Production Management Group (for production deployments) with policies attached.
      • An Internal Management Group (for internal applications).
        • Two subscriptions (each for different products).
      • An External Management Group (for applications managed by clients).
        • One subscription (for a given client).

Bicep modules

To achieve an automated deployment like the above, there are a few prerequisites, which I've listed on my GitHub sample repository.

Bicep makes things a bit harder than they should be with scopes for these types of deployments, such as having to change the scope from tenant to management group to subscription, but I'm hopeful this will be made simpler in future releases.

Management Group

Creating a Management Group (MG) can be done with a module such as this one, and notice the scope being the tenant.

targetScope = 'tenant'

@description('The ID of the parent MG')
param parentManagementGroupId string

@description('The name of the MG')
param name string

resource managementGroup 'Microsoft.Management/managementGroups@2021-04-01' = {
  name: name
  scope: tenant()
  properties: {
    details: {
      parent: {
        id: parentManagementGroupId
      }
    }
    displayName: name
  }
}

output id string = managementGroup.id

The template forces the MG to be under another MG, even if it's just the root MG.

Subscription

A Subscription can be deployed at the Tenant or at the Management Group level, but in our case, we want all Subscriptions to be parented by one of our Management Groups, so the template looks like this:

targetScope = 'managementGroup'

@description('The billing scope for this subscription')
param billingScope string

@description('The ID of the management group that is the parent of the subscription')
param parentManagementGroupId string

@description('Name of the subscription')
param subscriptionName string

@description('Workload type for the subscription')
@allowed([
    'Production'
    'DevTest'
])
param subscriptionWorkload string

resource subscription 'Microsoft.Subscription/aliases@2021-10-01' = {
  name: subscriptionName
  scope: tenant()
  properties: {
    workload: subscriptionWorkload
    displayName: subscriptionName
    billingScope: billingScope
    additionalProperties: {
      managementGroupId: parentManagementGroupId
    }
  }
}

output subscriptionId string = subscription.id

Do note the billingScope parameter for this template. That specifies the full path to the scope, and you can see in the prerequisites link how to obtain that.

Assigning policies and policy sets

Assigning existing policies and policy sets to MGs and Subscriptions is easy, as long as you use the correct scope for the module and obtain the policy's ID from Azure. For example, to link a MG to the ISO 27001 policy set, we first need to get full resource ID:

targetScope = 'managementGroup'

resource policySetDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' existing = {
  name: '89c6cddc-1c73-4ac1-b19c-54d1a15a42f2'
}

output policyDefinitionId string = policySetDefinition.id

And then assign the policy:

resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
  name: policyName
  properties: {
    policyDefinitionId: policyDefinitionId
    description: policyDescription
  }
}

Defining new policies

Policies can also be created through Bicep templates and subsequently assigned to the appropriate resources. For example, if you want to create a policy to restrict resource deployments to particular Azure regions, you can define the policy like this:

targetScope = 'managementGroup'

@description('The region to which resources can be deployed to')
@allowed([
  'UKSouth'
  'EuropeNorth'
])
param allowedRegion string

var policyDefinitionName = 'Region restriction'

resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
  name: policyDefinitionName  
  properties: {
    policyType: 'Custom'
    mode: 'All'
    parameters: {}
    policyRule: {
      if: {
        not: {
          field: 'location'
          in: [ allowedRegion ]
        }
      }
      then: {
        effect: 'deny'
      }
    }
  }
}

output policyDefinitionId string = policyDefinition.id

Notice that the policy definition is deployed at the Management Group level, so it's reusable for the Subscriptions below it. You would then apply the policy using the same Bicep template as we saw in the previous section.

Another note about scopes

As I said previously, Bicep makes things a bit complicated when it comes to the scopes for deploying things like policy assignments. If you want to have generic modules for deploying policy assignments to MGs and to Subscriptions, the best I could find is that you need to duplicate the modules.

For example, if the MG assignment module is like this:

targetScope = 'managementGroup'

@description('The ID of the policy definition to assign')
param policyDefinitionId string

@description('The name of the policy assignment')
param policyName string

@description('The description of the policy assignment')
param policyDescription string

resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
  name: policyName
  properties: {
    policyDefinitionId: policyDefinitionId
    description: policyDescription
  }
}

You'd have to duplicate the module, changing the targetScope from managementGroup to subscription.

Putting it all together

You can find the entire Bicep project available in my GitHub samples repository here. The sample is MIT licensed, so you are free to do as you please with the source code.

I do have to warn you that I do not currently have an account where I can deploy licenses, so it is likely that changes are required in the Bicep modules for the project to be deployable.

As usual, let me know in the comments if you find this useful or if you have any questions.