Build1 publisher2 min readPublished
Azure Policy declines the D-series request before the deployment bills an hour
A dev.to walkthrough puts the spend limit in an Azure Policy deny rule bound to one resource group, with a two-SKU allow list and a custom RBAC role whose only actions are read and restart. The gate declines the request at deployment time.
The Engineer · Build desk

What happened
- A DevSecOps engineer describes moving cloud cost control into the Azure Resource Manager admission path, using Azure Policy and custom least-privilege RBAC roles to decline unauthorized resource allocations.
- The assignment, named Enforce_B_Series_Only, is bound to one resource group, Marathahalli_Lab_RG, created in the southeastasia region.
- A companion custom role, VM Restart Operator, grants only virtualMachines/read and virtualMachines/restart/action, with an empty NotActions array and subscription-level assignable scope.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- capability Enforcement at the gate costs nothing to run: the denied request allocates no hardware, so there is no orphan instance for a weekend cleanup job to chase and no invoice line to dispute.
- cost The allow list caps the hourly rate a mistake can reach, and the sandbox still pays for an approved Standard_B1s that nobody remembers to delete.
- constraint Every other resource group in the subscription still accepts whatever SKU its principals are entitled to create, so the guardrail protects one perimeter and not the account.
- decision Someone else has to hold delete rights, because the restart-only role cannot remove the machine it is trusted to reboot.
Call `az vm create` inside Marathahalli_Lab_RG with a D-series shape and the deployment does not produce a resource [7]. The rule tests two conditions joined by `allOf`: the `type` field equals `Microsoft.Compute/virtualMachines`, and `sku.name` is not in a list holding `Standard_B1s` and `Standard_B2ats_v2` [4]. Both hold for that request, so the effect is `deny` [4]. No resource, so no billable hours [20].
The list has two entries, so every other VM SKU is denied, including the rest of the B-series [12]. The definition's display name is "Restrict VM SKUs to B-Series" [5], and a reviewer who reads that name instead of the JSON will overestimate what the assignment permits [13]. Anyone in that resource group who needs a bigger machine needs an exemption or a second assignment.
The condition is equally specific about the resource type. A request whose `type` field is anything other than `Microsoft.Compute/virtualMachines` fails the `allOf`, and the deny does not fire [14]. I would test that field first before trusting this gate with a sandbox subscription, because GPU capacity that arrives under a different type string is not covered by the rule as written.
The rule catches a different failure from the one the post opens with. Its author describes a junior developer spinning up a high-performance compute or GPU node for a minor test case and forgetting to deprovision it over the weekend [3]. A `Standard_B1s` left running over the weekend passes the allow list [16]. The deny caps the hourly rate a mistake can reach; the forgotten instance keeps running until somebody deletes it.
The identity half is a custom role named VM Restart Operator, with `IsCustom` true, two entries in `Actions` (`Microsoft.Compute/virtualMachines/read` and `Microsoft.Compute/virtualMachines/restart/action`), an empty `NotActions` array, and assignable scope set to the subscription [8]. The author wrote that the role "explicitly limits identity capabilities to virtual machine visibility and reboot actions, leaving write actions, configurations changes, and resource deletions sealed shut" [9]. Delete is absent from `Actions`, so a principal holding only this role cannot remove the forgotten VM it is allowed to reboot [17]. The subscription ID is injected into the scope string with `sed` from `az account show --query id --output tsv` before `az role definition create` runs [10].
One more limit on the demonstration: the assignment is bound to a single resource group [6], and a deployment into any other resource group in the same subscription is not evaluated against it [15]. The post says the lab produced two distinct runtime errors, and the text breaks off as it begins to describe them [11].
What to watch
- Whether the same rule survives a move from one resource group to subscription or management group scope.
- Whether an exemption path exists for the first team that needs a shape outside the two allowed SKUs.
- Whether the deny rule is paired with a control that stops approved-SKU machines from running unattended.