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.