Terraform module for a single Azure Policy assignment — works at any scope (management group, subscription, resource group, or specific resource) and handles the system-assigned identity + remediation role assignments in one place.
The Azure AVM ecosystem provides Azure/avm-ptn-policyassignment/azurerm, but it is a pattern module — opinionated about non-compliance messaging and assumes a specific identity flow. This module is a resource module: a thin, predictable wrapper around Microsoft.Authorization/policyAssignments, with the awkward bits (scope handling, identity setup, and the role assignments needed by DINE/Modify policies) handled internally.
Highlights:
- Any scope, one resource. Pass any ARM resource ID via
scope; it becomes the assignment'sparent_id, so management group, subscription, resource group and resource scopes all share one code path. - Identity + roles in one place. Set
managed_identities.system_assigned = trueandidentity_role_assignments = { ... }and the module wires the principal ID through to the role assignments automatically. - HCL-native parameters.
parameters,metadata, etc. accept HCL objects — no manualjsonencode.
module "audit_unmanaged_disks" {
source = "git::https://github.com/emberstack/terraform.git//src/modules/azure-res-policy-assignment?ref=vX.Y.Z"
name = "audit-unmanaged-disks"
display_name = "Audit unmanaged disks"
scope = "/subscriptions/${data.azapi_client_config.current.subscription_id}"
policy_definition_id = module.deny_unmanaged_disks.resource_id
enforce = false # audit-only
parameters = {
effect = { value = "Audit" }
}
}module "deploy_log_analytics_agent" {
source = "..."
name = "deploy-la-agent"
display_name = "Deploy Log Analytics agent on VMs"
scope = "/subscriptions/${var.workload_subscription_id}"
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/${var.deploy_la_agent_definition_guid}"
location = "westeurope"
managed_identities = {
system_assigned = true
}
parameters = {
logAnalytics = { value = var.platform_workspace_resource_id }
}
identity_role_assignments = {
contributor_on_workload = {
role_definition_id_or_name = "Contributor"
# scope defaults to the assignment scope; override here when the
# remediation identity must write outside it.
}
log_analytics_contributor_on_la = {
role_definition_id_or_name = "Log Analytics Contributor"
scope = var.platform_workspace_resource_id
}
}
}module "deny_public_blob" {
source = "..."
name = "deny-public-blob"
scope = "/subscriptions/${var.subscription_id}/resourceGroups/rg-workload"
policy_definition_id = module.deny_public_blob_def.resource_id
not_scopes = [
"/subscriptions/${var.subscription_id}/resourceGroups/rg-workload/providers/Microsoft.Storage/storageAccounts/stpublicassets",
]
non_compliance_messages = [
{
content = "Storage accounts in this resource group must disable public blob access. Contact #infra if you need an exemption."
},
]
}See variables.tf and outputs.tf. Every variable and output
carries a description, and CI enforces that.
- UAI role management. When attaching a user-assigned identity, manage role assignments on the UAI itself — they outlive any single policy assignment, and
identity_role_assignmentshere only targets the system-assigned identity. identity_role_assignmentsfollows the family pattern for name lookup, generated GUID names and adoption — see Role assignments. It differs in one way: the principal is always this assignment's own identity, soprincipalTypeis alwaysServicePrincipal.- Scope changes recreate.
scopeis the assignment'sparent_id, which forces replacement — an assignment at a subscription and the same assignment at a resource group are different ARM resources. The Terraform address does not move, but the resource is still destroyed and recreated, so a deny-effect assignment is unenforced for the length of the apply.