Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure Resource Policy Remediation Terraform Module

Runs an Azure Policy remediation task against a single resource, bringing existing non-compliant resources into line by executing the assigned policy's effect. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Posture


🧩 Overview

  • 🔧 Runs a remediation task against exactly one resource -- the narrowest scope Azure offers.
  • ⚡ Executes the assigned policy's deployIfNotExists or modify effect — this changes infrastructure.
  • 🎚️ Exposes the three blast-radius controls and reports when none of them is set.
  • 🌍 Can restrict the task to named Azure regions.
  • 🚨 Flags tolerates_total_failure, because failure_percentage is a fraction, not a percentage.
  • ⏱️ Documents why ReEvaluateCompliance makes terraform destroy slow — a cancel-and-poll the schema never mentions.

💡 Why it matters: this is the mirror image of a policy exemption. An exemption suppresses evaluation and changes nothing; a remediation reaches into resources Terraform has never managed and modifies them. The two sit next to each other in this library and are easy to confuse.


❤️ Support this project

If this module saved you time:


🗺️ Where this fits in the family

flowchart TB
    DEF["terraform-azurerm-policy-definition"]
    subgraph assign["Assignments, one module per scope"]
        MGA["terraform-azurerm-management-group-policy-assignment"]
        SUBA["terraform-azurerm-subscription-policy-assignment"]
        RGA["terraform-azurerm-policy-assignment"]
    end
    subgraph remed["Remediations, one module per scope, widest first"]
        MGR["terraform-azurerm-management-group-policy-remediation"]
        SUBR["terraform-azurerm-subscription-policy-remediation"]
        RGR["terraform-azurerm-resource-group-policy-remediation"]
        RESR["terraform-azurerm-resource-policy-remediation"]
    end
    EXEMPT["terraform-azurerm-resource-group-policy-exemption"]
    TARGET["existing resources, CHANGED in place"]
    DEF -->|"only deployIfNotExists or modify can be remediated"| RGA
    DEF -->|"policy_definition_id"| MGA
    DEF -->|"policy_definition_id"| SUBA
    MGA -->|"policy_assignment_id, updatable in place"| MGR
    SUBA -->|"policy_assignment_id, updatable in place"| SUBR
    RGA -->|"policy_assignment_id, updatable in place"| RGR
    RGA -->|"policy_assignment_id, updatable in place"| RESR
    RGR -->|"runs the policy effect against"| TARGET
    RESR -->|"runs the policy effect against"| TARGET
    EXEMPT -.->|"the opposite operation: suppresses evaluation, changes nothing"| RGA
    style RGR fill:#0078D4,stroke:#004578,color:#ffffff
    style RESR fill:#0078D4,stroke:#004578,color:#ffffff
    style RGA fill:#004578,stroke:#004578,color:#ffffff
    style TARGET fill:#8A2B06,stroke:#5C1D04,color:#ffffff
    style MGR fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style SUBR fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style MGA fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style SUBA fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style DEF fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style EXEMPT fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading

Azure offers remediations at four scopes as four separate resource types, and this library has one module for each. The dotted edge to the exemption module is the contrast worth holding onto: both attach to an assignment, and they do opposite things.


🧬 What this module builds

flowchart TB
    SCOPEIN["resource_id: WHERE resources get changed"]
    ASSIGNIN["policy_assignment_id: WHICH effect runs"]
    MODEIN["resource_discovery_mode: also decides how DESTROY behaves"]
    BOUNDS["resource_count, parallel_deployments, failure_percentage"]
    FILTERS["location_filters"]
    THIS["azurerm_resource_policy_remediation.this"]
    EFFECT["deployIfNotExists or modify RUNS against existing resources"]
    FACTS["parsed scope facts and posture flags"]
    RISK["is_unbounded and tolerates_total_failure"]
    CANCEL["destroy may CANCEL and POLL before deleting"]
    SCOPEIN -->|"FORCE-NEW"| THIS
    ASSIGNIN -->|"NOT force-new at any scope, unlike exemptions"| THIS
    MODEIN --> THIS
    BOUNDS -->|"all optional: unset means Azure decides"| THIS
    FILTERS -->|"the only geographic limit"| THIS
    THIS --> EFFECT
    THIS --> FACTS
    THIS --> RISK
    MODEIN -.->|"ReEvaluateCompliance only"| CANCEL
    style THIS fill:#0078D4,stroke:#004578,color:#ffffff
    style EFFECT fill:#8A2B06,stroke:#5C1D04,color:#ffffff
    style RISK fill:#8A2B06,stroke:#5C1D04,color:#ffffff
    style CANCEL fill:#8A2B06,stroke:#5C1D04,color:#ffffff
    style ASSIGNIN fill:#004578,stroke:#004578,color:#ffffff
    style SCOPEIN fill:#004578,stroke:#004578,color:#ffffff
    style MODEIN fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style BOUNDS fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style FILTERS fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style FACTS fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading

