Records a governed waiver for an Azure Advisor recommendation as code β dismissing one recommendation on one target resource for a bounded, reviewable duration instead of clicking "Dismiss" in the portal. Targets
hashicorp/azurerm ~> 4.0.
- π Creates one
azurerm_advisor_suppressionβ a single dismissal of one Advisor recommendation on one target resource. - π― Binds three things: a chosen
name(the waiver's own identity), arecommendation_id(the specific finding), and aresource_id(the target the finding is about). - β³ Prefers a bounded
ttlβ a days:hours:minutes:seconds duration after which the recommendation resurfaces for review β so a waiver cannot silently mask a drifting resource forever. - π§Ύ Emits the suppression's Resource ID and the Advisor-assigned
suppression_id, so the waiver is traceable back to both the finding and the resource. - π§± Standalone primitive: it consumes the target resource by
idand pairs with theazurerm_advisor_recommendationsdata source, which supplies the liverecommendation_id.
π‘ Why it matters: an Advisor recommendation you accept but never record keeps reappearing, and one you dismiss permanently in the portal disappears with no audit trail and no expiry. Expressing the waiver as code makes the decision reviewable, versioned, and β with a bounded
ttlβ self-expiring, so accepted risk is revisited on a schedule instead of forgotten.
If this module saves you time, please consider supporting the work:
- β Star the repository β it helps others find it.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
advisor["Azure Advisor<br/>(produces recommendations)"]
recs["azurerm_advisor_recommendations<br/>(data source)"]
target["Target resource<br/>(sibling module: storage / VM / SQL)"]
sup["azurerm_advisor_suppression<br/>(this module)"]
advisor -->|"raises recommendation on"| target
target -->|"resource_id"| sup
recs -->|"recommendation_id"| sup
sup -->|"dismisses recommendation in"| advisor
classDef this fill:#0078D4,stroke:#004578,color:#fff;
classDef target fill:#004578,stroke:#004578,color:#fff;
classDef ext fill:#F2F2F2,stroke:#8A8A8A,color:#222;
class sup this;
class target target;
class advisor,recs ext;
The suppression sits between a target resource and Azure Advisor. It creates nothing on the resource itself; it tells Advisor to stop surfacing one recommendation about that resource for the life of the suppression.
flowchart LR
subgraph inputs["Inputs"]
n["name"]
rid["recommendation_id"]
resid["resource_id"]
ttl["ttl<br/>(default null = never expires)"]
end
sup["azurerm_advisor_suppression.this"]
subgraph outputs["Outputs"]
oid["id"]
onm["name"]
osid["suppression_id"]
end
n -->|"name"| sup
rid -->|"recommendation_id"| sup
resid -->|"resource_id"| sup
ttl -->|"ttl"| sup
sup -->|"id"| oid
sup -->|"name"| onm
sup -->|"suppression_id"| osid
classDef this fill:#0078D4,stroke:#004578,color:#fff;
classDef target fill:#004578,stroke:#004578,color:#fff;
classDef ext fill:#F2F2F2,stroke:#8A8A8A,color:#222;
class sup this;
class n,rid,resid,ttl,oid,onm,osid ext;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_advisor_suppression.this |
1 | The dismissal record for one recommendation on one target resource |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, authentication, and subscription. |
Schema notes that bite (verified against the live provider schema):
- π Everything is force-new. Changing
name,recommendation_id,resource_id, orttlreplaces the suppression. There is no in-place update path β the resource exposes only create, read, and delete. - π
recommendation_idis not stable forever. Advisor generates recommendation IDs per resource and regenerates them when a recommendation is refreshed. A suppression pinned to a stale ID no longer matches the live recommendation, so the finding can reappear under a new ID. Read the current value from theazurerm_advisor_recommendationsdata source rather than hard-coding it. - β³ The
ttlFORMAT IS NOT WHAT "DD:HH:MM:SS" SUGGESTS. The provider's validator is^(?:[0-9]{1,2}:)?[0-9]{2}:[0-9]{2}:[0-9]{2}$β the days field is OPTIONAL and at most two digits. So"12:00:00"(twelve hours, no days field) is legal, and"100:00:00:00"is not: no postponement longer than 99 days can be expressed. The provider's own error message says the days field "has to be omit" when it is00, but its regex accepts"00:12:00:00"all the same β the message is advice, not the rule. This module mirrors the regex, not the message. - β³ Omitting
ttlis not a smaller version of setting it β it changes the KIND of suppression. In Advisor's own terms, attlmakes this a postpone and its absence makes it a dismiss. Microsoft's documentation states that a postponed recommendation returns to the Active state automatically once the time elapses, and that dismissed recommendations are excluded from Advisor's completion-progress calculation. An indefinite suppression therefore hides the finding and removes it from the denominator the score is measured against β the posture improves because the question was withdrawn. Reported through theis_dismissalandexcluded_from_advisor_completion_progressoutputs. - π΄ A MANAGEMENT GROUP OR TENANT SCOPE IS UNREACHABLE THROUGH THIS RESOURCE. Microsoft's ARM
reference lists
Microsoft.Advisor/recommendations/suppressionsas deployable at tenant, management group, subscription and resource group scope. The provider validatesresource_idwith a parser that requires asubscriptionspath segment and fails with "no subscription ID found" without one, so neither broader scope can be reached at all. That is a provider limitation rather than an Azure one, and nothing in the provider documentation says the scope set is narrower than the API's. - π·οΈ No
tags, nolocation. Confirmed against the live provider schema, this resource type accepts neither, so the module deliberately omits both. It is not a regional resource β it lives under the target recommendation. - β No
updatetimeout. Thetimeoutsblock exposes onlycreate,read, anddelete; there is no update operation to time out.
- The
Microsoft.Advisor/suppressions/writeandMicrosoft.Advisor/suppressions/deleteactions at the scope of the target resource (or above). The built-in Contributor role at the target scope carries these, as does an equivalent custom role that includes theMicrosoft.Advisor/suppressions/*actions. - No tenant-wide privilege is required. Grant at the smallest scope that contains the target resource named
by
resource_id.
- The
Microsoft.Advisorresource provider registered on the target subscription. - An existing target resource (or subscription / resource-group scope) whose resource
idis supplied asresource_id, and against which Advisor has produced the recommendation being suppressed. - The live
recommendation_idof the recommendation to suppress β read it from theazurerm_advisor_recommendationsdata source or the Advisor REST API rather than hard-coding a value that Advisor may have regenerated. - The caller configures the
provider "azurerm" { features {} }block, authentication, and subscription.
terraform-azurerm-advisor-suppression/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # deeply-typed inputs; name/recommendation_id/resource_id/ttl + timeouts tail
βββ main.tf # the single keystone azurerm_advisor_suppression.this
βββ outputs.tf # id first, then name, then suppression_id and the target references
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT, Copyright (c) 2026 Casey Wood
βββ .gitignore # canonical library ignore set
The smallest real call β a bounded, thirty-day waiver on a resource, using a recommendation ID read from the data source:
provider "azurerm" {
features {}
}
data "azurerm_advisor_recommendations" "example" {}
module "advisor_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "accepted-cost-tradeoff"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.storage.id
ttl = "30:00:00:00" # thirty days, then the recommendation resurfaces for review
}βΉοΈ The caller owns the provider, authentication, and the mandatory
features {}block. Pin the module with?ref=v1.0.0; never track a branch.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_id |
string |
the target resource's own module (id) β for example terraform-azurerm-storage-account, terraform-azurerm-linux-virtual-machine, terraform-azurerm-mssql-server |
recommendation_id |
string |
the azurerm_advisor_recommendations data source (recommendations[*].recommendation_name) |
name |
string |
caller-chosen suppression name (the waiver's own identity) |
ttl |
string |
caller-chosen duration; omit only for a conscious permanent waiver |
Emits
| Output | Description |
|---|---|
id |
Advisor suppression Resource ID (first) |
name |
Advisor suppression name |
suppression_id |
The GUID Azure Advisor assigned to the suppression |
recommendation_id |
The suppressed recommendation's ID |
resource_id |
The target Resource ID the recommendation is suppressed for |
ttl |
The suppression duration, or null when it never expires |
The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:
variable "storage_archive_id" {
description = "id of an existing storage archive that these examples reference but do not create."
type = string
}
variable "storage_logs_id" {
description = "id of an existing storage logs that these examples reference but do not create."
type = string
}1 Β· Minimal call β a bounded waiver
module "waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "accepted-cost-tradeoff"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.storage.id
ttl = "30:00:00:00"
}π‘ The
ttlis adays:hours:minutes:secondsduration. Thirty days keeps the waiver honest β the recommendation returns for review once it lapses.
2 Β· Reading the recommendation_id from the data source
data "azurerm_advisor_recommendations" "all" {
filter_by_category = ["Cost"]
filter_by_resource_groups = ["rg-platform"]
}
module "waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "cost-waiver-first-match"
recommendation_id = data.azurerm_advisor_recommendations.all.recommendations[0].recommendation_name
resource_id = data.azurerm_advisor_recommendations.all.recommendations[0].resource_name
ttl = "14:00:00:00"
}βΉοΈ
recommendation_nameis the recommendation's GUID;resource_nameis the target Resource ID Advisor raised it against. Reading both from the same element keeps the pair consistent.
3 Β· A short cooling-off suppression (7 days)
module "cooling_off" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "post-deploy-cooling-off"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.vm.id
ttl = "07:00:00:00"
}π‘ A short
ttlsuits a freshly deployed resource whose metrics have not yet stabilized β the recommendation is silenced briefly, then returns automatically.
4 Β· A thirty-day operational waiver
module "sprint_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "deferred-to-next-sprint"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.sql_server.id
ttl = "30:00:00:00"
}βΉοΈ Naming the waiver after the reason ("deferred-to-next-sprint") makes the Advisor blade self-documenting for the next reviewer.
5 Β· Permanent waiver β a conscious opt-out
module "permanent_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "waiver-CHG0012345-by-design"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.storage.id
# ttl omitted β the suppression never expires
}
β οΈ Omittingttlsuppresses the recommendation indefinitely. Reach for this only when the acceptance is genuinely permanent and tracked elsewhere (a change record, an architecture decision). A boundedttlis the safer default because it forces the finding back into view.
6 Β· Suppressing at subscription scope
module "subscription_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "subscription-level-acceptance"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000"
ttl = "30:00:00:00"
}βΉοΈ
resource_idmay be a subscription scope when Advisor raises the recommendation there. The module validates that the value begins with/subscriptions/.
7 Β· Custom operation timeouts
module "with_timeouts" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "accepted-cost-tradeoff"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.storage.id
ttl = "30:00:00:00"
timeouts = {
create = "10m"
delete = "10m"
}
}βΉοΈ Timeouts are optional β omit the block to accept the provider defaults (create/delete 30m, read 5m). There is no
updatetimeout: every argument change forces replacement.
8 Β· The same recommendation across many resources with for_each
locals {
# A recommendation category accepted on several storage accounts, each with its own recommendation_id.
waived_storage = {
logs = {
resource_id = var.storage_logs_id
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
}
archive = {
resource_id = var.storage_archive_id
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[1].recommendation_name
}
}
}
module "storage_waivers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
for_each = local.waived_storage
name = "cost-waiver-${each.key}"
recommendation_id = each.value.recommendation_id
resource_id = each.value.resource_id
ttl = "30:00:00:00"
}π‘ A suppression is scoped to one recommendation on one resource, so a category accepted across resources needs one record per resource. Keying
for_eachby a stable map key keeps additions and removals from re-indexing the rest.
9 Β· A governed waiver register
locals {
waiver_register = {
"sql-tls-accepted" = {
resource_id = module.sql_server.id
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
ttl = "30:00:00:00"
}
"vm-rightsizing-deferred" = {
resource_id = module.vm.id
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[1].recommendation_name
ttl = "14:00:00:00"
}
}
}
module "waivers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
for_each = local.waiver_register
name = each.key
recommendation_id = each.value.recommendation_id
resource_id = each.value.resource_id
ttl = each.value.ttl
}π‘ A single map becomes the auditable register of every accepted recommendation β each entry names the waiver, its target, and its expiry in one place under version control.
10 Β· Least-visibility-loss variant β a named, bounded, reviewable waiver
# Preferred: an explicit reason in the name and a bounded ttl.
module "reviewable_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "waiver-INC0004521-accepted-until-remediation"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.storage.id
ttl = "30:00:00:00"
}π The one meaningful safety control on a suppression is how long it hides a finding. A descriptive
nameplus a boundedttlkeeps accepted risk visible: the waiver explains itself, and the recommendation returns on a known date rather than disappearing forever.
11 Β· Integration with the storage-account sibling
module "storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "stplatformlogs"
resource_group_name = module.rg.name
location = module.rg.location
}
module "storage_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "storage-cost-accepted"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.storage.id
ttl = "30:00:00:00"
}βΉοΈ The suppression consumes the storage account by its resource
id. The implicit reference orders creation: the account exists before the waiver is written against it.
12 Β· Integration with the linux-virtual-machine sibling
module "nic_vm" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface.git?ref=v1.0.0"
name = "nic-vm"
resource_group_name = module.rg.name
location = module.rg.location
ip_configurations = [{
name = "internal"
subnet_id = var.vm_subnet_id
}]
}
module "vm" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-linux-virtual-machine.git?ref=v1.0.0"
admin_username = "azureadmin"
network_interface_ids = [module.nic_vm.id]
size = "Standard_D2s_v5"
name = "vm-batch-01"
resource_group_name = module.rg.name
location = module.rg.location
# ... size, image, and network inputs ...
}
module "vm_rightsizing_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "vm-rightsizing-deferred"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.vm.id
ttl = "14:00:00:00"
}π‘ A right-sizing recommendation on a batch VM that peaks monthly is a common false positive β a two-week waiver silences it across a cycle without hiding it permanently.
13 Β· Integration with the mssql-server sibling
module "sql_server" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server.git?ref=v1.0.0"
azuread_administrator = {
login_username = "sqladmin@contoso.example"
object_id = "00000000-0000-0000-0000-000000000000"
}
name = "sql-platform-eus2"
resource_group_name = module.rg.name
location = module.rg.location
}
module "sql_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "sql-recommendation-accepted"
recommendation_id = data.azurerm_advisor_recommendations.example.recommendations[0].recommendation_name
resource_id = module.sql_server.id
ttl = "30:00:00:00"
}
β οΈ Suppress security-category recommendations only after a documented risk acceptance. A boundedttlkeeps the finding coming back until it is genuinely remediated.
14 Β· ποΈ End-to-end composition β a governed waiver path
Wire a resource group, a target storage account, the Advisor recommendations data source, and this suppression into one reviewable waiver.
provider "azurerm" {
features {}
}
# 1. Resource group to hold the target resource.
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-platform"
location = "eastus2"
}
# 2. The target resource Advisor produces recommendations about.
module "storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "stplatformlogs"
resource_group_name = module.rg.name
location = module.rg.location
}
# 3. The live recommendations, filtered to the target's resource group and category.
data "azurerm_advisor_recommendations" "cost" {
filter_by_category = ["Cost"]
filter_by_resource_groups = [module.rg.name]
}
# 4. The waiver β a bounded, named dismissal of one recommendation on the storage account.
module "storage_cost_waiver" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-advisor-suppression.git?ref=v1.0.0"
name = "storage-cost-accepted-Q3"
recommendation_id = data.azurerm_advisor_recommendations.cost.recommendations[0].recommendation_name
resource_id = module.storage.id
ttl = "30:00:00:00"
}π The result: the accepted Cost recommendation on the storage account is dismissed for thirty days, recorded in version control with a self-describing name, and set to resurface automatically for the next review. Read the
recommendation_idfrom the data source rather than pinning a literal, since Advisor regenerates recommendation IDs.
Required
| Name | Type | Description |
|---|---|---|
name |
string |
The suppression's own name β non-empty; no length ceiling is imposed. Force-new. |
recommendation_id |
string |
GUID of the Advisor recommendation to suppress. Force-new. |
resource_id |
string |
Resource ID of the target the recommendation applies to. Force-new. |
Optional
| Name | Type | Default | Description |
|---|---|---|---|
ttl |
string |
null |
Duration; the days field is optional and at most 2 digits, so "12:00:00" works and nothing beyond 99 days can be expressed. Null makes it a dismissal, not a short postponement. Force-new. |
timeouts |
object(...) |
null |
Optional create/read/delete timeouts (no update). |
Full input schemas
variable "name" {
type = string
# Non-empty. NO length ceiling: the provider publishes none, and a limit invented here would
# refuse a name someone may already own - which would block `terraform destroy` as well as apply.
# Rejected: a bare GUID (almost always recommendation_id misplaced) and a Resource ID.
# Immutable: changing this forces replacement.
}
variable "recommendation_id" {
type = string
# A non-empty Advisor recommendation ID (read from the azurerm_advisor_recommendations data source).
# Immutable: changing this forces replacement.
}
variable "resource_id" {
type = string
# A full Azure Resource ID beginning with /subscriptions/.
# Immutable: changing this forces replacement.
}
variable "ttl" {
type = string
default = null
# null, or a duration matching the provider's own ^(?:[0-9]{1,2}:)?[0-9]{2}:[0-9]{2}:[0-9]{2}$ -
# so the DAYS FIELD IS OPTIONAL and at most two digits: "12:00:00" is legal, "100:00:00:00" is not.
# null / omitted -> a DISMISSAL rather than a postponement: indefinite, and excluded from Advisor's
# completion-progress calculation. Immutable: changing this forces replacement.
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
delete = optional(string)
})
default = null
}This resource type supports neither
tagsnorlocation, so this module omits both. Thetimeoutsblock has noupdatemember because the resource has no update operation.
| Output | Description | Notes |
|---|---|---|
id |
Advisor suppression Resource ID | Emitted first. A scoped ID: the target's own ID with the suppression path appended. |
name |
Suppression name | The only free-text field the API carries β the only place a justification fits. |
suppression_id |
GUID Azure Advisor assigned to the suppression | Computed. Distinct from recommendation_id. |
recommendation_id |
The suppressed recommendation's ID | Per-resource, not per-category. |
resource_id |
Target Resource ID the recommendation is suppressed for | Use this, not id, when the advised resource is wanted. |
ttl |
The duration originally requested | Empty when unset. Not a countdown β Advisor reports no time remaining here. |
is_postponement |
A ttl was supplied |
The recommendation returns to Active by itself. |
is_dismissal |
No ttl |
Hidden until this resource is deleted. |
excluded_from_advisor_completion_progress |
True for a dismissal | The score improves because the question was withdrawn. |
recommendation_returns_to_active_automatically |
True for a postponement | Assert on this if waivers must be time-bounded. |
scope_kind |
subscription, resourceGroup or resource |
The three read identically in a plan; the blast radius does not. |
suppresses_one_recommendation_on_one_scope |
Constant true |
No wildcard, no category form, no tag selector. |
cannot_target_management_group_or_tenant_scope |
Constant true |
A provider limitation, not an Azure one. |
every_argument_is_force_new |
Constant true |
Extending a postponement restarts the clock. |
hides_the_finding_without_changing_the_resource |
Constant true |
The misconception most worth heading off. |
No secret is output β the resource stores none. Nothing here is marked
sensitive.
- The whole resource is a dismissal record. There is no data plane, no network surface, and no secret.
The only meaningful lever is how long β and how visibly β a finding is hidden, which is why
ttland a descriptivenamecarry the design weight. - Every argument is force-new. Confirmed against the live provider schema,
name,recommendation_id,resource_id, andttlare all immutable in place β changing any of them recreates the suppression. There is no update operation, so thetimeoutsblock omitsupdate. recommendation_iddrifts. Advisor regenerates recommendation IDs per resource. A literal that was valid at authoring time can go stale, after which the suppression targets a recommendation that no longer exists and the finding reappears under a new ID. Source the value from theazurerm_advisor_recommendationsdata source so it tracks the live recommendation.ttlis the safety control, and it selects the KIND of suppression. With a duration this is a postpone: Microsoft's documentation states the recommendation returns to Active automatically when the time elapses. Without one it is a dismiss: indefinite, and excluded from Advisor's completion-progress calculation β so the score improves because the finding left the denominator, not because anything was fixed. A boundedttlis the documented posture; a permanent waiver is a deliberate opt-out (Example 5).- Nothing here changes the resource. A suppression alters what Advisor displays. A suppressed availability recommendation leaves the resource exactly as unavailable as it was β which is worth saying because this module sits next to the resource, is named after the finding, and makes the finding disappear.
- The scope set is narrower than the API's. Tenant and management-group suppressions exist in ARM but
cannot be created through this resource; the provider's
resource_idvalidator requires asubscriptionssegment. The failure reads as a parsing error rather than a scope one. - Terraform cannot see the clock. When a postponement expires, nothing about the managed resource changes, so no drift is reported and no plan is produced. The record stays in state; only Advisor's display changes.
for_eachkey stability. A recommendation accepted across several resources needs one suppression per resource. Keyfor_eachby a stable identifier so adding or removing one waiver never re-indexes the others.- No
tags, nolocation. Confirmed against the live provider schema, this resource type accepts neither, so β unlike most azurerm modules β there is notagstail here. features {}dependence. Like every azurerm resource, this will not plan without the caller'sprovider "azurerm" { features {} }block. That is expected; the module never declares a provider.
The empty call already produces the hardened resource. Each relaxation is an explicit caller opt-out.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Visibility of accepted findings | A bounded ttl is documented as the recommended posture, so a suppressed recommendation resurfaces for review |
Omit ttl (or set it null) to suppress indefinitely β a conscious permanent waiver, reported through is_dismissal |
| Target scoping | resource_id must be a full Resource ID whose subscription segment is a GUID and whose path has an even, non-empty segment count β mirroring the provider's own parser |
Not permitted β a malformed scope is rejected at terraform validate |
| Recommendation freshness | recommendation_id is validated as a GUID, and the all-zero placeholder is rejected because Advisor accepts a suppression for a recommendation that does not exist and reports no error |
Pin a literal recommendation ID (discouraged β it can go stale) |
| Duration format | ttl mirrors the provider's regex exactly β optional 1β2 digit days field β and an all-zero duration is rejected, so a malformed value fails at terraform validate with no credentials |
Not permitted β but note the ceiling this implies: no postponement beyond 99 days is expressible |
| Secret handling | The module neither accepts nor emits a secret | Not applicable |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0; never track a branch. - Plan-only during authoring β a human runs
terraform plan/applyfrom CI against real credentials.
The offline proof gate is what this module guarantees:
terraform init -backend=falseβ resolves the pinned provider with no backend and no cloud call.terraform validateβ proves the configuration is type-correct against the pinned provider schema, catching every typing mistake the input schemas surface, plus theresource_idResource-ID check, thettlduration-format check, and the non-emptyname/recommendation_idvalidations.terraform fmt -checkβ enforces canonical formatting.
Only terraform plan (run by a human, from CI, against real credentials) reaches the Advisor API and proves
the recommendation_id matches a live recommendation. This module is never applied during authoring.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform/providers/Microsoft.Storage/storageAccounts/stplatformlogs/providers/Microsoft.Advisor/recommendations/9c1b2d3e-4f56-7890-abcd-ef1234567890/suppressions/storage-cost-accepted-Q3"
name = "storage-cost-accepted-Q3"
suppression_id = "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809"
recommendation_id = "9c1b2d3e-4f56-7890-abcd-ef1234567890"
resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform/providers/Microsoft.Storage/storageAccounts/stplatformlogs"
ttl = "30:00:00:00"| Symptom | Cause | Fix |
|---|---|---|
| Plan wants to replace the suppression on a small edit | You changed name, recommendation_id, resource_id, or ttl β all are force-new |
Restore the original value, or accept the recreation; there is no in-place update |
| Suppressed recommendation reappears in Advisor | The recommendation_id went stale (Advisor regenerated it) and no longer matches a live recommendation |
Re-read recommendation_id from the azurerm_advisor_recommendations data source and re-create the waiver |
ttl must be null, or a duration matching DD:HH:MM:SS with an OPTIONAL one-or-two-digit days field at terraform validate |
The duration does not match the provider's regex β commonly a one-digit hour ("1:00:00", which needs "01:00:00") |
Use "07:00:00:00" or "12:00:00"; hours, minutes and seconds are two digits each |
ttl has a days field of three or more digits |
A postponement longer than 99 days was requested | Not expressible here. Omit ttl for a dismissal β noting it is then excluded from completion progress β and track the review date outside Terraform |
resource_id must be a full Azure Resource ID beginning with /subscriptions/<id> |
resource_id is a name, a partial value, has a trailing slash, or is a management group path |
Pass a full Resource ID β normally a sibling module's id output. A management group scope cannot be used: the provider's validator requires a subscriptions segment |
the subscription segment of resource_id must be a GUID |
A placeholder such as /subscriptions/my-subscription/... |
Use the real subscription GUID. The provider's own validator accepts the placeholder and lets it fail against Azure at apply |
recommendation_id is the all-zero placeholder GUID |
A template value was left in place | Read the real GUID from the azurerm_advisor_recommendations data source. Advisor accepts a suppression for a recommendation that does not exist and reports no error, so this would otherwise apply cleanly and suppress nothing |
| Advisor score improved but nothing was fixed | The suppression is a dismissal, and dismissed recommendations are excluded from completion-progress calculation | Expected, and reported by excluded_from_advisor_completion_progress. Use a bounded ttl if the finding should stay in the denominator |
AuthorizationFailed creating the suppression |
The identity lacks Microsoft.Advisor/suppressions/write at the target scope |
Grant Contributor (or an equivalent custom role) at the smallest scope containing resource_id |
| Recommendation never comes back after the intended date | ttl was omitted, so the suppression never expires |
Set a bounded ttl, or delete the suppression when the waiver ends |
Provider configuration not present / features error |
Caller has no provider "azurerm" { features {} } block |
Add the provider block in the root module; the module never declares one |
- Provider resource:
azurerm_advisor_suppression - Provider data source:
azurerm_advisor_recommendations - Azure Advisor recommendation state management
- Sibling modules:
terraform-azurerm-storage-account,terraform-azurerm-linux-virtual-machine,terraform-azurerm-mssql-server,terraform-azurerm-resource-group - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."