[{"data":1,"prerenderedAt":1971},["ShallowReactive",2],{"post:\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity\u002F":3},{"post":4,"newer":1917,"older":1928,"related":1938,"series":1969},{"id":5,"title":6,"body":7,"canonical":1899,"categories":1900,"date":1903,"description":1904,"extension":1905,"featured":1906,"hero":1907,"image":1907,"meta":1908,"navigation":187,"path":1909,"readingTime":222,"seo":1910,"series":1907,"seriesOrder":1907,"sites":1911,"source":1907,"stem":1912,"tags":1913,"updated":1907,"url":1915,"__hash__":1916},"blog\u002Fblog\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity.md","Azure: Cost Governance Without Killing Developer Velocity",{"type":8,"value":9,"toc":1892},"minimark",[10,14,19,22,138,141,144,541,544,547,636,643,647,650,653,656,660,663,666,983,1081,1095,1099,1102,1109,1212,1223,1803,1806,1809,1813,1888],[11,12,13],"p",{},"The first instinct after a surprise Azure bill is almost always to add an approval gate: someone has to sign off before a new resource gets created, or before a resource above some size gets provisioned. It works, in the narrow sense that it stops the specific mistake that triggered it, and it fails in the broader sense that every developer who wasn't the cause of that mistake now waits on a human for something that used to be self-service. I've watched this pattern play out enough times to trust a different order of operations: guardrails before gates, visibility before restriction, and automation before either. Below is each piece with the code I actually deploy.",[15,16,18],"h2",{"id":17},"guardrails-instead-of-gates","Guardrails instead of gates",[11,20,21],{},"A gate asks permission before the fact; a guardrail makes the expensive mistake structurally hard to make in the first place. Azure Policy is the right tool for guardrails, and the built-in definitions cover the cost-relevant ones without writing custom JSON:",[23,24,25,44],"table",{},[26,27,28],"thead",{},[29,30,31,35,38,41],"tr",{},[32,33,34],"th",{},"Built-in policy",[32,36,37],{},"Definition ID",[32,39,40],{},"Parameter",[32,42,43],{},"Use",[45,46,47,67,85,103,121],"tbody",{},[29,48,49,53,59,64],{},[50,51,52],"td",{},"Allowed virtual machine size SKUs",[50,54,55],{},[56,57,58],"code",{},"cccc23c7-8427-4f53-ad12-b6a63eb452b3",[50,60,61],{},[56,62,63],{},"listOfAllowedSKUs",[50,65,66],{},"Cap VM sizes in non-production",[29,68,69,72,77,82],{},[50,70,71],{},"Allowed locations",[50,73,74],{},[56,75,76],{},"e56962a6-4747-49cd-b67b-bf8b01975c4c",[50,78,79],{},[56,80,81],{},"listOfAllowedLocations",[50,83,84],{},"No accidental deployments to pricier or unapproved regions",[29,86,87,90,95,100],{},[50,88,89],{},"Not allowed resource types",[50,91,92],{},[56,93,94],{},"6c112d4e-5bc7-47ae-a041-ea2d9dccd749",[50,96,97],{},[56,98,99],{},"listOfResourceTypesNotAllowed",[50,101,102],{},"Block specific expensive services in sandboxes",[29,104,105,108,113,118],{},[50,106,107],{},"Require a tag on resource groups",[50,109,110],{},[56,111,112],{},"96670d01-0a4d-4649-9c89-2d3abc0a5025",[50,114,115],{},[56,116,117],{},"tagName",[50,119,120],{},"No resource group without a cost owner",[29,122,123,126,131,135],{},[50,124,125],{},"Inherit a tag from the resource group if missing",[50,127,128],{},[56,129,130],{},"ea3f2387-9b95-492a-a190-fcdc54f7b070",[50,132,133],{},[56,134,117],{},[50,136,137],{},"Copy the cost tag down to every resource",[11,139,140],{},"None of these asks a human to approve anything. They make the unintentional expensive path unavailable, while leaving the deliberate one open through an explicit exception process. The distinction that matters is that a guardrail fails fast, at deployment time, with a clear reason, where a gate fails slow, days later, with a queue. Azure Policy supports the \"clear reason\" part directly: an assignment's non-compliance message is shown in the deny error the developer sees, so it can say what to do instead.",[11,142,143],{},"This script assigns the non-production guardrails to a management group that holds dev\u002Ftest subscriptions. The allowed-SKU list is an example; pick sizes from your own usage.",[145,146,151],"pre",{"className":147,"code":148,"language":149,"meta":150,"style":150},"language-bash shiki shiki-themes github-dark","#!\u002Fusr\u002Fbin\u002Fenv bash\n# assign-nonprod-guardrails.sh: cost guardrails on a non-production management group.\nset -euo pipefail\n\nMG=\"\u003Cnonprod-management-group-id>\"\nSCOPE=\"\u002Fproviders\u002FMicrosoft.Management\u002FmanagementGroups\u002F$MG\"\nLOCATION=\"eastus\"   # required for the modify assignment's managed identity\n\n# Deny VM sizes outside an approved list.\naz policy assignment create \\\n  --name \"nonprod-vm-skus\" \\\n  --display-name \"Non-prod: allowed VM sizes\" \\\n  --scope \"$SCOPE\" \\\n  --policy \"cccc23c7-8427-4f53-ad12-b6a63eb452b3\" \\\n  --params '{ \"listOfAllowedSKUs\": { \"value\": [ \"Standard_B2s\", \"Standard_B2ms\", \"Standard_D2s_v5\", \"Standard_D4s_v5\" ] } }' \\\n  --non-compliance-messages '[{ \"message\": \"Non-production VMs are limited to B-series and small D-series. Ask the platform team for an exemption if you need more.\" }]'\n\n# Every resource group needs a costCenter tag.\naz policy assignment create \\\n  --name \"nonprod-rg-costcenter\" \\\n  --display-name \"Non-prod: require costCenter on resource groups\" \\\n  --scope \"$SCOPE\" \\\n  --policy \"96670d01-0a4d-4649-9c89-2d3abc0a5025\" \\\n  --params '{ \"tagName\": { \"value\": \"costCenter\" } }' \\\n  --non-compliance-messages '[{ \"message\": \"Add a costCenter tag to the resource group.\" }]'\n\n# Resources inherit costCenter from their resource group (modify effect, needs an identity with Contributor).\naz policy assignment create \\\n  --name \"nonprod-inherit-cc\" \\\n  --display-name \"Non-prod: inherit costCenter from resource group\" \\\n  --scope \"$SCOPE\" \\\n  --policy \"ea3f2387-9b95-492a-a190-fcdc54f7b070\" \\\n  --params '{ \"tagName\": { \"value\": \"costCenter\" } }' \\\n  --mi-system-assigned \\\n  --identity-scope \"$SCOPE\" \\\n  --role Contributor \\\n  --location \"$LOCATION\"\n","bash","",[56,152,153,162,168,182,189,203,220,234,239,245,264,275,286,303,314,325,334,339,345,358,368,378,391,401,411,419,424,430,443,453,463,476,486,495,503,517,528],{"__ignoreMap":150},[154,155,158],"span",{"class":156,"line":157},"line",1,[154,159,161],{"class":160},"sAwPA","#!\u002Fusr\u002Fbin\u002Fenv bash\n",[154,163,165],{"class":156,"line":164},2,[154,166,167],{"class":160},"# assign-nonprod-guardrails.sh: cost guardrails on a non-production management group.\n",[154,169,171,175,178],{"class":156,"line":170},3,[154,172,174],{"class":173},"sDLfK","set",[154,176,177],{"class":173}," -euo",[154,179,181],{"class":180},"sU2Wk"," pipefail\n",[154,183,185],{"class":156,"line":184},4,[154,186,188],{"emptyLinePlaceholder":187},true,"\n",[154,190,192,196,200],{"class":156,"line":191},5,[154,193,195],{"class":194},"s95oV","MG",[154,197,199],{"class":198},"snl16","=",[154,201,202],{"class":180},"\"\u003Cnonprod-management-group-id>\"\n",[154,204,206,209,211,214,217],{"class":156,"line":205},6,[154,207,208],{"class":194},"SCOPE",[154,210,199],{"class":198},[154,212,213],{"class":180},"\"\u002Fproviders\u002FMicrosoft.Management\u002FmanagementGroups\u002F",[154,215,216],{"class":194},"$MG",[154,218,219],{"class":180},"\"\n",[154,221,223,226,228,231],{"class":156,"line":222},7,[154,224,225],{"class":194},"LOCATION",[154,227,199],{"class":198},[154,229,230],{"class":180},"\"eastus\"",[154,232,233],{"class":160},"   # required for the modify assignment's managed identity\n",[154,235,237],{"class":156,"line":236},8,[154,238,188],{"emptyLinePlaceholder":187},[154,240,242],{"class":156,"line":241},9,[154,243,244],{"class":160},"# Deny VM sizes outside an approved list.\n",[154,246,248,252,255,258,261],{"class":156,"line":247},10,[154,249,251],{"class":250},"svObZ","az",[154,253,254],{"class":180}," policy",[154,256,257],{"class":180}," assignment",[154,259,260],{"class":180}," create",[154,262,263],{"class":173}," \\\n",[154,265,267,270,273],{"class":156,"line":266},11,[154,268,269],{"class":173},"  --name",[154,271,272],{"class":180}," \"nonprod-vm-skus\"",[154,274,263],{"class":173},[154,276,278,281,284],{"class":156,"line":277},12,[154,279,280],{"class":173},"  --display-name",[154,282,283],{"class":180}," \"Non-prod: allowed VM sizes\"",[154,285,263],{"class":173},[154,287,289,292,295,298,301],{"class":156,"line":288},13,[154,290,291],{"class":173},"  --scope",[154,293,294],{"class":180}," \"",[154,296,297],{"class":194},"$SCOPE",[154,299,300],{"class":180},"\"",[154,302,263],{"class":173},[154,304,306,309,312],{"class":156,"line":305},14,[154,307,308],{"class":173},"  --policy",[154,310,311],{"class":180}," \"cccc23c7-8427-4f53-ad12-b6a63eb452b3\"",[154,313,263],{"class":173},[154,315,317,320,323],{"class":156,"line":316},15,[154,318,319],{"class":173},"  --params",[154,321,322],{"class":180}," '{ \"listOfAllowedSKUs\": { \"value\": [ \"Standard_B2s\", \"Standard_B2ms\", \"Standard_D2s_v5\", \"Standard_D4s_v5\" ] } }'",[154,324,263],{"class":173},[154,326,328,331],{"class":156,"line":327},16,[154,329,330],{"class":173},"  --non-compliance-messages",[154,332,333],{"class":180}," '[{ \"message\": \"Non-production VMs are limited to B-series and small D-series. Ask the platform team for an exemption if you need more.\" }]'\n",[154,335,337],{"class":156,"line":336},17,[154,338,188],{"emptyLinePlaceholder":187},[154,340,342],{"class":156,"line":341},18,[154,343,344],{"class":160},"# Every resource group needs a costCenter tag.\n",[154,346,348,350,352,354,356],{"class":156,"line":347},19,[154,349,251],{"class":250},[154,351,254],{"class":180},[154,353,257],{"class":180},[154,355,260],{"class":180},[154,357,263],{"class":173},[154,359,361,363,366],{"class":156,"line":360},20,[154,362,269],{"class":173},[154,364,365],{"class":180}," \"nonprod-rg-costcenter\"",[154,367,263],{"class":173},[154,369,371,373,376],{"class":156,"line":370},21,[154,372,280],{"class":173},[154,374,375],{"class":180}," \"Non-prod: require costCenter on resource groups\"",[154,377,263],{"class":173},[154,379,381,383,385,387,389],{"class":156,"line":380},22,[154,382,291],{"class":173},[154,384,294],{"class":180},[154,386,297],{"class":194},[154,388,300],{"class":180},[154,390,263],{"class":173},[154,392,394,396,399],{"class":156,"line":393},23,[154,395,308],{"class":173},[154,397,398],{"class":180}," \"96670d01-0a4d-4649-9c89-2d3abc0a5025\"",[154,400,263],{"class":173},[154,402,404,406,409],{"class":156,"line":403},24,[154,405,319],{"class":173},[154,407,408],{"class":180}," '{ \"tagName\": { \"value\": \"costCenter\" } }'",[154,410,263],{"class":173},[154,412,414,416],{"class":156,"line":413},25,[154,415,330],{"class":173},[154,417,418],{"class":180}," '[{ \"message\": \"Add a costCenter tag to the resource group.\" }]'\n",[154,420,422],{"class":156,"line":421},26,[154,423,188],{"emptyLinePlaceholder":187},[154,425,427],{"class":156,"line":426},27,[154,428,429],{"class":160},"# Resources inherit costCenter from their resource group (modify effect, needs an identity with Contributor).\n",[154,431,433,435,437,439,441],{"class":156,"line":432},28,[154,434,251],{"class":250},[154,436,254],{"class":180},[154,438,257],{"class":180},[154,440,260],{"class":180},[154,442,263],{"class":173},[154,444,446,448,451],{"class":156,"line":445},29,[154,447,269],{"class":173},[154,449,450],{"class":180}," \"nonprod-inherit-cc\"",[154,452,263],{"class":173},[154,454,456,458,461],{"class":156,"line":455},30,[154,457,280],{"class":173},[154,459,460],{"class":180}," \"Non-prod: inherit costCenter from resource group\"",[154,462,263],{"class":173},[154,464,466,468,470,472,474],{"class":156,"line":465},31,[154,467,291],{"class":173},[154,469,294],{"class":180},[154,471,297],{"class":194},[154,473,300],{"class":180},[154,475,263],{"class":173},[154,477,479,481,484],{"class":156,"line":478},32,[154,480,308],{"class":173},[154,482,483],{"class":180}," \"ea3f2387-9b95-492a-a190-fcdc54f7b070\"",[154,485,263],{"class":173},[154,487,489,491,493],{"class":156,"line":488},33,[154,490,319],{"class":173},[154,492,408],{"class":180},[154,494,263],{"class":173},[154,496,498,501],{"class":156,"line":497},34,[154,499,500],{"class":173},"  --mi-system-assigned",[154,502,263],{"class":173},[154,504,506,509,511,513,515],{"class":156,"line":505},35,[154,507,508],{"class":173},"  --identity-scope",[154,510,294],{"class":180},[154,512,297],{"class":194},[154,514,300],{"class":180},[154,516,263],{"class":173},[154,518,520,523,526],{"class":156,"line":519},36,[154,521,522],{"class":173},"  --role",[154,524,525],{"class":180}," Contributor",[154,527,263],{"class":173},[154,529,531,534,536,539],{"class":156,"line":530},37,[154,532,533],{"class":173},"  --location",[154,535,294],{"class":180},[154,537,538],{"class":194},"$LOCATION",[154,540,219],{"class":180},[11,542,543],{},"Assignment names at management group scope are limited to 24 characters, which is why they're terse. The modify assignment only tags resources as they're created or updated; existing ones show as non-compliant until you run a remediation task.",[11,545,546],{},"The exception process is a policy exemption with an expiry date, so it can't quietly become permanent:",[145,548,550],{"className":147,"code":549,"language":149,"meta":150,"style":150},"# Waive the VM size guardrail for one resource group until the end of the quarter.\naz policy exemption create \\\n  --name \"loadtest-large-vms\" \\\n  --display-name \"Load test: large VMs until 2026-09-30\" \\\n  --policy-assignment \"\u002Fproviders\u002FMicrosoft.Management\u002FmanagementGroups\u002F\u003Cnonprod-management-group-id>\u002Fproviders\u002FMicrosoft.Authorization\u002FpolicyAssignments\u002Fnonprod-vm-skus\" \\\n  --exemption-category Waiver \\\n  --expires-on \"2026-09-30T23:59:59Z\" \\\n  --resource-group \"\u003Cresource-group>\" \\\n  --description \"Approved in \u003Cticket-id>\"\n",[56,551,552,557,570,579,588,598,608,618,628],{"__ignoreMap":150},[154,553,554],{"class":156,"line":157},[154,555,556],{"class":160},"# Waive the VM size guardrail for one resource group until the end of the quarter.\n",[154,558,559,561,563,566,568],{"class":156,"line":164},[154,560,251],{"class":250},[154,562,254],{"class":180},[154,564,565],{"class":180}," exemption",[154,567,260],{"class":180},[154,569,263],{"class":173},[154,571,572,574,577],{"class":156,"line":170},[154,573,269],{"class":173},[154,575,576],{"class":180}," \"loadtest-large-vms\"",[154,578,263],{"class":173},[154,580,581,583,586],{"class":156,"line":184},[154,582,280],{"class":173},[154,584,585],{"class":180}," \"Load test: large VMs until 2026-09-30\"",[154,587,263],{"class":173},[154,589,590,593,596],{"class":156,"line":191},[154,591,592],{"class":173},"  --policy-assignment",[154,594,595],{"class":180}," \"\u002Fproviders\u002FMicrosoft.Management\u002FmanagementGroups\u002F\u003Cnonprod-management-group-id>\u002Fproviders\u002FMicrosoft.Authorization\u002FpolicyAssignments\u002Fnonprod-vm-skus\"",[154,597,263],{"class":173},[154,599,600,603,606],{"class":156,"line":205},[154,601,602],{"class":173},"  --exemption-category",[154,604,605],{"class":180}," Waiver",[154,607,263],{"class":173},[154,609,610,613,616],{"class":156,"line":222},[154,611,612],{"class":173},"  --expires-on",[154,614,615],{"class":180}," \"2026-09-30T23:59:59Z\"",[154,617,263],{"class":173},[154,619,620,623,626],{"class":156,"line":236},[154,621,622],{"class":173},"  --resource-group",[154,624,625],{"class":180}," \"\u003Cresource-group>\"",[154,627,263],{"class":173},[154,629,630,633],{"class":156,"line":241},[154,631,632],{"class":173},"  --description",[154,634,635],{"class":180}," \"Approved in \u003Cticket-id>\"\n",[11,637,638,639,642],{},"A tag-based guardrail also pays off in reporting. If you're on an EA, MCA or MPA with an Azure plan, Cost Management's tag inheritance setting applies subscription and resource group tags to child resource usage records (not to the resources themselves), within 24 hours for the current month. Between that and the modify policy, the ",[56,640,641],{},"costCenter"," split in cost analysis stops having a big \"untagged\" bucket.",[15,644,646],{"id":645},"where-a-gate-is-still-worth-the-friction","Where a gate is still worth the friction",[11,648,649],{},"I'm not against approval workflows entirely. They're the right tool for the small number of decisions that are genuinely expensive to reverse: reservation and savings plan purchases, anything else with a multi-year commitment, and cross-region data transfer architecture decisions. Undoing those costs real money or real rework. The mistake is applying that same friction to routine, reversible actions like spinning up a dev VM or a test database, where the cost of a wrong decision is measured in dollars per day and the cost of the gate is measured in developer-hours per week across the whole team.",[11,651,652],{},"Commitments deserve the gate because they're easy to get wrong in both directions. Microsoft's own guidance frames the choice: a reservation commits to a specific instance type or family in a specific region and gives the greatest savings when fully used; a savings plan commits to an hourly spend across eligible compute services in any region and suits workloads that change. The recommended order is to right-size first (\"Discounts reduce rates, not waste\"), exchange or trade in underused reservations, and only then buy new reservations for stable workloads and savings plans for the flexible remainder. That sequence is a checklist a reviewer can hold a purchase request against.",[11,654,655],{},"The other structural lever is the subscription offer itself. Subscription vending in the Cloud Adoption Framework asks at request time whether a workload is production or DevTest, because the DevTest offer has lower resource charges under its own terms (and isn't available under an MPA). Getting non-production onto the right offer is a one-time decision at vending, not an ongoing approval.",[15,657,659],{"id":658},"visibility-changes-behavior-faster-than-restriction-does","Visibility changes behavior faster than restriction does",[11,661,662],{},"The governance move with the best ratio of effort to result, by a wide margin, has been making cost visible to the people actually creating resources, not just to whoever reads the monthly report. A budget alert that reaches a finance inbox a week after the spend happened changes nothing about next month's behavior. A per-subscription or per-tag cost view that a team can see themselves changes behavior almost immediately; teams that can see their own trend line stop leaving test environments running over the weekend without anyone telling them to.",[11,664,665],{},"Budgets are the cheapest way to push that signal to the team instead of to finance. Know their limits: budgets alert, they don't stop anything (\"Resources aren't affected, and your consumption isn't stopped\"). Cost data typically lands within 8 to 24 hours and budgets are evaluated every 24 hours, so an alert is a next-day signal, not a real-time one. Forecasted thresholds help with that lag by warning when the month's projection crosses a line. At subscription and resource group scope a budget can also call an action group, which is how you route it to a team channel. This is the budget I deploy into every vended subscription, based on Microsoft's Bicep quickstart with a forecast threshold and role-based recipients added:",[145,667,671],{"className":668,"code":669,"language":670,"meta":150,"style":150},"language-bicep shiki shiki-themes github-dark","\u002F\u002F budget.bicep: monthly subscription budget with actual and forecast alerts.\ntargetScope = 'subscription'\n\nparam budgetName string = 'monthly-subscription-budget'\nparam amount int\n\n@description('First day of the current month, YYYY-MM-DD.')\nparam startDate string\n\nparam contactEmails array = []\n\n@description('Optional action group resource IDs, for example one that posts to the team channel.')\nparam actionGroupIds array = []\n\nresource budget 'Microsoft.Consumption\u002Fbudgets@2023-11-01' = {\n  name: budgetName\n  properties: {\n    category: 'Cost'\n    amount: amount\n    timeGrain: 'Monthly'\n    timePeriod: {\n      startDate: startDate\n    }\n    notifications: {\n      forecast100: {\n        enabled: true\n        operator: 'GreaterThan'\n        threshold: 100\n        thresholdType: 'Forecasted'\n        contactEmails: contactEmails\n        contactRoles: [\n          'Owner'\n          'Contributor'\n        ]\n        contactGroups: actionGroupIds\n      }\n      actual80: {\n        enabled: true\n        operator: 'GreaterThan'\n        threshold: 80\n        thresholdType: 'Actual'\n        contactEmails: contactEmails\n        contactRoles: [\n          'Owner'\n        ]\n        contactGroups: actionGroupIds\n      }\n      actual100: {\n        enabled: true\n        operator: 'GreaterThan'\n        threshold: 100\n        thresholdType: 'Actual'\n        contactEmails: contactEmails\n        contactRoles: [\n          'Owner'\n          'Contributor'\n        ]\n        contactGroups: actionGroupIds\n      }\n    }\n  }\n}\n","bicep",[56,672,673,678,683,687,692,697,701,706,711,715,720,724,729,734,738,743,748,753,758,763,768,773,778,783,788,793,798,803,808,813,818,823,828,833,838,843,848,853,858,863,869,875,880,885,890,895,900,905,911,916,921,926,931,936,941,946,951,956,961,966,971,977],{"__ignoreMap":150},[154,674,675],{"class":156,"line":157},[154,676,677],{},"\u002F\u002F budget.bicep: monthly subscription budget with actual and forecast alerts.\n",[154,679,680],{"class":156,"line":164},[154,681,682],{},"targetScope = 'subscription'\n",[154,684,685],{"class":156,"line":170},[154,686,188],{"emptyLinePlaceholder":187},[154,688,689],{"class":156,"line":184},[154,690,691],{},"param budgetName string = 'monthly-subscription-budget'\n",[154,693,694],{"class":156,"line":191},[154,695,696],{},"param amount int\n",[154,698,699],{"class":156,"line":205},[154,700,188],{"emptyLinePlaceholder":187},[154,702,703],{"class":156,"line":222},[154,704,705],{},"@description('First day of the current month, YYYY-MM-DD.')\n",[154,707,708],{"class":156,"line":236},[154,709,710],{},"param startDate string\n",[154,712,713],{"class":156,"line":241},[154,714,188],{"emptyLinePlaceholder":187},[154,716,717],{"class":156,"line":247},[154,718,719],{},"param contactEmails array = []\n",[154,721,722],{"class":156,"line":266},[154,723,188],{"emptyLinePlaceholder":187},[154,725,726],{"class":156,"line":277},[154,727,728],{},"@description('Optional action group resource IDs, for example one that posts to the team channel.')\n",[154,730,731],{"class":156,"line":288},[154,732,733],{},"param actionGroupIds array = []\n",[154,735,736],{"class":156,"line":305},[154,737,188],{"emptyLinePlaceholder":187},[154,739,740],{"class":156,"line":316},[154,741,742],{},"resource budget 'Microsoft.Consumption\u002Fbudgets@2023-11-01' = {\n",[154,744,745],{"class":156,"line":327},[154,746,747],{},"  name: budgetName\n",[154,749,750],{"class":156,"line":336},[154,751,752],{},"  properties: {\n",[154,754,755],{"class":156,"line":341},[154,756,757],{},"    category: 'Cost'\n",[154,759,760],{"class":156,"line":347},[154,761,762],{},"    amount: amount\n",[154,764,765],{"class":156,"line":360},[154,766,767],{},"    timeGrain: 'Monthly'\n",[154,769,770],{"class":156,"line":370},[154,771,772],{},"    timePeriod: {\n",[154,774,775],{"class":156,"line":380},[154,776,777],{},"      startDate: startDate\n",[154,779,780],{"class":156,"line":393},[154,781,782],{},"    }\n",[154,784,785],{"class":156,"line":403},[154,786,787],{},"    notifications: {\n",[154,789,790],{"class":156,"line":413},[154,791,792],{},"      forecast100: {\n",[154,794,795],{"class":156,"line":421},[154,796,797],{},"        enabled: true\n",[154,799,800],{"class":156,"line":426},[154,801,802],{},"        operator: 'GreaterThan'\n",[154,804,805],{"class":156,"line":432},[154,806,807],{},"        threshold: 100\n",[154,809,810],{"class":156,"line":445},[154,811,812],{},"        thresholdType: 'Forecasted'\n",[154,814,815],{"class":156,"line":455},[154,816,817],{},"        contactEmails: contactEmails\n",[154,819,820],{"class":156,"line":465},[154,821,822],{},"        contactRoles: [\n",[154,824,825],{"class":156,"line":478},[154,826,827],{},"          'Owner'\n",[154,829,830],{"class":156,"line":488},[154,831,832],{},"          'Contributor'\n",[154,834,835],{"class":156,"line":497},[154,836,837],{},"        ]\n",[154,839,840],{"class":156,"line":505},[154,841,842],{},"        contactGroups: actionGroupIds\n",[154,844,845],{"class":156,"line":519},[154,846,847],{},"      }\n",[154,849,850],{"class":156,"line":530},[154,851,852],{},"      actual80: {\n",[154,854,856],{"class":156,"line":855},38,[154,857,797],{},[154,859,861],{"class":156,"line":860},39,[154,862,802],{},[154,864,866],{"class":156,"line":865},40,[154,867,868],{},"        threshold: 80\n",[154,870,872],{"class":156,"line":871},41,[154,873,874],{},"        thresholdType: 'Actual'\n",[154,876,878],{"class":156,"line":877},42,[154,879,817],{},[154,881,883],{"class":156,"line":882},43,[154,884,822],{},[154,886,888],{"class":156,"line":887},44,[154,889,827],{},[154,891,893],{"class":156,"line":892},45,[154,894,837],{},[154,896,898],{"class":156,"line":897},46,[154,899,842],{},[154,901,903],{"class":156,"line":902},47,[154,904,847],{},[154,906,908],{"class":156,"line":907},48,[154,909,910],{},"      actual100: {\n",[154,912,914],{"class":156,"line":913},49,[154,915,797],{},[154,917,919],{"class":156,"line":918},50,[154,920,802],{},[154,922,924],{"class":156,"line":923},51,[154,925,807],{},[154,927,929],{"class":156,"line":928},52,[154,930,874],{},[154,932,934],{"class":156,"line":933},53,[154,935,817],{},[154,937,939],{"class":156,"line":938},54,[154,940,822],{},[154,942,944],{"class":156,"line":943},55,[154,945,827],{},[154,947,949],{"class":156,"line":948},56,[154,950,832],{},[154,952,954],{"class":156,"line":953},57,[154,955,837],{},[154,957,959],{"class":156,"line":958},58,[154,960,842],{},[154,962,964],{"class":156,"line":963},59,[154,965,847],{},[154,967,969],{"class":156,"line":968},60,[154,970,782],{},[154,972,974],{"class":156,"line":973},61,[154,975,976],{},"  }\n",[154,978,980],{"class":156,"line":979},62,[154,981,982],{},"}\n",[145,984,986],{"className":147,"code":985,"language":149,"meta":150,"style":150},"# Deploy into one subscription. The start date must be the first of a month.\naz account set --subscription \"\u003Csubscription-id>\"\naz deployment sub create \\\n  --name budget-deploy \\\n  --location eastus \\\n  --template-file budget.bicep \\\n  --parameters amount=1500 startDate=\"$(date -u +%Y-%m-01)\" \\\n               contactEmails='[\"\u003Cteam-dl>@example.com\"]'\n",[56,987,988,993,1009,1023,1032,1041,1051,1076],{"__ignoreMap":150},[154,989,990],{"class":156,"line":157},[154,991,992],{"class":160},"# Deploy into one subscription. The start date must be the first of a month.\n",[154,994,995,997,1000,1003,1006],{"class":156,"line":164},[154,996,251],{"class":250},[154,998,999],{"class":180}," account",[154,1001,1002],{"class":180}," set",[154,1004,1005],{"class":173}," --subscription",[154,1007,1008],{"class":180}," \"\u003Csubscription-id>\"\n",[154,1010,1011,1013,1016,1019,1021],{"class":156,"line":170},[154,1012,251],{"class":250},[154,1014,1015],{"class":180}," deployment",[154,1017,1018],{"class":180}," sub",[154,1020,260],{"class":180},[154,1022,263],{"class":173},[154,1024,1025,1027,1030],{"class":156,"line":184},[154,1026,269],{"class":173},[154,1028,1029],{"class":180}," budget-deploy",[154,1031,263],{"class":173},[154,1033,1034,1036,1039],{"class":156,"line":191},[154,1035,533],{"class":173},[154,1037,1038],{"class":180}," eastus",[154,1040,263],{"class":173},[154,1042,1043,1046,1049],{"class":156,"line":205},[154,1044,1045],{"class":173},"  --template-file",[154,1047,1048],{"class":180}," budget.bicep",[154,1050,263],{"class":173},[154,1052,1053,1056,1059,1062,1065,1068,1071,1074],{"class":156,"line":222},[154,1054,1055],{"class":173},"  --parameters",[154,1057,1058],{"class":180}," amount=",[154,1060,1061],{"class":173},"1500",[154,1063,1064],{"class":180}," startDate=\"$(",[154,1066,1067],{"class":250},"date",[154,1069,1070],{"class":173}," -u",[154,1072,1073],{"class":180}," +%Y-%m-01)\"",[154,1075,263],{"class":173},[154,1077,1078],{"class":156,"line":236},[154,1079,1080],{"class":180},"               contactEmails='[\"\u003Cteam-dl>@example.com\"]'\n",[11,1082,1083,1084,1089,1090,1094],{},"Routing roles like Owner and Contributor means the alert reaches whoever actually runs the subscription, which is the point. If you want budget alerts in Slack rather than email, the ",[1085,1086,1088],"a",{"href":1087},"\u002F2026\u002F02\u002F18\u002Fnode-js-azure-a-slack-bot-for-resource-group-budget-alerts\u002F","Node.js Slack bot for resource group budget alerts"," does that, and ",[1085,1091,1093],{"href":1092},"\u002F2025\u002F12\u002F24\u002Fpython-azure-automate-cost-anomaly-alerts-with-the-cost-management-api\u002F","Cost Anomaly Alerts from the Cost Management API"," covers the spikes a monthly budget is too coarse to catch.",[15,1096,1098],{"id":1097},"automating-the-boring-cleanup","Automating the boring cleanup",[11,1100,1101],{},"The remaining category, the one guardrails and dashboards don't fully solve, is idle resource decay: dev environments nobody remembered to tear down, orphaned disks left behind after a VM was deleted, public IPs reserved and never attached. That's not a governance problem so much as a garbage collection problem, and it belongs in automation, not policy.",[11,1103,1104,1105,1108],{},"For VMs, the built-in auto-shutdown schedule is enough for most non-production machines. The time is UTC in ",[56,1106,1107],{},"hhmm"," format:",[145,1110,1112],{"className":147,"code":1111,"language":149,"meta":150,"style":150},"# Put a 23:00 UTC auto-shutdown on every VM tagged environment=dev in the current subscription.\naz vm list --query \"[?tags.environment=='dev'].id\" --output tsv |\nwhile read -r vmId; do\n  az vm auto-shutdown --ids \"$vmId\" --time 2300 --output none\n  echo \"auto-shutdown set: $vmId\"\ndone\n",[56,1113,1114,1119,1144,1164,1195,1207],{"__ignoreMap":150},[154,1115,1116],{"class":156,"line":157},[154,1117,1118],{"class":160},"# Put a 23:00 UTC auto-shutdown on every VM tagged environment=dev in the current subscription.\n",[154,1120,1121,1123,1126,1129,1132,1135,1138,1141],{"class":156,"line":164},[154,1122,251],{"class":250},[154,1124,1125],{"class":180}," vm",[154,1127,1128],{"class":180}," list",[154,1130,1131],{"class":173}," --query",[154,1133,1134],{"class":180}," \"[?tags.environment=='dev'].id\"",[154,1136,1137],{"class":173}," --output",[154,1139,1140],{"class":180}," tsv",[154,1142,1143],{"class":198}," |\n",[154,1145,1146,1149,1152,1155,1158,1161],{"class":156,"line":170},[154,1147,1148],{"class":198},"while",[154,1150,1151],{"class":180}," read",[154,1153,1154],{"class":173}," -r",[154,1156,1157],{"class":180}," vmId",[154,1159,1160],{"class":194},"; ",[154,1162,1163],{"class":198},"do\n",[154,1165,1166,1169,1171,1174,1177,1179,1182,1184,1187,1190,1192],{"class":156,"line":184},[154,1167,1168],{"class":250},"  az",[154,1170,1125],{"class":180},[154,1172,1173],{"class":180}," auto-shutdown",[154,1175,1176],{"class":173}," --ids",[154,1178,294],{"class":180},[154,1180,1181],{"class":194},"$vmId",[154,1183,300],{"class":180},[154,1185,1186],{"class":173}," --time",[154,1188,1189],{"class":173}," 2300",[154,1191,1137],{"class":173},[154,1193,1194],{"class":180}," none\n",[154,1196,1197,1200,1203,1205],{"class":156,"line":191},[154,1198,1199],{"class":173},"  echo",[154,1201,1202],{"class":180}," \"auto-shutdown set: ",[154,1204,1181],{"class":194},[154,1206,219],{"class":180},[154,1208,1209],{"class":156,"line":205},[154,1210,1211],{"class":198},"done\n",[11,1213,1214,1215,1218,1219,1222],{},"For orphans, this weekly sweep flags unattached managed disks (",[56,1216,1217],{},"managedBy"," is null, the same test Microsoft's own cleanup script uses) and public IPs with no IP configuration or NAT gateway. The first run tags them with the date they were found; later runs report anything flagged longer than the grace period, and delete it only when you pass ",[56,1220,1221],{},"--delete",".",[145,1224,1226],{"className":147,"code":1225,"language":149,"meta":150,"style":150},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# orphan-sweep.sh [--delete]: flag, then after a grace period remove, orphaned disks and public IPs.\nset -euo pipefail\n\nGRACE_DAYS=14\nTAG=\"orphanFlaggedOn\"\nDELETE=false\n[[ \"${1:-}\" == \"--delete\" ]] && DELETE=true\nTODAY=$(date -u +%F)\nCUTOFF=$(date -u -d \"-${GRACE_DAYS} days\" +%F)\n\nsweep() {\n  local kind=\"$1\" query=\"$2\"\n  az $kind list --query \"$query\" --output tsv |\n  while IFS=$'\\t' read -r id flagged; do\n    if [[ -z \"$flagged\" || \"$flagged\" == \"None\" ]]; then\n      az resource tag --ids \"$id\" --tags \"$TAG=$TODAY\" --is-incremental --output none\n      echo \"flagged   $id\"\n    elif [[ \"$flagged\" \u003C \"$CUTOFF\" || \"$flagged\" == \"$CUTOFF\" ]]; then\n      if $DELETE; then\n        az resource delete --ids \"$id\" --output none\n        echo \"deleted   $id (flagged $flagged)\"\n      else\n        echo \"expired   $id (flagged $flagged, rerun with --delete)\"\n      fi\n    else\n      echo \"waiting   $id (flagged $flagged)\"\n    fi\n  done\n}\n\nfor sub in $(az account list --query \"[?state=='Enabled'].id\" --output tsv); do\n  az account set --subscription \"$sub\"\n  echo \"== subscription $sub\"\n  sweep \"disk\" \"[?managedBy==null].[id, tags.${TAG}]\"\n  sweep \"network public-ip\" \"[?ipConfiguration==null && natGateway==null].[id, tags.${TAG}]\"\ndone\n",[56,1227,1228,1232,1237,1245,1249,1259,1269,1279,1311,1331,1359,1363,1371,1400,1425,1458,1496,1538,1550,1593,1603,1625,1643,1648,1664,1669,1674,1689,1694,1699,1703,1707,1741,1758,1769,1785,1799],{"__ignoreMap":150},[154,1229,1230],{"class":156,"line":157},[154,1231,161],{"class":160},[154,1233,1234],{"class":156,"line":164},[154,1235,1236],{"class":160},"# orphan-sweep.sh [--delete]: flag, then after a grace period remove, orphaned disks and public IPs.\n",[154,1238,1239,1241,1243],{"class":156,"line":170},[154,1240,174],{"class":173},[154,1242,177],{"class":173},[154,1244,181],{"class":180},[154,1246,1247],{"class":156,"line":184},[154,1248,188],{"emptyLinePlaceholder":187},[154,1250,1251,1254,1256],{"class":156,"line":191},[154,1252,1253],{"class":194},"GRACE_DAYS",[154,1255,199],{"class":198},[154,1257,1258],{"class":180},"14\n",[154,1260,1261,1264,1266],{"class":156,"line":205},[154,1262,1263],{"class":194},"TAG",[154,1265,199],{"class":198},[154,1267,1268],{"class":180},"\"orphanFlaggedOn\"\n",[154,1270,1271,1274,1276],{"class":156,"line":222},[154,1272,1273],{"class":194},"DELETE",[154,1275,199],{"class":198},[154,1277,1278],{"class":180},"false\n",[154,1280,1281,1284,1286,1289,1292,1295,1297,1300,1303,1306,1308],{"class":156,"line":236},[154,1282,1283],{"class":194},"[[ ",[154,1285,300],{"class":180},[154,1287,1288],{"class":173},"${1",[154,1290,1291],{"class":198},":-",[154,1293,1294],{"class":173},"}",[154,1296,300],{"class":180},[154,1298,1299],{"class":198}," ==",[154,1301,1302],{"class":180}," \"--delete\"",[154,1304,1305],{"class":194}," ]] && DELETE",[154,1307,199],{"class":198},[154,1309,1310],{"class":180},"true\n",[154,1312,1313,1316,1318,1321,1323,1325,1328],{"class":156,"line":241},[154,1314,1315],{"class":194},"TODAY",[154,1317,199],{"class":198},[154,1319,1320],{"class":194},"$(",[154,1322,1067],{"class":250},[154,1324,1070],{"class":173},[154,1326,1327],{"class":180}," +%F",[154,1329,1330],{"class":194},")\n",[154,1332,1333,1336,1338,1340,1342,1344,1347,1350,1352,1355,1357],{"class":156,"line":247},[154,1334,1335],{"class":194},"CUTOFF",[154,1337,199],{"class":198},[154,1339,1320],{"class":194},[154,1341,1067],{"class":250},[154,1343,1070],{"class":173},[154,1345,1346],{"class":173}," -d",[154,1348,1349],{"class":180}," \"-${",[154,1351,1253],{"class":194},[154,1353,1354],{"class":180},"} days\"",[154,1356,1327],{"class":180},[154,1358,1330],{"class":194},[154,1360,1361],{"class":156,"line":266},[154,1362,188],{"emptyLinePlaceholder":187},[154,1364,1365,1368],{"class":156,"line":277},[154,1366,1367],{"class":250},"sweep",[154,1369,1370],{"class":194},"() {\n",[154,1372,1373,1376,1379,1381,1383,1386,1388,1391,1393,1395,1398],{"class":156,"line":288},[154,1374,1375],{"class":198},"  local",[154,1377,1378],{"class":194}," kind",[154,1380,199],{"class":198},[154,1382,300],{"class":180},[154,1384,1385],{"class":173},"$1",[154,1387,300],{"class":180},[154,1389,1390],{"class":194}," query",[154,1392,199],{"class":198},[154,1394,300],{"class":180},[154,1396,1397],{"class":173},"$2",[154,1399,219],{"class":180},[154,1401,1402,1404,1407,1410,1412,1414,1417,1419,1421,1423],{"class":156,"line":305},[154,1403,1168],{"class":250},[154,1405,1406],{"class":194}," $kind ",[154,1408,1409],{"class":180},"list",[154,1411,1131],{"class":173},[154,1413,294],{"class":180},[154,1415,1416],{"class":194},"$query",[154,1418,300],{"class":180},[154,1420,1137],{"class":173},[154,1422,1140],{"class":180},[154,1424,1143],{"class":198},[154,1426,1427,1430,1433,1435,1438,1441,1444,1446,1448,1451,1454,1456],{"class":156,"line":316},[154,1428,1429],{"class":198},"  while",[154,1431,1432],{"class":180}," IFS",[154,1434,199],{"class":198},[154,1436,1437],{"class":180},"$'",[154,1439,1440],{"class":173},"\\t",[154,1442,1443],{"class":180},"'",[154,1445,1151],{"class":180},[154,1447,1154],{"class":173},[154,1449,1450],{"class":180}," id",[154,1452,1453],{"class":180}," flagged",[154,1455,1160],{"class":194},[154,1457,1163],{"class":198},[154,1459,1460,1463,1466,1469,1471,1474,1476,1479,1481,1483,1485,1487,1490,1493],{"class":156,"line":327},[154,1461,1462],{"class":198},"    if",[154,1464,1465],{"class":194}," [[ ",[154,1467,1468],{"class":198},"-z",[154,1470,294],{"class":180},[154,1472,1473],{"class":194},"$flagged",[154,1475,300],{"class":180},[154,1477,1478],{"class":198}," ||",[154,1480,294],{"class":180},[154,1482,1473],{"class":194},[154,1484,300],{"class":180},[154,1486,1299],{"class":198},[154,1488,1489],{"class":180}," \"None\"",[154,1491,1492],{"class":194}," ]]; ",[154,1494,1495],{"class":198},"then\n",[154,1497,1498,1501,1504,1507,1509,1511,1514,1516,1519,1521,1524,1526,1529,1531,1534,1536],{"class":156,"line":336},[154,1499,1500],{"class":250},"      az",[154,1502,1503],{"class":180}," resource",[154,1505,1506],{"class":180}," tag",[154,1508,1176],{"class":173},[154,1510,294],{"class":180},[154,1512,1513],{"class":194},"$id",[154,1515,300],{"class":180},[154,1517,1518],{"class":173}," --tags",[154,1520,294],{"class":180},[154,1522,1523],{"class":194},"$TAG",[154,1525,199],{"class":180},[154,1527,1528],{"class":194},"$TODAY",[154,1530,300],{"class":180},[154,1532,1533],{"class":173}," --is-incremental",[154,1535,1137],{"class":173},[154,1537,1194],{"class":180},[154,1539,1540,1543,1546,1548],{"class":156,"line":341},[154,1541,1542],{"class":173},"      echo",[154,1544,1545],{"class":180}," \"flagged   ",[154,1547,1513],{"class":194},[154,1549,219],{"class":180},[154,1551,1552,1555,1557,1559,1561,1563,1566,1568,1571,1573,1575,1577,1579,1581,1583,1585,1587,1589,1591],{"class":156,"line":347},[154,1553,1554],{"class":198},"    elif",[154,1556,1465],{"class":194},[154,1558,300],{"class":180},[154,1560,1473],{"class":194},[154,1562,300],{"class":180},[154,1564,1565],{"class":198}," \u003C",[154,1567,294],{"class":180},[154,1569,1570],{"class":194},"$CUTOFF",[154,1572,300],{"class":180},[154,1574,1478],{"class":198},[154,1576,294],{"class":180},[154,1578,1473],{"class":194},[154,1580,300],{"class":180},[154,1582,1299],{"class":198},[154,1584,294],{"class":180},[154,1586,1570],{"class":194},[154,1588,300],{"class":180},[154,1590,1492],{"class":194},[154,1592,1495],{"class":198},[154,1594,1595,1598,1601],{"class":156,"line":360},[154,1596,1597],{"class":198},"      if",[154,1599,1600],{"class":194}," $DELETE; ",[154,1602,1495],{"class":198},[154,1604,1605,1608,1610,1613,1615,1617,1619,1621,1623],{"class":156,"line":370},[154,1606,1607],{"class":250},"        az",[154,1609,1503],{"class":180},[154,1611,1612],{"class":180}," delete",[154,1614,1176],{"class":173},[154,1616,294],{"class":180},[154,1618,1513],{"class":194},[154,1620,300],{"class":180},[154,1622,1137],{"class":173},[154,1624,1194],{"class":180},[154,1626,1627,1630,1633,1635,1638,1640],{"class":156,"line":380},[154,1628,1629],{"class":173},"        echo",[154,1631,1632],{"class":180}," \"deleted   ",[154,1634,1513],{"class":194},[154,1636,1637],{"class":180}," (flagged ",[154,1639,1473],{"class":194},[154,1641,1642],{"class":180},")\"\n",[154,1644,1645],{"class":156,"line":393},[154,1646,1647],{"class":198},"      else\n",[154,1649,1650,1652,1655,1657,1659,1661],{"class":156,"line":403},[154,1651,1629],{"class":173},[154,1653,1654],{"class":180}," \"expired   ",[154,1656,1513],{"class":194},[154,1658,1637],{"class":180},[154,1660,1473],{"class":194},[154,1662,1663],{"class":180},", rerun with --delete)\"\n",[154,1665,1666],{"class":156,"line":413},[154,1667,1668],{"class":198},"      fi\n",[154,1670,1671],{"class":156,"line":421},[154,1672,1673],{"class":198},"    else\n",[154,1675,1676,1678,1681,1683,1685,1687],{"class":156,"line":426},[154,1677,1542],{"class":173},[154,1679,1680],{"class":180}," \"waiting   ",[154,1682,1513],{"class":194},[154,1684,1637],{"class":180},[154,1686,1473],{"class":194},[154,1688,1642],{"class":180},[154,1690,1691],{"class":156,"line":432},[154,1692,1693],{"class":198},"    fi\n",[154,1695,1696],{"class":156,"line":445},[154,1697,1698],{"class":198},"  done\n",[154,1700,1701],{"class":156,"line":455},[154,1702,982],{"class":194},[154,1704,1705],{"class":156,"line":465},[154,1706,188],{"emptyLinePlaceholder":187},[154,1708,1709,1712,1715,1718,1721,1723,1725,1727,1729,1732,1734,1736,1739],{"class":156,"line":478},[154,1710,1711],{"class":198},"for",[154,1713,1714],{"class":194}," sub ",[154,1716,1717],{"class":198},"in",[154,1719,1720],{"class":194}," $(",[154,1722,251],{"class":250},[154,1724,999],{"class":180},[154,1726,1128],{"class":180},[154,1728,1131],{"class":173},[154,1730,1731],{"class":180}," \"[?state=='Enabled'].id\"",[154,1733,1137],{"class":173},[154,1735,1140],{"class":180},[154,1737,1738],{"class":194},"); ",[154,1740,1163],{"class":198},[154,1742,1743,1745,1747,1749,1751,1753,1756],{"class":156,"line":488},[154,1744,1168],{"class":250},[154,1746,999],{"class":180},[154,1748,1002],{"class":180},[154,1750,1005],{"class":173},[154,1752,294],{"class":180},[154,1754,1755],{"class":194},"$sub",[154,1757,219],{"class":180},[154,1759,1760,1762,1765,1767],{"class":156,"line":497},[154,1761,1199],{"class":173},[154,1763,1764],{"class":180}," \"== subscription ",[154,1766,1755],{"class":194},[154,1768,219],{"class":180},[154,1770,1771,1774,1777,1780,1782],{"class":156,"line":505},[154,1772,1773],{"class":250},"  sweep",[154,1775,1776],{"class":180}," \"disk\"",[154,1778,1779],{"class":180}," \"[?managedBy==null].[id, tags.${",[154,1781,1263],{"class":194},[154,1783,1784],{"class":180},"}]\"\n",[154,1786,1787,1789,1792,1795,1797],{"class":156,"line":519},[154,1788,1773],{"class":250},[154,1790,1791],{"class":180}," \"network public-ip\"",[154,1793,1794],{"class":180}," \"[?ipConfiguration==null && natGateway==null].[id, tags.${",[154,1796,1263],{"class":194},[154,1798,1784],{"class":180},[154,1800,1801],{"class":156,"line":530},[154,1802,1211],{"class":198},[11,1804,1805],{},"Run it from a pipeline with an identity that has Contributor on the subscriptions in scope, and send the output wherever the teams will see it. The grace period and the tag are what make deletion safe: anyone who still needs a disk has two weeks to remove the tag or attach it, and the tag shows in the portal next to the resource. Before trusting the delete path, run it for a few weeks in report-only mode and check what it would have removed. Automating the cleanup means the governance conversation with developers is only ever about the decisions that actually need a decision, not about nagging people to remember chores a script should be doing anyway.",[11,1807,1808],{},"The throughline across all of it is that governance earns developer trust by being fast and legible, not by being strict. A rule that's instant, explainable, and has a documented exception path gets followed. A rule that's slow and opaque gets worked around, quietly, the first time it's in someone's way, and a worked-around control is worse than no control at all, because it looks like coverage on paper that isn't there in practice.",[15,1810,1812],{"id":1811},"references","References",[1814,1815,1816,1825,1832,1839,1846,1853,1860,1867,1874,1881],"ul",{},[1817,1818,1819],"li",{},[1085,1820,1824],{"href":1821,"rel":1822},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fgovernance\u002Fpolicy\u002Fsamples\u002Fbuilt-in-policies",[1823],"nofollow","Azure Policy built-in policy definitions",[1817,1826,1827],{},[1085,1828,1831],{"href":1829,"rel":1830},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fcli\u002Fazure\u002Fpolicy\u002Fassignment",[1823],"az policy assignment (Azure CLI)",[1817,1833,1834],{},[1085,1835,1838],{"href":1836,"rel":1837},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fcli\u002Fazure\u002Fpolicy\u002Fexemption",[1823],"az policy exemption (Azure CLI)",[1817,1840,1841],{},[1085,1842,1845],{"href":1843,"rel":1844},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fgovernance\u002Fpolicy\u002Fconcepts\u002Feffect-modify",[1823],"Azure Policy modify effect",[1817,1847,1848],{},[1085,1849,1852],{"href":1850,"rel":1851},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Fenable-tag-inheritance",[1823],"Group and allocate costs using tag inheritance",[1817,1854,1855],{},[1085,1856,1859],{"href":1857,"rel":1858},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Ftutorial-acm-create-budgets",[1823],"Create and manage budgets",[1817,1861,1862],{},[1085,1863,1866],{"href":1864,"rel":1865},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Fquick-create-budget-bicep",[1823],"Quickstart: Create a budget with Bicep",[1817,1868,1869],{},[1085,1870,1873],{"href":1871,"rel":1872},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fsavings-plan\u002Fdecide-between-savings-plan-reservation",[1823],"Decide between a savings plan and a reservation",[1817,1875,1876],{},[1085,1877,1880],{"href":1878,"rel":1879},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcloud-adoption-framework\u002Fready\u002Flanding-zone\u002Fdesign-area\u002Fsubscription-vending",[1823],"Subscription vending (Cloud Adoption Framework)",[1817,1882,1883],{},[1085,1884,1887],{"href":1885,"rel":1886},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fvirtual-machines\u002Flinux\u002Ffind-unattached-disks",[1823],"Find and delete unattached Azure managed disks (Azure CLI)",[1889,1890,1891],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":150,"searchDepth":164,"depth":164,"links":1893},[1894,1895,1896,1897,1898],{"id":17,"depth":164,"text":18},{"id":645,"depth":164,"text":646},{"id":658,"depth":164,"text":659},{"id":1097,"depth":164,"text":1098},{"id":1811,"depth":164,"text":1812},"techcolumnist",[1901,1902],"engineering","strategy","2026-08-12T14:00:00Z","Azure cost governance that catches waste without approval gates: built-in policy guardrails, budget alerts as code, and scheduled cleanup, with the scripts.","md",false,null,{},"\u002Fblog\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity",{"title":6,"description":1904},[1899],"blog\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity",[1914],"azure","\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity\u002F","XXx11ybmuXQqRAm2s2WZuJTbu_TbNJargCHwmjNCyLc",{"title":1918,"description":1919,"date":1920,"url":1921,"categories":1922,"tags":1923,"image":1907,"readingTime":222,"canonical":1899,"sites":1927,"series":1907,"seriesOrder":1907},"Data Warehouse: Reusable SQL Models for IT Operations Metrics","Layered, version-controlled SQL models for MTTR, ticket aging, SLA compliance, and patch compliance, with plain-SQL tests and the dbt equivalent.","2026-08-19T14:00:00Z","\u002F2026\u002F08\u002F19\u002Fdata-warehouse-reusable-sql-models-for-it-operations-metrics\u002F",[1901],[1924,1925,1926],"data-warehouse","reporting","sql",[1899],{"title":1929,"description":1930,"date":1931,"url":1932,"categories":1933,"tags":1934,"image":1907,"readingTime":288,"canonical":1899,"sites":1937,"series":1907,"seriesOrder":1907},"Intune: Conditional Access Policies That Don't Lock Out Your Help Desk","Seven concrete Entra ID Conditional Access policies, the Graph JSON and PowerShell to create them in report-only, and the roles and queries your help desk needs.","2026-08-05T14:00:00Z","\u002F2026\u002F08\u002F05\u002Fintune-conditional-access-policies-that-don-t-lock-out-your-help-desk\u002F",[1901],[1935,1936],"intune","entra-id",[1899],[1939,1947,1958],{"title":1940,"description":1941,"date":1942,"url":1943,"categories":1944,"tags":1945,"image":1907,"readingTime":222,"canonical":1899,"sites":1946,"series":1907,"seriesOrder":1907},"Azure: Landing Zone Design for a Mid-Size Company, Three Years In","Three years of running an Azure landing zone: a smaller management group tree, pinned policy versions, and subscription vending, with the Bicep and az CLI to build it.","2026-04-15T14:00:00Z","\u002F2026\u002F04\u002F15\u002Fazure-landing-zone-design-for-a-mid-size-company-three-years-in\u002F",[1901,1902],[1914],[1899],{"title":1948,"description":1949,"date":1950,"url":1951,"categories":1952,"tags":1953,"image":1907,"readingTime":191,"canonical":1899,"sites":1957,"series":1907,"seriesOrder":1907},"Hyper-V to Talos: Rethinking the Virtualization Stack","Why moving workloads off Hyper-V VMs onto a Talos Kubernetes cluster changed the whole stack, with an inventory script and a VM-to-manifest example.","2026-09-16T14:00:00Z","\u002F2026\u002F09\u002F16\u002Fhyper-v-to-talos-rethinking-the-virtualization-stack\u002F",[1901,1902],[1954,1955,1956],"hyper-v","talos","kubernetes",[1899],{"title":1959,"description":1960,"date":1961,"url":1962,"categories":1963,"tags":1964,"image":1907,"readingTime":191,"canonical":1899,"sites":1968,"series":1907,"seriesOrder":1907},"Windows Management: A Discovery Inventory Nobody Maintains by Hand","Why a self-refreshing Windows inventory built from AD and CIM discovery outlasts a spreadsheet, with the sweep script, staleness report and schedule.","2026-09-02T14:00:00Z","\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand\u002F",[1901,1902],[1965,1966,1967],"discovery","windows","active-directory",[1899],{"doc":1907,"posts":1970},[],1790052514087]