This diagram is shared with terraform-azurerm-resource-policy-remediation, deliberately: the two resources are identical in the schema apart from the name of the scope field.

Resource Cardinality Notes
azurerm_resource_policy_remediation.this single, named this Named by you, so a group may hold several against different assignments.

✅ Provider / Versions

Item Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module. The caller configures provider "azurerm" { features {} }, auth and subscription.

Schema notes that bite, each confirmed against the provider's own source at the pinned version:

  • 🔴 resource_discovery_mode = "ReEvaluateCompliance" changes how DESTROY behaves. The provider must cancel the running task and poll until the cancellation completes before deleting — and it checks the state first, because cancelling a completed remediation returns a 400. Invisible in the schema.
  • 🔴 failure_percentage is a FRACTION between 0 and 1, despite the name. 0.1 is ten per cent. 1 is one hundred.
  • 🔴 failure_percentage = 0 is silently discarded. The validator accepts it; the expand function only sends the field when it is non-zero. The strictest setting a caller can express becomes Azure's default. This module rejects 0.
  • 🔴 policy_assignment_id is NOT force-new at any scope — the opposite of the policy exemption resources, where three of the four scopes replace.
  • ⚠️ name must be lower case, because Azure returns it lower-cased and an upper-case name would differ on every plan. The provider enforces this; the reason is only in its source comment.
  • ⚠️ The forbidden character set %^#/\&? is the provider's empirical finding, not a published Azure rule — Azure publishes naming rules for assignments, definitions and exemptions, but not for remediations.
  • ⚠️ policy_definition_reference_id is a SINGLE string here, where the exemption resources take a list.
  • ⚠️ The management-group-scoped remediation has no resource_discovery_mode at all — the one scope of four without it.
  • ✅ The default ExistingNonCompliant is already the cheaper, more predictable mode. This module keeps it.
  • No tags.

🔑 Required Azure RBAC Roles / Permissions

