Infrastructure

Azure Storage: validate lifecycle policy impact before deletion

A production runbook for qualifying an Azure Storage lifecycle policy with Blob inventory, versions, snapshots, delete or tiering rules, evidence, validation and rollback before removing data.

25 Jul 2026 azurestorageblob-storagelifecycle-managementretentionobservabilitydata-protectionrunbookrollbackproduction

An Azure Storage lifecycle policy often looks harmless: move old blobs to Cool or Archive, delete stale snapshots, clean up temporary exports. In production, that is a real operational change. A broad rule can hit a shared prefix, delete a version that was still useful, archive an object the application reads every night, or make rollback much slower than the team expected.

The use case is a Storage account that receives application exports, attachments and diagnostic files. The team wants to enable cleanup to reduce noise and cost. The hard part is not writing the policy. The hard part is proving what it would touch, which paths must be excluded, how to validate after activation, and what to do if a useful object is deleted or moved.

The goal of this runbook is to make a clear decision: enable the policy, narrow it, postpone it, or roll it back before evidence is lost.

Define the data scope

Start by naming the containers, prefixes and object types in scope. A lifecycle policy acts on blobs, versions and snapshots based on age, prefix, blob type and sometimes index tags. It does not understand the business intent behind a folder name.

yaml storage-lifecycle-scope.yml
storage_account: stprodappdata
subscription: production
resource_group: rg-data-prod

containers:
exports:
  intended_action: delete_after_45_days
  owners: product-operations
  restore_need: last_successful_export_for_replay
attachments:
  intended_action: no_delete_without_business_owner
  owners: application-team
  restore_need: user_visible_content
diagnostics:
  intended_action: move_to_cool_after_30_days_delete_after_180_days
  owners: platform-operations
  restore_need: incident_evidence

policy_change:
decision_needed: enable_limit_or_postpone
rollback_owner: platform-operations
validation_window: 48h
excluded_prefixes:
  - attachments/
  - exports/manual-replay/

Without this contract, the policy will be treated as storage optimization while it actually changes restore capability, evidence retention and sometimes compliance posture.

Read the existing policy and history

Before adding a rule, capture the current state. Confirm whether a lifecycle policy already exists, who changed it recently, and whether the account uses soft delete, versioning, snapshots or immutability.

bash 01-read-storage-lifecycle-state.sh
RG="rg-data-prod"
ACCOUNT="stprodappdata"

az storage account management-policy show --resource-group "$RG" --account-name "$ACCOUNT" --output json

az storage account blob-service-properties show --resource-group "$RG" --account-name "$ACCOUNT" --query "{deleteRetention:deleteRetentionPolicy,containerDeleteRetention:containerDeleteRetentionPolicy,versioning:isVersioningEnabled,changeFeed:changeFeed.enabled,restorePolicy:restorePolicy}" --output json

az monitor activity-log list --resource-group "$RG" --offset 14d --query "[?contains(operationName.value, 'managementPolicies') || contains(operationName.value, 'blobServices')].{time:eventTimestamp,operation:operationName.value,caller:caller,status:status.value}" --output table

This avoids two common mistakes: enabling deletion without a suitable recovery window, or changing a policy another team already relies on.

Rebuild impact before activation

Azure Storage Lifecycle Management should not be treated as a button with a magic dry run. Before activation, build an inventory of blobs that would match the rule criteria. The target output is a reviewable list of paths, ages, sizes and likely owners.

bash 02-inventory-candidate-blobs.sh
ACCOUNT="stprodappdata"
CONTAINER="exports"
CUTOFF="2026-06-10T00:00:00Z"

az storage blob list --account-name "$ACCOUNT" --container-name "$CONTAINER" --auth-mode login --include m t v --query "[?properties.lastModified < '$CUTOFF'].{name:name,lastModified:properties.lastModified,accessTier:properties.blobTier,size:properties.contentLength,versionId:versionId,tags:tags}" --output json

For large accounts, use Blob Inventory or a controlled export instead of a fragile CLI loop. The operating point is the same: produce a verifiable sample and a prefix-level estimate before writing the rule.

Classify deletion, tiering and restore risk

Not all actions carry the same risk. Moving to Cool is not the same as archiving, deleting a version or deleting a snapshot.

text lifecycle-action-risk.txt
Move to Cool
Main risk: different read cost and latency
Validation: does the application read these objects during normal operation?

Move to Archive
Main risk: slow rehydration and broken application path if immediate reads are expected
Validation: no production job depends on direct reads

Delete base blob
Main risk: object disappears from the application view
Validation: business owner, retention, soft delete and proof of non-use

Delete version or snapshot
Main risk: rollback or incident evidence path is lost
Validation: useful versions or snapshots, restore test, accepted retention window

Filter by prefix only
Main risk: shared prefix or unstable naming convention
Validation: prefix inventory and explicit exclusions

This classification helps keep the first policy small. A good first change may tier a well-understood diagnostics prefix, not delete everything older than a global age.

Check real access before deciding

An old blob can still be useful. Before deletion, look for recent reads, application errors and batch dependencies. Storage logs do not replace business ownership, but they prevent the team from deciding only from last modified time.

