Skip to content

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

Illustration accompanying Azure Policy declines the D-series request before the deployment bills an hour

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.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories