Azure Policy as Code using Terraform – Part 2

In Part 1, we saw how we can deploy custom policies and initiatives dynamically controlled by a JSON file. In Part 2, we will look at policy and initiative assignments. Until a policy is assigned, it doesn’t have any impact on the creation or modification of resources.

Azure Policies can be assigned to management groups, subscriptions, or resource groups. The policy definition itself must be placed at a level that encompasses all the policy assignment scopes, usually at a top-level management group. Even though policies can be assigned to resource groups, it is recommended that policies be assigned to management groups or subscriptions, as it would make policy governance easier. The lower levels would just inherit the policy assignments from the higher levels. The same goes for initiative assignments. In my implementation, I have included policy assignments at the resource group level for testing purposes.

The assignments.json file

The assignments are listed in the assignments.json file. Assignments are listed as an array, with each one specified as either a policy or an initiative assignment by using the type property. The policy_name and initiative_name properties must match the names in the policies.json file exactly.

{
    "assignments": [
        {
            "name": "enforce-location-sub-targetsub",
            "type": "policy",
            "policy_name": "allowedlocations",
            "management_group_name": "",
            "subscription_id": "<subscription id>",
            "resource_group_name": "",
            "parameters": {
                "allowedLocations": {
                    "value": [
                        "australiaeast"
                    ]
                }
            }
        },
        {
            "name": "location-initiative-mg",
            "type": "initiative",
            "initiative_name": "LocationInitiative",
            "management_group_name": "<mgmt group name>",
            "subscription_id": "",
            "resource_group_name": "",
            "parameters": {
                "locations": {
                    "value": [
                        "australiaeast"
                    ]
                }
            }
        },
        {
            "name": "namingtagging-initiative-sub",
            "type": "initiative",
            "initiative_name": "TaggingNamingInitiative",
            "management_group_name": "",
            "subscription_id": "<subscription id>",
            "resource_group_name": "",
            "parameters": {
                "costcentreTagName": {
                    "value": "costcentre"
                },
                "costcentreTagValues": {
                    "value": [
                        "finance",
                        "engineering"
                    ]
                },
                "environmentTagName": {
                    "value": "environment"
                },
                "environmentTagValues": {
                    "value": [
                        "production",
                        "development",
                        "testing"
                    ]
                },
                "namingPattern": {
                    "value": "projectxyz-dev-*-vm"
                }
            }
        }
    ]
}

If the management_group_name property of an assignment is not empty, then the policy will be assigned at the management group level. If the resource_group_name property is empty and the subscription id is not empty, then the policy will be assigned at the subscription level. Finally, if the resource_group_name property is not empty, then the policy will be assigned at the resource group level.

Passing parameters

Care should be taken while passing parameters to initiatives. The parameters listed in the assignments.json file are the initiative parameters and not the policy parameters. The initiative definition will take of mapping the initiative parameters to the correct policy parameters.

main.tf file changes

Here are the new additions to the locals.

  assignment_data = jsondecode(file("${path.root}/policies/assignments.json"))

  # Filter assignments for subscriptions
  policy_subscription_assignments = {
    for assignment in local.assignment_data.assignments :
    assignment.name => assignment
    if assignment.type == "policy"
    && assignment.resource_group_name == ""
    && assignment.management_group_name == ""
    && assignment.subscription_id != ""
  }

  # Filter assignments for resource groups
  policy_resource_group_assignments = {
    for assignment in local.assignment_data.assignments :
    assignment.name => assignment
    if assignment.type == "policy"
    && assignment.resource_group_name != ""
    && assignment.subscription_id != ""
  }

  # Filter assignments for management groups
  policy_management_group_assignments = {
    for assignment in local.assignment_data.assignments :
    assignment.name => assignment
    if assignment.type == "policy"
    && assignment.management_group_name != ""
  }

  # Filter initiative assignments for management groups
  initiative_management_group_assignments = {
    for assignment in local.assignment_data.assignments :
    assignment.name => assignment
    if assignment.type == "initiative"
    && assignment.management_group_name != ""
  }

  # Filter initiative assignments for subscriptions
  initiative_subscription_assignments = {
    for assignment in local.assignment_data.assignments :
    assignment.name => assignment
    if assignment.type == "initiative"
    && assignment.resource_group_name == ""
    && assignment.management_group_name == ""
    && assignment.subscription_id != ""
  }

The following are the data blocks required for the assignments.

data "azurerm_management_group" "assignment_management_groups" {
  for_each = toset([for assignment in local.assignment_data.assignments :
  assignment.management_group_name if assignment.management_group_name != ""])
  name = each.value
}