Action Role Scope
Create / update / delete the remediation Resource Policy Contributor, or a custom role with Microsoft.PolicyInsights/remediations/* The resource group
Read it for plan Reader The resource group
Perform the remediation itself Whatever the policy's deployIfNotExists / modify effect requires — held by the assignment's managed identity, not by you The remediated resources

🔴 The permission that does the work is not yours. A remediation executes under the policy assignment's managed identity, using the roles granted to that identity in the assignment. So triggering a remediation is a small permission, and what it then changes is bounded by a different principal's rights entirely. Review both together — terraform-azurerm-policy-assignment emits identity_principal_id for exactly this.

✅ No secret is involved. This resource accepts none and emits none.


Azure Prerequisites

  • Microsoft.PolicyInsights registered on the subscription.
  • An existing policy assignment at or above this scope, whose policy effect is deployIfNotExists or modify. Anything else gives the task nothing to do.
  • A managed identity on that assignment, with the roles the effect needs. Without it the remediation runs and fails.
  • An evaluation cycle to have run, unless you are using ReEvaluateCompliance.
  • A generous delete timeout if you use ReEvaluateCompliance.

📁 Module Structure

terraform-azurerm-resource-policy-remediation/
├── providers.tf    # required_version + pinned azurerm; no provider block
├── variables.tf    # 3 required inputs, 7 optional, 21 validations
├── main.tf         # the keystone `this` + derived locals
├── outputs.tf      # 26 outputs; id first; none sensitive
├── README.md       # this file
├── SCOPE.md        # the cross-module contract
├── LICENSE         # MIT
└── .gitignore      # the canonical library ignore set

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "remediate_diagnostics" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-policy-remediation.git?ref=v1.0.0"

  name                 = "remediate-diagnostics"
  resource_id          = "${module.rg.id}/providers/Microsoft.Storage/storageAccounts/observabilitylogs"
  policy_assignment_id = module.deploy_diagnostics.id

  resource_count       = 100
  parallel_deployments = 5
  failure_percentage   = 0.1
}

The caller configures the provider, its authentication and the mandatory features {} block. The module declares none of them.


🔌 Cross-Module Contract

Consumes

Input Type Source
resource_id string Whichever module creates the resource → its id
policy_assignment_id string terraform-azurerm-policy-assignment → id

Emits

Output Description
id The remediation's Resource ID
is_unbounded, bounded_control_count Whether any blast-radius control was set
tolerates_total_failure Whether failure_percentage is 1
re_evaluates_compliance Whether destroy will have to cancel and poll
is_region_restricted, location_filter_count Geographic limits
subscription_id, resource_group_name, provider_namespace, resource_type, resource_name Parsed scope facts
4 constant facts It changes resources; only two effects are remediable; destroy may cancel first; the scope covers exactly one resource

📚 Example Library

1 · The smallest real call
module "remediation" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-policy-remediation.git?ref=v1.0.0"

  name                 = "remediate-diagnostics"
  resource_id          = var.resource_id
  policy_assignment_id = var.policy_assignment_id
}

⚠️ This is legal and it is the shape to be careful with: no blast-radius control at all. Azure's own defaults decide how many resources change and how fast. is_unbounded reports true for exactly this call.

2 · Bounding what the task may change
resource_count       = 100
parallel_deployments = 5
failure_percentage   = 0.1

🎚️ These three are the only things limiting the change. resource_count caps how many resources are touched, parallel_deployments caps the rate, and failure_percentage stops the task once too many have failed.

💡 bounded_control_count reports how many of the three you set, so a check can require all of them.

3 · The fraction that looks like a percentage
failure_percentage = 0.1   # ten per cent

# failure_percentage = 50  # REJECTED: not a percentage
# failure_percentage = 1   # legal, but means ONE HUNDRED per cent

🚨 The argument is named percentage and takes a fraction between 0 and 1. Writing 50 is rejected outright; writing 1 intending one per cent silently tolerates every remediation failing.

💡 tolerates_total_failure exists because the module cannot reject 1 — it is a legal value — but it can make the consequence visible.

4 · The zero that does nothing
# REJECTED by this module:
failure_percentage = 0

🚧 The provider's validator accepts 0, but its expand function only sends the field when the value is non-zero. So 0 — which reads as "abort on the first failure" — is discarded, and Azure's default applies instead. The strictest setting a caller can express is indistinguishable from setting nothing, so the module converts that silence into an error.

5 · Restricting the task to specific regions
location_filters = ["eastus2", "westus2"]

🌍 Without filters the task acts wherever it finds non-compliant resources in scope. The provider validates each entry against the real region list, so a typo is caught at plan.

ℹ️ Duplicates are rejected case- and space-insensitively: East US and eastus are the same region.

6 · Remediating one policy inside an initiative
policy_definition_reference_id = "deployDiagnosticsToStorage"

⚠️ Note this is a single string, where the policy exemption resources take a list. A remediation acts on one policy at a time.

💡 Omitting it remediates the assignment as a whole; targets_single_policy_reference reports which you chose.

7 · Re-evaluating compliance first
resource_discovery_mode = "ReEvaluateCompliance"

timeouts = {
  delete = "60m"
}

⏱️ This mode also changes how terraform destroy behaves. With it set, the provider must cancel a still-running task and poll until the cancellation finishes before deleting — so raise the delete timeout alongside it.

ℹ️ The default, ExistingNonCompliant, acts on resources already assessed as non-compliant. It is cheaper and more predictable, and this module keeps it.

8 · Blocking unbounded remediations
check "remediation_is_bounded" {
  assert {
    condition     = !module.remediation.is_unbounded
    error_message = "This remediation sets none of resource_count, parallel_deployments or failure_percentage, so Azure decides how much changes."
  }
}

🚨 A remediation changes live infrastructure. This is the single most useful control available on the resource, and it passes only when a human has thought about the limits.

9 · Requiring all three controls
check "remediation_fully_bounded" {
  assert {
    condition     = module.remediation.bounded_control_count == 3
    error_message = "Set all of resource_count, parallel_deployments and failure_percentage before remediating in production."
  }

  assert {
    condition     = !module.remediation.tolerates_total_failure
    error_message = "failure_percentage is 1, which tolerates every remediation failing. Did you mean 0.01?"
  }
}

💡 The second assertion catches the specific confusion the argument's name invites.

10 · Several remediations with `for_each`
locals {
  tasks = {
    "remediate-diagnostics" = var.diagnostics_assignment_id
    "remediate-tags"        = var.tags_assignment_id
  }
}

module "remediations" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-policy-remediation.git?ref=v1.0.0"
  for_each = local.tasks

  name                 = each.key
  resource_id          = var.resource_id
  policy_assignment_id = each.value

  resource_count       = 50
  parallel_deployments = 2
  failure_percentage   = 0.05
}

ℹ️ for_each is safe: the remediation's Resource ID ends in the name you chose. Note the names are lower case, which the module requires.

11 · Re-pointing at a different assignment
policy_assignment_id = var.new_assignment_id

✅ This updates in place at every scope — unlike the policy exemption resources, where three of the four scopes replace instead. The two families are adjacent and behave differently here, which is worth knowing before reading a plan.

12 · Names the module rejects
# All REJECTED:
name = "Remediate-Diagnostics"   # must be lower case
name = "remediate#diagnostics"   # %^#/\&? are not allowed

🚧 The lower-case rule is not cosmetic: Azure returns the name lower-cased, so an upper-case name would differ on every plan and never settle. The provider enforces it; this module explains why.

13 · The narrowest scope, and what the provider will let through
# This module: exactly one resource, and it never widens.
resource_id = var.resource_id

# REJECTED by this module, though the PROVIDER would accept both:
# resource_id = "/subscriptions/0000.../resourceGroups/rg-prod"   # a resource group
# resource_id = "/subscriptions/0000..."                          # a whole subscription

🛑 This field is validated with azure.ValidateResourceID, whose own source comment says it "should only be used when a more specific Resource ID validation function is unavailable". It parses the string as an ARM ID and nothing more, and an ARM ID needs only a /subscriptions/<id> pair and an even number of segments — so a resource-group ID and a bare subscription ID both pass. The other three scopes each get a strict validator; this one does not.

⚠️ Since a remediation changes resources, a mistyped scope here is considerably worse than the same mistake on an exemption: it would silently run the policy effect across a whole resource group or subscription. The module requires a /providers/ segment and names the right sibling in the error.

14 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-observability"
  location = "eastus2"
}

module "deploy_diagnostics_definition" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-policy-definition.git?ref=v1.0.0"

  name         = "deploy-diagnostics-to-law"
  display_name = "Deploy diagnostic settings to Log Analytics"
}

module "deploy_diagnostics" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-policy-assignment.git?ref=v1.0.0"

  name                 = "deploy-diagnostics"
  resource_group_id    = module.rg.id
  policy_definition_id = module.deploy_diagnostics_definition.id
}

module "grant_remediation_identity" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = module.rg.id

  role_assignments = {
    contributor = {
      principal_id         = module.deploy_diagnostics.identity_principal_id
      role_definition_name = "Contributor"
      principal_type       = "ServicePrincipal"
    }
  }
}

module "remediation" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-policy-remediation.git?ref=v1.0.0"

  name                 = "remediate-diagnostics"
  resource_id          = "${module.rg.id}/providers/Microsoft.Storage/storageAccounts/observabilitylogs"
  policy_assignment_id = module.deploy_diagnostics.id

  resource_count       = 100
  parallel_deployments = 5
  failure_percentage   = 0.1
}

check "remediation_is_bounded" {
  assert {
    condition     = !module.remediation.is_unbounded
    error_message = "This remediation is unbounded."
  }
}

🏗️ Read the chain: a definition is written, assigned, its managed identity is granted the rights the effect needs, and only then is a remediation run. Skip the role assignment and the remediation runs and fails — the identity doing the work is the assignment's, not yours.

⚠️ Terraform orders these only through the module.deploy_diagnostics references they share; it does not understand that the role assignment must propagate first. A remediation started immediately after the grant can still fail on permissions, and re-applying is the fix.


📥 Inputs

Name Type Required Summary
name string yes Lower case, ≤260 chars, no %^#/\&?. Force-new.
resource_id string yes The single resource to remediate. Force-new.
policy_assignment_id string yes The assignment to act on. Updates in place.
policy_definition_reference_id string no One policy inside an initiative. A single string.
resource_discovery_mode string no ExistingNonCompliant (default) or ReEvaluateCompliance.
location_filters list(string) no Restrict to named regions.
resource_count number no Cap on resources touched.
parallel_deployments number no Cap on concurrency.
failure_percentage number no A fraction 0–1, exclusive of 0.
timeouts object no create / read / update / delete.

🧾 Outputs

Output Description
id, name The remediation.
resource_id, subscription_id, resource_group_name, provider_namespace, resource_type, resource_name The scope, raw and parsed.
policy_assignment_id, policy_definition_reference_id, targets_single_policy_reference What it acts on.
resource_discovery_mode, re_evaluates_compliance Which mode, and whether destroy will cancel first.
location_filters, location_filter_count, is_region_restricted Geographic limits.
resource_count, parallel_deployments, failure_percentage The controls, as stored.
bounded_control_count, is_unbounded How many controls were set.
tolerates_total_failure Whether failure_percentage is 1.
remediation_changes_resources_it_is_not_an_audit Constant true.
only_deploy_if_not_exists_and_modify_policies_can_be_remediated Constant true.
destroy_may_have_to_cancel_a_running_task_first Constant true.
scope_covers_exactly_one_resource Constant true.

No output is sensitive. Nothing about this resource is a credential.


🧠 Architecture Notes

A remediation changes resources; an exemption suppresses evaluation. The two attach to the same policy assignment and sit side by side in this library, and they are opposites. Applying this module reaches into infrastructure Terraform has never managed — resources it has never seen, in whatever state they are in — and runs the assigned policy's deployIfNotExists or modify effect against them. Everything else in the module follows from taking that seriously.

The work is done by somebody else's identity. A remediation executes under the policy assignment's managed identity, with the roles granted to that identity. Creating the remediation is therefore a small permission; what it goes on to change is bounded by a different principal's rights. Reviewing one without the other tells you very little, which is why the composition example puts the role assignment in the same frame and the permissions table names the split explicitly.

Three optional arguments are the only brakes, and the empty call sets none of them. resource_count, parallel_deployments and failure_percentage bound how much changes and how fast. The module cannot choose values on a caller's behalf without inventing numbers, so instead it counts how many were set and emits is_unbounded — making the absence of a decision visible rather than pretending a default is one.

failure_percentage is the sharpest trap on the resource, twice over. It is a fraction despite its name, so 1 means one hundred per cent and is exactly what somebody reaches for when they mean one; the module emits tolerates_total_failure because it cannot reject a legal value. And 0 — the strictest possible setting — passes validation and is then discarded by the expand function, silently substituting Azure's default. That second one the module can reject, and does.

ReEvaluateCompliance reaches into the destroy path. With that mode set and the task still running, the provider cancels the remediation and polls until the cancellation settles before it can delete, checking the state first because cancelling a completed task returns a 400. A terraform destroy is then bounded by the delete timeout rather than being immediate, and a destroy that times out leaves a cancelling task behind. None of this appears in the schema; it is only in the provider's delete body.

The naming rules come from three different authorities, and it is worth knowing which. The 260-character limit is what the service itself reports. The forbidden set %^#/\&? is the provider's own empirical finding, stated as such in its source. And the lower-case requirement exists because Azure returns the name lower-cased, so an upper-case name would produce a difference on every plan — a round-trip trap the provider prevents by refusing the input. Azure publishes naming rules for policy assignments, definitions and exemptions, but not for remediations.

Force-new differs from the neighbouring exemption family. Here policy_assignment_id updates in place at all four scopes. On the exemptions, three of the four scopes replace. Nothing in the schema shows either fact; both come from the ForceNew flags in the Go sources, and a reader who generalises from one family to the other will be wrong.


🧱 Design Principles

Concern This module's position Why
Blast radius All three controls optional, and is_unbounded emitted Picking caps for a caller would invent numbers; reporting that none were picked does not.
failure_percentage = 0 Rejected Accepted upstream and then discarded — the strictest setting silently becomes the default.
failure_percentage = 1 Allowed, and flagged Legal, so it cannot be rejected; it is also the value the argument's name invites by mistake.
resource_discovery_mode Provider default ExistingNonCompliant kept Already the cheaper and more predictable mode. Examined and agreed, not overlooked.
Delete timeout Documented rather than raised The module cannot know how long a cancellation takes; it can say when one will happen.
name Lower case enforced, with the reason given Prevents a round-trip diff. The provider enforces it; only its source says why.
Scope choice Narrower siblings named in the errors Four scopes, four resource types, not interchangeable.
Secrets None accepted, none emitted Nothing here is a credential.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module with ?ref=v1.0.0, never a branch. This library is plan-only: a human applies from CI.


🧪 Testing

What the offline gate covers:

  • terraform validate proves the configuration parses and every type is satisfied.
  • terraform fmt -check proves the HCL is canonically formatted.
  • Feeding deliberately bad .tfvars through terraform console fires the input validations — all 21 validation blocks in this module have been proven reachable, each by a fixture that triggers it, with zero condition-evaluation errors.
  • All 16 derived locals have been driven to more than one value, including is_unbounded and tolerates_total_failure in both states, and a CHILD resource ID proving resource_name takes the last segment while resource_type reports the top-level type.

What only a real plan or apply can exercise:

  • Whether the assignment exists, sits at or above this scope, and has a remediable effect.
  • Whether its managed identity holds the roles the effect needs.
  • How long a ReEvaluateCompliance cancellation actually takes.
  • What the remediation changes, which is the part that matters and which no static check can evaluate.

💬 Example Output

id                              = "/subscriptions/.../resourceGroups/rg-observability/providers/Microsoft.PolicyInsights/remediations/remediate-diagnostics"
name                            = "remediate-diagnostics"
resource_group_name             = "rg-observability"
policy_assignment_id            = "/subscriptions/.../providers/Microsoft.Authorization/policyAssignments/deploy-diagnostics"
resource_discovery_mode         = "ExistingNonCompliant"
re_evaluates_compliance         = false
location_filter_count           = 0
is_region_restricted            = false
resource_count                  = 100
parallel_deployments            = 5
failure_percentage              = 0.1
bounded_control_count           = 3
is_unbounded                    = false
tolerates_total_failure         = false
targets_single_policy_reference = false

🔍 Troubleshooting

Symptom Cause Fix
The remediation completes but nothing changed The assignment's policy effect is audit, deny or disabled Only deployIfNotExists and modify are remediable. There is no error for this
The remediation fails on permissions The assignment's managed identity lacks the roles the effect needs Grant them to identity_principal_id, not to your own principal
Far more changed than expected No blast-radius control was set, so Azure's defaults applied Set resource_count, parallel_deployments and failure_percentage; assert is_unbounded is false
The task kept going after many failures failure_percentage was 1, meaning one hundred per cent It is a fraction. 0.01 is one per cent
failure_percentage = 0 had no effect Accepted by the validator, then discarded by the provider's expand This module now rejects 0; omit the argument for Azure's default
terraform destroy hung or timed out ReEvaluateCompliance requires a cancel-and-poll before delete Raise the delete timeout; re-run destroy if it times out
name rejected after working elsewhere It contained capitals Azure lower-cases the name on read, so capitals would diff forever
Nothing was remediated in a region you expected location_filters excluded it Omit the filter, or add the region
A whole resource group was remediated unexpectedly A resource-group ID was passed as resource_id; the provider's generic validator accepts it This module now rejects it and names the correct sibling

🔗 Related Docs

  • azurerm_resource_policy_remediation — provider resource reference.
  • Remediate non-compliant resources — what a remediation task does and which effects it applies to.
  • Sibling modules: terraform-azurerm-resource-group-policy-remediation, terraform-azurerm-subscription-policy-remediation, terraform-azurerm-management-group-policy-remediation, terraform-azurerm-policy-assignment, terraform-azurerm-resource-group-policy-exemption.
  • This module's SCOPE.md.

💙 "Infrastructure as Code should be standardized, consistent, and secure."