Automation, Azure, DevOps, Professional, Terraform

Dynamically Adding Terraform Policy Assignments…Reusing Infrastructure as Code!

This a post related to my post on “Creating Azure Policy via Terraform” and throws in how to deal reusing an Azure Policy Definition for multiple assignments. In this case we may want to pass in a list of required tags and what the intended effect for each might be.

There are numerous ways to do this. For the purposes of this blog, we will focus on handling this information at the policy definition ASSIGNMENT level as opposed to encoded in the definition or within an initiative. (Full disclosure I wanted to do this in an initiative but ran into Terraform limitations, if you have overcome this feel free to reach out!)

Taking a step back doing this via the assignment level means we can define the Azure Policy as Terraform just once and pass in a variable list or object containing the details. This becomes useful as we can create one policy definition for all the required tags and at assignment pass in the necessary information.

For this example, we will define our variable as an object passing in the tag name(s) and our effect. This allows for greater granularity of control when applying policies. The Terraform might look like:

variable "required_tags" {
  type = map(object({
    tagName         = string
    tagPolicyEffect = string
  }))
  default = {
    department = {
      tagName         = "department"
      tagPolicyEffect = "Disabled"
    },
    delete-by = {
      tagName         = "delete-by"
      tagPolicyEffect = "Deny"
    }
  }

By constructing the Terraform variable file this way we will be required a tag of delete-by to create; however, the tage of department might be optional for now until we get up to compliance.

The associated policy rule may look like:


  policy_rule = <<POLICY_RULE
  {
 "if": {
        "allOf": [
          {
            "field": "type",
            "equals": "Microsoft.Resources/subscriptions/resourceGroups"
          },
          {
            "field": "[concat('tags[', parameters('tagName'), ']')]",
            "exists": "false"
          }
        ]
      },
      "then": {
        "effect": "[parameters('tagPolicyEffect')]"
      }
    }
  
POLICY_RULE


  parameters = <<PARAMETERS
    {
        "tagName": {
        "type": "String",
        "metadata": {
          "displayName": "Tag Required",
          "description": "Name of the tag, such as 'owner'"
        }
      },
              "tagPolicyEffect": {
 "type": "String",
                 "metadata": {
                 "displayName": "Effect",
                 "description": "Enable or disable the execution of the policy"
                 },
                 "allowedValues": [
                 "AuditIfNotExists",
                 "Disabled",
                 "Deny"
                 ],
                 "defaultValue": "AuditIfNotExists"
      }
  }
PARAMETERS

Then the magic of our assignment may look like:

resource "azurerm_policy_assignment" "assign_rg_required_tags" {
  for_each=  var.required_tags
  name                 = "Assign Required Tags ${each.value.tagName}"
  scope                = data.azurerm_subscription.subscription_info.id
  policy_definition_id = module.required_tag.policy_id
  description          = "Policy Assignment created for Required Tag ${each.value.tagName}"
  display_name         = "Required Tag ${each.value.tagName}"


  parameters = <<PARAMETERS
    {
      "tagName": {"value": "${each.value.tagName}" },
      "tagPolicyEffect": {"value": "${each.value.tagPolicyEffect}" }
    }
PARAMETERS
}

Note that the display_name, description, and name will read each instance of the loop in. This is required since we can’t have two assignments of the same name.

That’s it! Now we can specify different required tags potentially in different variable files so that our tagging requirements in dev might be different then a production environment.