data "azurerm_resource_group" "assignment_resource_groups" {
  for_each = toset([for assignment in local.assignment_data.assignments :
  assignment.resource_group_name if assignment.resource_group_name != ""])
  name = each.value
}

data "azurerm_management_group" "initiative_assignment_management_groups" {
  for_each = toset([for assignment in local.initiative_management_group_assignments :
  assignment.management_group_name if assignment.management_group_name != ""])
  name = each.value
}

Shown below are the resource blocks for policy and initiative assignments. Each scope of assignment has a different Terraform resource type. The policy_definition_id is set using the azurerm_policy_definition.custom_policy.custom_policy resource, as detailed in my previous post, as it contains the IDs of the custom policies deployed.

# Create management group-level policy assignments
resource "azurerm_management_group_policy_assignment" "management_group_assignments" {
  for_each             = local.policy_management_group_assignments
  name                 = substr("Assignment-${each.value.policy_name}-mg", 0, 24)
  management_group_id  = data.azurerm_management_group.assignment_management_groups[each.value.management_group_name].id
  policy_definition_id = azurerm_policy_definition.custom_policy[each.value.policy_name].id
  display_name         = "Assignment for ${each.value.policy_name}"
  parameters           = jsonencode(lookup(each.value, "parameters", {}))
}

# Create subscription-level policy assignments
resource "azurerm_subscription_policy_assignment" "subscription_assignments" {
  for_each             = local.policy_subscription_assignments
  name                 = substr("Assignment-${each.value.policy_name}-sub", 0, 24)
  subscription_id      = "/subscriptions/${each.value.subscription_id}"
  policy_definition_id = azurerm_policy_definition.custom_policy[each.value.policy_name].id
  display_name         = "Assignment for ${each.value.policy_name}"
  parameters           = jsonencode(lookup(each.value, "parameters", {}))
}

# Create resource group-level policy assignments
resource "azurerm_resource_group_policy_assignment" "resource_group_assignments" {
  for_each             = local.policy_resource_group_assignments
  name                 = substr("Assignment-${each.value.policy_name}-rg", 0, 24)
  resource_group_id    = data.azurerm_resource_group.assignment_resource_groups[each.value.resource_group_name].id
  policy_definition_id = azurerm_policy_definition.custom_policy[each.value.policy_name].id
  display_name         = "Assignment for ${each.value.policy_name}"
  parameters           = jsonencode(lookup(each.value, "parameters", {}))
}

# Create management group-level initiative assignments
resource "azurerm_management_group_policy_assignment" "initiative_managementgroup_assignments" {
  for_each             = local.initiative_management_group_assignments
  name                 = substr("Assignment-${each.value.initiative_name}-mg", 0, 24)
  management_group_id  = data.azurerm_management_group.initiative_assignment_management_groups[each.value.management_group_name].id
  policy_definition_id = azurerm_management_group_policy_set_definition.custom_initiative[each.value.initiative_name].id
  display_name         = "Assignment for ${each.value.initiative_name}"
  parameters           = jsonencode(lookup(each.value, "parameters", {}))
}

# Create subscription-level initiative assignments
resource "azurerm_subscription_policy_assignment" "initiative_subscription_assignments" {
  for_each             = local.initiative_subscription_assignments
  name                 = substr("Assignment-${each.value.initiative_name}-sub", 0, 24)
  subscription_id      = "/subscriptions/${each.value.subscription_id}"
  policy_definition_id = azurerm_management_group_policy_set_definition.custom_initiative[each.value.initiative_name].id
  display_name         = "Assignment for ${each.value.initiative_name}"
  parameters           = jsonencode(lookup(each.value, "parameters", {}))
}

Result

Applying the Terraform file will result in all the assignments being deployed to Azure. Here are the screenshots of the assignments at the subscription level.

The above image shows the parameters of the TagginNamingInitiative at the subscription level. After you deploy the assignments, check what is displayed under the “Policy assignment parameter reference type”. It will display “User defined parameter” if you are passing the parameters correctly. If not set correctly, the default value will be used.

Here is a screenshot of the management group scope assignment.

As with policy and initiative definitions, adding more assignments is as simple as modifying the assignments.json file. I’ll cover policy exemptions and remediations in a future post.

References

Resource: azurerm_management_group_policy_assignment

Resource: azurerm_subscription_policy_assignment

Resource: azurerm_resource_group_policy_assignment

Leave a comment