Repository navigation
Retirements
tf-snag can tell you which resources in your estate are on a published Azure retirement notice — a Basic SKU public IP, a v1 Application Gateway, a storage account still accepting TLS 1.0 — and how long you have left.
terraform plan -out tfplan
terraform show -json tfplan > plan.json
tf-snag -check retirements -plan plan.jsonretirement(s): 2
! azurerm_public_ip — Basic SKU public IP addresses retire (2025-09-30), 3 instance(s)
azurerm_public_ip.bastion
module.network.azurerm_public_ip.egress[0]
module.network.azurerm_public_ip.egress[1]
retired 351 days ago — Move to Standard SKU. Basic cannot be upgraded in place while attached, so set the address to static, detach it, upgrade, then reattach.
https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/public-ip-basic-upgrade-guidance
! azurerm_storage_account — Azure Storage stops accepting TLS 1.0 and 1.1 (2026-02-03), 1 instance(s)
azurerm_storage_account.legacy
in 140 days — Set min_tls_version = "TLS1_2", after checking storage logs for clients still negotiating 1.0 or 1.1.
Nothing leaves your network. The catalogue of retirement notices is compiled into the tf-snag binary, so the check needs no credentials, no Azure permissions and no outbound access — the same bargain as drift and deprecations.
The estate comes from the plan you already produce. terraform show -json includes every resource with all its attributes, not just the ones changing, so no extra Terraform run and no cloud access are needed.
The catalogue can go stale. Every other check is derived from the plan and cannot be wrong; this one depends on a file that ships with a release. Reports carry the catalogue's date, and a catalogue more than 90 days old prints a warning, because a clean result from a stale catalogue is a claim tf-snag has not really checked. Upgrade tf-snag to get the current one.
An entry matches a resource type and, usually, an attribute value: azurerm_public_ip with sku = "Basic". Some entries match a whole resource type, where every instance is affected — azurerm_frontdoor is always Front Door (classic).
The check reads planned values, so a resource this plan already fixes is not reported, and one this plan destroys is not reported either.
Findings are grouped per retirement, not per resource: "stop using Basic public IPs" is one decision however many resources carry it. That is also how work items are raised.
Worked out at run time against the retirement date:
| Urgency | Meaning |
|---|---|
retired |
the date has passed. The resource may still work — Azure often leaves retired SKUs running, unsupported and outside the SLA |
imminent |
within 90 days |
approaching |
within 180 days |
scheduled |
announced, further out |
In SARIF, retired and imminent are errors and the rest warnings, so the Scans tab ranks by deadline rather than by wording.
By default every un-suppressed retirement trips -exit-code. Adopting the check on an older estate usually means starting narrower:
tf-snag -check retirements -plan plan.json -retirements-fail-within 90dEverything is still reported; only retirements inside 90 days fail the run.
Rules go in the same ignore file as everything else, by catalogue id or by resource address:
retirements:
- id: azure-public-ip-basic-sku
reason: "CSES deployment, exempt from the retirement"
- address: module.legacy.azurerm_lb.edge
reason: "decommissioned in Q3, tracked in PLAT-412"An id rule waives the whole finding. An address rule drops just that resource — the finding is suppressed only when every instance has been waived, so a waiver for one resource cannot silence the other nine. Both appear in the Exceptions Register.
-retirements <file> replaces the built-in catalogue, so you can add internal deadlines, trial an entry before it ships, or pin a catalogue in an air-gapped environment:
updated: 2026-09-16
retirements:
- id: internal-basic-tier-deadline
title: No Basic tier storage after the Q3 platform review
retires_on: 2027-01-01
url: https://intranet.example/deadlines
severity: medium
match:
type: azurerm_storage_account
attributes:
account_tier: Standard
remediation: Talk to the platform team before adding more.Attribute tests take three shapes: a bare value means equals, {in: [a, b]} a set, and {not: value} a negation. Nested blocks are dotted paths (sku.tier). An entry with no attributes matches every resource of its type. A resource whose plan does not carry the attribute never matches, including under not.
Set Checks to one of the retirement options on the tf-snag task, and optionally Fail only on retirements within (days):
- task: tf-snag@0
inputs:
checks: 'drift,retirements'
retirementsFailWithin: '90' # optional: only fail on what is close
retirementsFile: retirements.yaml # optional: your catalogue instead of the built-in oneFindings show in the Retirements pivot of the tf-snag tab, in the Teams card, and as one work item per retirement. See Azure DevOps Pipelines.
- Retirements with no signal in the plan. API Management stv1 versus stv2, for example, looks identical in Terraform.
-
Resources the provider has dropped. MySQL Single Server and App Service Environment v1/v2 are retired, but
azurermv4 removed those resource types, so no current plan can contain them. -
Resources that are not their own Terraform resource. A VMSS instance public IP configuration is not a
publicIPAddressesresource, so a Basic IP there never appears as anazurerm_public_ip. - Anything Microsoft has not dated. Legacy is not the same as retiring, and an entry with no date would be an opinion.
A clean retirements report is not proof of full coverage.
Start here
How it works
Integrations
Project