Skip to content

Retirements

Wes Key edited this page Sep 17, 2026 · 2 revisions

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.json
retirement(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.

Where the data comes from

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.

What it checks

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.

Urgency

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.

Failing a build

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 90d

Everything is still reported; only retirements inside 90 days fail the run.

Ignoring a retirement

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.

Your own deadlines

-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.

In a pipeline

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 one

Findings 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.

What it does not catch

  • 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 azurerm v4 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 publicIPAddresses resource, so a Basic IP there never appears as an azurerm_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.

Clone this wiki locally