kusto 03-storage-access-before-delete.kql
let Window = 30d;
StorageBlobLogs
| where TimeGenerated > ago(Window)
| where AccountName == "stprodappdata"
| where ContainerName in ("exports", "diagnostics")
| summarize reads=countif(OperationName in ("GetBlob", "GetBlobProperties")),
          writes=countif(OperationName in ("PutBlob", "PutBlockList")),
          deletes=countif(OperationName has "Delete"),
          failures=countif(StatusCode >= 400),
          callers=make_set(AuthenticationType, 5),
          sampleUris=make_set(Uri, 5)
by ContainerName, bin(TimeGenerated, 1d)
| order by TimeGenerated desc

If logs are not available, that is part of the decision. The safer path may be enabling diagnostics first, narrowing the scope, or requiring owner sign-off before any deletion rule.

Write a bounded policy

The policy should be readable. Name rules, limit prefixes, separate deletion from tiering, and avoid mixing business objects with technical artifacts.

json 04-bounded-lifecycle-policy.json
{
"rules": [
  {
    "enabled": true,
    "name": "diagnostics-cool-after-30-days",
    "type": "Lifecycle",
    "definition": {
      "filters": {
        "blobTypes": ["blockBlob"],
        "prefixMatch": ["diagnostics/platform/"]
      },
      "actions": {
        "baseBlob": {
          "tierToCool": { "daysAfterModificationGreaterThan": 30 },
          "delete": { "daysAfterModificationGreaterThan": 180 }
        },
        "snapshot": {
          "delete": { "daysAfterCreationGreaterThan": 90 }
        },
        "version": {
          "delete": { "daysAfterCreationGreaterThan": 90 }
        }
      }
    }
  }
]
}

Do not start with a high-level prefix such as exports/ if that directory contains replayable exports, user files and temporary artifacts. Create stable operational prefixes before automating deletion.

Enable with a short validation window

After applying the policy, validation should prove three things: the policy is in place, expected operations appear, and excluded paths are untouched.

bash 05-apply-policy-and-read-back.sh
RG="rg-data-prod"
ACCOUNT="stprodappdata"

az storage account management-policy create --resource-group "$RG" --account-name "$ACCOUNT" --policy @04-bounded-lifecycle-policy.json

az storage account management-policy show --resource-group "$RG" --account-name "$ACCOUNT" --query "policy.rules[].{name:name,enabled:enabled,filters:definition.filters,actions:definition.actions}" --output json

Add monitoring for deletes and tier changes.

kusto 06-watch-lifecycle-effects.kql
let Window = 48h;
StorageBlobLogs
| where TimeGenerated > ago(Window)
| where AccountName == "stprodappdata"
| where OperationName has_any ("Delete", "SetBlobTier")
| summarize operations=count(), statuses=make_set(StatusText, 10), sampleUris=make_set(Uri, 10)
by OperationName, ContainerName, bin(TimeGenerated, 1h)
| order by TimeGenerated desc

If deletion appears in an unexpected container or prefix, disable the rule immediately and preserve logs before editing the policy again.

Prepare rollback

Rolling back a lifecycle policy does not automatically restore objects. It stops future actions. Recovery then depends on soft delete, versions, snapshots, backup or rehydration from Archive.

yaml lifecycle-rollback-card.yml
rollback:
stop_policy:
  action: disable_or_remove_rule
  validation: management_policy_read_back_without_rule

restore_deleted_blob:
  prerequisites:
    - soft_delete_enabled
    - retention_window_not_expired
    - exact_container_and_blob_name_known
  validation: blob_readable_from_application_path

restore_version_or_snapshot:
  prerequisites:
    - version_id_or_snapshot_timestamp_known
    - owner_approval_for_restore
  validation: restored_object_hash_or_size_matches_evidence

rehydrate_archive:
  prerequisites:
    - archive_tier_was_action_taken
    - business_accepts_rehydration_delay
  validation: object_available_before_batch_resume

evidence_to_keep:
  - policy_before_after
  - impacted_prefix_inventory
  - StorageBlobLogs_extract
  - business_owner_decision

The rollback decision must therefore be quick. The shorter the retention window, the stricter the initial validation should be.

Make an explicit decision

The final decision should fit on an operations card.

text lifecycle-policy-decision.txt
Enable
Candidate inventory reviewed by owner
Bounded prefixes and explicit exclusions
Soft delete, versioning or another restore path matches the risk
Logs available to watch Delete and SetBlobTier
Rollback tested or documented

Narrow
Prefix is too broad
Batch reads or replayable exports remain uncertain
Versions or snapshots are still useful
Restore path has not been tested

Postpone
No clear owner
Insufficient logs
Business or compliance retention not confirmed
Existing policy is not understood

Roll back
Deletion or tiering outside the approved scope
Application errors correlated with touched objects
Useful object recoverable only during a short window

Conclusion

An Azure Storage lifecycle policy is not only a cost rule. It is a production action on data availability, evidence and recovery. The runbook should therefore start with inventory, not JSON syntax.

The right decision is to bound prefixes, separate tiering from deletion, verify real access, enable with a short watch window, then keep a rollback path that stops the policy and restores impacted objects while recovery is still possible. Reliable automatic deletion is deletion proven before it is automated.