While Azure provides a powerful native policy engine, managing its definitions and assignments through the portal can become a significant operational bottleneck. By adopting a Policy as Code (PaC) approach using Terraform, you can bring version control, automation, and audibility to your entire Azure governance framework.

This guide provides in-depth, practical examples of how to define, assign, and manage Azure Policy using Terraform to enforce security, cost, and organizational standards.

The Core Workflow: Managing Azure Policy with Terraform

The end-to-end process involves using Terraform to manage the three key components of Azure Policy:

  • Policy Definitions: The actual rule or logic that evaluates resources. We'll write these in JSON and create them in Azure using the `azurerm_policy_definition` resource.
  • Policy Initiatives (Sets): Collections of related policy definitions that are managed and assigned as a single unit. Created with the `azurerm_policy_set_definition` resource.
  • Policy Assignments: The application of a policy or initiative to a specific scope (like a management group, subscription, or resource group). Managed with resources like `azurerm_subscription_policy_assignment`.

Use Case 1: Enforcing Cost Allocation Tags

Our first goal is a classic FinOps requirement: ensure all resource groups have a `CostCenter` tag. This provides the visibility needed for accurate cost reporting.

1. The Azure Policy Definition (JSON)

First, we define the policy logic in a separate JSON file.

// file: policies/require_cost_center_tag.json
{
  "properties": {
    "displayName": "Require CostCenter Tag on Resource Groups",
    "policyType": "Custom",
    "mode": "All",
    "description": "Enforces the presence of a 'CostCenter' tag on all resource groups.",
    "parameters": {},
    "policyRule": {
      "if": {
        "allOf": [
          {
            "field": "type",
            "equals": "Microsoft.Resources/subscriptions/resourceGroups"
          },
          {
            "field": "tags['CostCenter']",
            "exists": "false"
          }
        ]
      },
      "then": {
        "effect": "deny"
      }
    }
  }
}

2. The Terraform Implementation (HCL)

Next, we use Terraform to create this definition in Azure and then assign it to a subscription.

# main.tf

# Create the policy definition from the JSON file
resource "azurerm_policy_definition" "require_tags" {
  name                 = "require-cost-center-tag"
  policy_type          = "Custom"
  mode                 = "All"
  display_name         = "Require CostCenter Tag on Resource Groups"
  description          = "Enforces the presence of a 'CostCenter' tag on all resource groups."
  policy_rule          = file("policies/require_cost_center_tag.json")
}

# Assign the policy to our primary subscription
resource "azurerm_subscription_policy_assignment" "tagging_enforcement" {
  name                 = "enforce-tagging-on-all-rgs"
  subscription_id      = "/subscriptions/your-subscription-id" # Replace with your subscription ID
  policy_definition_id = azurerm_policy_definition.require_tags.id
  display_name         = "Enforce CostCenter Tagging"
  enforcement_mode     = "Enabled" # Can be "Disabled" to audit without blocking
}

Use Case 2: Blocking Public IPs on Network Interfaces

A critical security guardrail is to prevent VMs from having public IP addresses in secure environments. This policy denies the creation of a Network Interface (NIC) if it includes a public IP configuration.

1. The Azure Policy Definition (JSON)

// file: policies/deny_public_ip_on_nic.json
{
  "properties": {
    "displayName": "Deny Public IP on Network Interfaces",
    "policyType": "Custom",
    "mode": "Indexed",
    "description": "This policy denies the creation of network interfaces with a public IP.",
    "policyRule": {
      "if": {
        "allOf": [
          {
            "field": "type",
            "equals": "Microsoft.Network/networkInterfaces"
          },
          {
            "field": "Microsoft.Network/networkInterfaces/ipconfigurations[*].properties.publicIpAddress.id",
            "notEquals": ""
          }
        ]
      },
      "then": {
        "effect": "deny"
      }
    }
  }
}

2. The Terraform Implementation (HCL)

This time, we'll assign the policy to a specific resource group intended for secure applications.

# Create the policy definition
resource "azurerm_policy_definition" "deny_public_ip" {
  name         = "deny-public-ip-on-nic"
  policy_type  = "Custom"
  mode         = "Indexed"
  display_name = "Deny Public IP on Network Interfaces"
  policy_rule  = file("policies/deny_public_ip_on_nic.json")
}

# Assign the policy to a specific resource group
data "azurerm_resource_group" "secure_app_rg" {
  name = "rg-secure-apps-prod"
}

resource "azurerm_resource_group_policy_assignment" "deny_public_ip_assignment" {
  name                 = "deny-public-ip-for-secure-apps"
  resource_group_id    = data.azurerm_resource_group.secure_app_rg.id
  policy_definition_id = azurerm_policy_definition.deny_public_ip.id
  enforcement_mode     = "Enabled"
}

Advanced Use Case: Grouping Policies with Initiatives

Managing individual policy assignments can be cumbersome. Azure Policy Initiatives (also known as Policy Sets) allow you to group related policies and assign them as a single unit. Here, we'll create a "CIS Benchmark Foundation" initiative.

Terraform Implementation for a Policy Initiative

# Get built-in policy definitions we want to include
data "azurerm_policy_definition" "allow_locations" {
  display_name = "Allowed locations"
}

data "azurerm_policy_definition" "enable_defender" {
  display_name = "Microsoft Defender for Storage should be enabled"
}

# Create the Policy Set Definition (Initiative)
resource "azurerm_policy_set_definition" "cis_foundation" {
  name         = "cis-foundation-initiative"
  policy_type  = "Custom"
  display_name = "CIS Benchmark Foundation Policies"
  description  = "A set of policies to enforce foundational CIS benchmarks."

  # Group the policies together
  policy_definition_reference {
    policy_definition_id = data.azurerm_policy_definition.allow_locations.id
    parameter_values = jsonencode({
        listOfAllowedLocations = {
            value = ["uksouth", "ukwest"]
        }
    })
  }
  
  policy_definition_reference {
    policy_definition_id = data.azurerm_policy_definition.enable_defender.id
  }
}

# Assign the entire initiative to a Management Group
data "azurerm_management_group" "corp" {
  name = "corp-management-group"
}

resource "azurerm_management_group_policy_assignment" "cis_assignment" {
  name                 = "assign-cis-foundation"
  management_group_id  = data.azurerm_management_group.corp.id
  policy_definition_id = azurerm_policy_set_definition.cis_foundation.id
  enforcement_mode     = "Enabled"
}

💡 Best Practices for Azure Policy with Terraform

  • Separate Definitions and Assignments: Keep your policy definitions in a central, version-controlled repository. Apply assignments in separate Terraform configurations specific to each environment or application.
  • Use Initiatives for Grouping: Always group related policies into initiatives (sets). This simplifies management and makes assignments more readable.
  • Leverage Management Groups: Assign foundational policies (like CIS benchmarks or tagging requirements) at the highest possible level, such as a management group, to ensure broad coverage.
  • Start with `Audit` Mode: Before setting a policy's `enforcement_mode` to `Enabled`, deploy it in `Disabled` mode first. This allows you to audit the impact and identify non-compliant resources without blocking developers.
  • Parameterize Your Policies: Use parameters in your policy definitions (e.g., a list of allowed VM SKUs) and provide the values during assignment. This makes your policies reusable.

Conclusion: Scalable and Auditable Azure Governance

By managing Azure Policy with Terraform, you move from manual, error-prone portal configurations to a fully automated, version-controlled governance system. This Policy as Code approach is not just a best practice; it's essential for any organization looking to scale its Azure environment securely and efficiently.