<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonny Diamond</title>
    <description>The latest articles on DEV Community by Jonny Diamond (@jonny_diamond).</description>
    <link>https://dev.to/jonny_diamond</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3649997%2F044590b8-ac34-49cb-9542-149f27a6bb79.png</url>
      <title>DEV Community: Jonny Diamond</title>
      <link>https://dev.to/jonny_diamond</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jonny_diamond"/>
    <language>en</language>
    <item>
      <title>Using Azure archetypes at scale to simplify governance: Why the CAF works, part 2</title>
      <dc:creator>Jonny Diamond</dc:creator>
      <pubDate>Tue, 06 Oct 2026 20:21:16 +0000</pubDate>
      <link>https://dev.to/jonny_diamond/using-azure-archetypes-at-scale-to-simplify-governance-why-the-caf-works-part-2-1fn</link>
      <guid>https://dev.to/jonny_diamond/using-azure-archetypes-at-scale-to-simplify-governance-why-the-caf-works-part-2-1fn</guid>
      <description>&lt;p&gt;&lt;em&gt;Using Azure archetypes at scale to simplify governance: Why the CAF works, part 2&lt;/em&gt;&lt;br&gt;&lt;br&gt;
In part 1 we looked at why the Cloud Adoption Framework works. In this post I want to look at the part of it that I think does the most for governance at scale: landing zone archetypes. What are they, and why does governing two hundred subscriptions with them take about the same effort as governing ten?&lt;/p&gt;

&lt;h4&gt;
  
  
  Start with what you’re hosting
&lt;/h4&gt;

&lt;p&gt;Let’s start by emphasising that archetypes work best when you know the compliance and security requirements of your cloud workloads ahead of time. A good time to learn that lesson is early in adoption, ideally in the Strategy phase &lt;a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/strategy/" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/strategy/&lt;/a&gt;. It sounds something like this: we are Company X, and we need to host workloads that are PCI DSS compliant, or ISO 27001 compliant, or that meet our very rigorous internal security benchmark. Or maybe it’s the Microsoft cloud security benchmark (MCSB), which is a collection of excellent security best practices.&lt;/p&gt;

&lt;p&gt;If you don’t know the security requirements of what you’re hosting, that’s the place to start. It’s the cornerstone of everything that follows. What you’re hosting drives your requirements, and your security and information risk should decide what those requirements look like. You don’t need a list of every workload you’ll ever host, but you do need to know the kinds of risk you’ll carry, like the regulations that apply to you and how you classify your data.&lt;/p&gt;

&lt;p&gt;Yes, you could just assume the highest standard for everything, but for most organisations I think that’s the wrong default. It comes with an operational toll and added cost that most workloads don’t need. If we’re adopting Azure, it’s because we want to host things there and get the benefits, like lower operational effort and more agility. Locking everything down to PCI level gives a lot of that back. One security posture for the whole estate is either too weak for your regulated workloads or too costly for everything else, and archetypes mean you can host both and have them coexist.&lt;/p&gt;

&lt;h4&gt;
  
  
  What it looks like without them
&lt;/h4&gt;

&lt;p&gt;You may have seen a tenant where this went wrong. The management groups mirror the org chart, with one per business unit or region, and sometimes Dev, Test and Prod under each of those. Policy gets assigned wherever someone needed it at the time, the same assignments get repeated at different levels, and anything that doesn’t fit gets an exemption. Before long the exemption list is longer than the assignment list, and nobody can tell you what posture a subscription actually has without going and looking.&lt;/p&gt;

&lt;p&gt;Microsoft’s management group guidance calls this out directly &lt;a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/tailoring-alz" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/tailoring-alz&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don’t duplicate your organisational structure into a deeply nested management group hierarchy. Use management groups for policy assignment versus billing and RBAC purposes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also says not to create management groups for production, testing and development. Those should be separate subscriptions in the same management group.&lt;/p&gt;

&lt;h4&gt;
  
  
  Platform landing zones
&lt;/h4&gt;

&lt;p&gt;Before we get to archetypes properly, I need to explain platform landing zones, commonly referred to as the shared services. These are the connectivity, identity and management landing zones. They exist to provide network services, identity services, platform automation and so on, which the application landing zones (our workloads) consume.&lt;/p&gt;

&lt;p&gt;Depending on the operating model, the hub is centrally managed. It hosts either a standard hub VNet or an Azure Virtual WAN hub. Not every workload uses it, though. Some sit in isolated networks with their own VNet and firewall, sometimes called islands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5k5g51zhesyqutl3otc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5k5g51zhesyqutl3otc.png" width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each workload on the right is peered to the hub and consumes its central services. Keep this picture in mind, because the network is one of the things that defines an archetype.&lt;/p&gt;

&lt;h4&gt;
  
  
  So what is an archetype?
&lt;/h4&gt;

&lt;p&gt;In the CAF we use landing zone archetypes to group subscriptions with the same policy requirements, and therefore the same compliance requirements. This is how Microsoft defines one &lt;a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/tailoring-alz" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/tailoring-alz&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A landing zone archetype describes what needs to be true to ensure a landing zone (Azure subscription) meets the expected environment and compliance requirements at a specific scope.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The examples it gives are Azure Policy assignments, RBAC assignments and “centrally managed resources such as networking”. So an archetype isn’t just a management group. The management group is how you implement it. Because policy is inherited down the tree, everything assigned above it counts too. A subscription in a PCI archetype gets the intermediate root’s policies, plus the Landing Zones policies, plus the PCI ones.&lt;/p&gt;

&lt;p&gt;The question we must ask first is: can I categorise the types of workloads I’ll host? Do they have different compliance or security requirements? Should security be more strictly enforced in some than in others? It’s these requirements that drive the archetypes. Whatever types of workload you need to host, that’s the right answer for your archetypes, and Microsoft’s guidance says much the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create management groups under your landing zone management group to represent the types of workloads that you host. These groups are based on the security, compliance, connectivity, and feature needs of the workloads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice connectivity in that list. Out of the box, ALZ gives us Corp and Online (plus Local, for workloads on Azure Local), and the difference between Corp and Online is the hub-and-spoke picture from earlier. Corp is for workloads that connect to the corporate network through the hub. Online is for workloads that need direct internet access, or that don’t need a virtual network at all.&lt;/p&gt;

&lt;p&gt;If you look at the ALZ library, every policy assignment Corp adds is about the network: deny public endpoints, deny public IPs on NICs, deny hybrid networking (that’s the hub’s job), and point private endpoints at the central private DNS zones. Online doesn’t add any of its own. It just inherits from Landing Zones. The islands have a different connectivity requirement again, so they don’t belong in Corp. They go in Online or in an archetype of their own.&lt;/p&gt;

&lt;p&gt;The platform landing zones work the same way. Identity has its own assignment to deny public IPs. Connectivity can’t have that one, because the hub needs public IPs for its firewall and gateways, so the two get different policy.&lt;/p&gt;

&lt;h4&gt;
  
  
  Audit or enforce?
&lt;/h4&gt;

&lt;p&gt;Now policy has a target, and we can enforce a posture on an archetype because the workload type requires it. This is where the catch with MCSB comes in: out of the box, it audits. If we look at the initiative definition, none of its effect parameters default to Deny. Assign it as-is and you get a very good report of where you’re falling short, but on its own it won’t stop anyone deploying something non-compliant. The PCI DSS v4.0.1 initiative is the same.&lt;/p&gt;

&lt;h4&gt;
  
  
  There are ways to enforce it, though:
&lt;/h4&gt;

&lt;p&gt;Set the effect parameters to Deny in your assignment (or DeployIfNotExists or Modify, where the policy supports them). Dozens of the MCSB effect parameters accept Deny.&lt;br&gt;&lt;br&gt;
Use the Deny and Enforce options in Defender for Cloud, which work on a subset of MCSB recommendations.&lt;br&gt;&lt;br&gt;
Assign deny policies directly to the archetype, which is exactly what ALZ does for Corp.&lt;br&gt;&lt;br&gt;
Don’t flip everything to Deny on day one. Run it in audit first, look at what would break, then move it to Deny. And keep in mind that policy only covers the Azure side. PCI DSS also covers people and process, and no initiative does that part for you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Okay, so how does this let governance scale?
&lt;/h4&gt;

&lt;p&gt;Once we vend a subscription, we place it in an archetype and policy enforces the requirements. That’s pretty much it. The ALZ FAQ uses this as its example of policy-driven governance: a new subscription is created and placed into a management group, and Azure Policy makes sure Defender for Cloud is enabled on it.&lt;/p&gt;

&lt;p&gt;Nobody configures controls subscription by subscription, and nobody has to remember that this particular workload needs private endpoints. Policy becomes the guardrail for the archetype, and the archetype is the workload type. Whether we have ten subscriptions or two hundred, the effort is about the same.&lt;/p&gt;

&lt;p&gt;It also means subscription vending is where the important decision gets made. When a new workload comes in, the intake asks what it is, what data it handles and what it needs to connect to, and this informs the archetype. This is why knowing roughly what types of workloads you need to host is beneficial.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyirkjobmwxu0lj20mr9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyirkjobmwxu0lj20mr9.png" width="800" height="403"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Some tailored archetypes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The boxes in green are the safest place to change the CAF management group structure. That’s Microsoft’s wording too: it calls the area under Landing Zones “&lt;em&gt;the most common and safest place in the hierarchy to add more archetypes&lt;/em&gt;”. Anything assigned at the intermediate root applies to every subscription below it, so that’s where you want to change the least. The example here includes PCI, which is also the example Microsoft uses, so any subscription with that requirement goes in there.&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;Archetypes simplify landing zone (workload) governance by letting us group workloads with the same requirements together. Work out your requirements early and let security and information risk shape them. Turn them into archetypes, make sure the policies enforce and don’t only audit, and pick the archetype when you vend each subscription. So why use them? Because once they’re in place, governing a new workload comes down to one decision at vending time, and policy does the rest.&lt;/p&gt;

</description>
      <category>governance</category>
      <category>azure</category>
      <category>azurecloudadoption</category>
      <category>security</category>
    </item>
    <item>
      <title>Enabling Azure DDoS Protection at Scale with Policy-Driven Governance</title>
      <dc:creator>Jonny Diamond</dc:creator>
      <pubDate>Sun, 04 Oct 2026 14:04:41 +0000</pubDate>
      <link>https://dev.to/jonny_diamond/enabling-azure-ddos-protection-at-scale-with-policy-driven-governance-1ea</link>
      <guid>https://dev.to/jonny_diamond/enabling-azure-ddos-protection-at-scale-with-policy-driven-governance-1ea</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 of Why the CAF works: how the Cloud Adoption Framework (CAF) solves governance and scale problems. This one: DDoS protection.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To mitigate the threat from DDoS attacks we can use Azure DDoS Protection. In October 2025 Azure mitigated a 15.72 Tbps DDoS attack from more than 500,000 source IPs, the largest ever seen in the cloud (CPO Magazine). Cloudflare’s 2025 Q4 report shows network-layer DDoS attacks more than tripled in 2025, with attacks reaching 31.4 Tbps.&lt;/p&gt;

&lt;p&gt;So the question becomes: how can we use Azure DDoS Protection to automatically protect our Azure estate? The solution needs to work reliably, at scale, under full automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  A few caveats
&lt;/h3&gt;

&lt;p&gt;When we talk about protecting our Azure estate with DDoS protection, we really mean protecting public IP addresses in Online landing zones in a hub-and-spoke topology. That’s where our network is exposed to the internet. These PIPs might be attached to VMs, Application Gateways, API Management, Bastion, Azure Firewall and so on.&lt;/p&gt;

&lt;p&gt;DDoS Protection works at the network layer (L3 and L4). For application-layer protection we need Azure WAF or a similar solution.&lt;/p&gt;

&lt;p&gt;PaaS services with public endpoints (for example a storage account) are out of scope. They sit behind Microsoft’s own edge protection, not your VNet.&lt;/p&gt;

&lt;p&gt;Azure DDoS Protection is a resource like any other. It lives in the Microsoft.Network/ddosProtectionPlans resource provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which tier do we need?
&lt;/h3&gt;

&lt;p&gt;Every public IP in Azure gets infrastructure protection for free. It’s tuned to protect Azure itself, not your specific workload. Above that there are two paid tiers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DDoS IP Protection&lt;/strong&gt; : per-IP pricing, suited to small deployments with a handful of public IPs. You get core DDoS mitigation, but no DDoS Rapid Response, no cost protection and no WAF discount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DDoS Network Protection&lt;/strong&gt; : a plan that covers every public IP in any VNet enrolled in it. You also get DRR (access to the DDoS response team during an active attack), cost protection and a discount on Azure WAF.&lt;/p&gt;

&lt;p&gt;The key design decision is scale. Once an estate has more than a handful of public IPs, IP Protection’s per-IP pricing quickly approaches the cost of a plan, and the plan covers 100 PIPs. Protect the VNet, not individual IPs. For an enterprise-scale estate, Network Protection is the correct default.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem this solves
&lt;/h3&gt;

&lt;p&gt;In the CAF architecture, Online landing zones may be added frequently. Each one gets a spoke VNet, and each spoke VNet might get a PIP. If DDoS enrollment is a manual step, it might be missed or done incorrectly. Either way you end up with unprotected public endpoints. So we treat DDoS protection, along with other aspects of core networking, as a Platform concern. Also DDoS IP protection is less cost effective per resource than a DDoS plan (assuming you deploy more than about 15 PIPs).&lt;/p&gt;

&lt;p&gt;This is exactly the class of problem CAF solves: a platform team that owns shared network services, and policy-driven governance that enforces the desired state instead of relying on a documented manual process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who owns the spoke VNet?
&lt;/h3&gt;

&lt;p&gt;This is the question the whole design depends on, and it’s easy to skip past.&lt;/p&gt;

&lt;p&gt;In the CAF reference model, the spoke VNet isn’t built by the workload team. It arrives with the landing zone through subscription vending. The platform team’s vending pipeline creates the subscription, places it in the Online Management Group, creates the spoke VNet and peers it to the hub. The workload team gets a subscription with its network already connected, and builds inside it.&lt;/p&gt;

&lt;p&gt;That vending principal already has rights in the Connectivity subscription, because it has to create the hub side of the peering. That’s also important to note.&lt;/p&gt;

&lt;h3&gt;
  
  
  The CAF pattern
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;One plan. Create a single Microsoft.Network/ddosProtectionPlans resource in the Connectivity subscription alongside the hub. One plan can be shared across subscriptions in the same tenant.&lt;/li&gt;
&lt;li&gt;Vend VNets already protected. Subscription vending creates each spoke VNet with enableDdosProtection: true and ddosProtectionPlan.id set to the central plan. New landing zones are protected from the moment the VNet exists.&lt;/li&gt;
&lt;li&gt;Policy as the guardrail. Assign the built-in &lt;em&gt;Virtual networks should be protected by Azure DDoS Protection&lt;/em&gt; (94de2ad3-e0c1-4caf-ad78-5d47bbc83d3d) at the Online landing zones Management Group. Don't assign it at the intermediate root, or it will also catch Platform and Corp VNets (or do, it depends on a few factors). It uses the Modify effect: any VNet created or updated without the plan gets it added in flight, and a remediation task fixes existing VNets.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vending is the primary mechanism. The policy catches drift and anything created outside vending.&lt;/p&gt;

&lt;p&gt;Modify is the right effect here. We’re only setting two properties on the resource being evaluated, there’s no nested deployment, and the VNet is protected at creation rather than minutes afterwards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it works: the join check
&lt;/h3&gt;

&lt;p&gt;Associating a VNet with a DDoS plan is an operation with two scopes. On every VNet create or update that references the DDoS protection plan, ARM checks that the caller has Microsoft.Network/ddosProtectionPlans/join/action on the plan.&lt;/p&gt;

&lt;p&gt;Modify alters the caller’s request, so that check runs against whoever made the request. In the CAF model, that’s the platform vending principal, which already has rights in Connectivity. The check passes and everything works.&lt;/p&gt;

&lt;p&gt;Remediation tasks for existing VNets run as the policy assignment’s managed identity instead, so that identity needs join rights on the plan too. The portal only grants the roles in roleDefinitionIds at the assignment scope (the Online MG). It doesn't grant anything on a plan in another subscription. Add that grant yourself: a custom role with just ddosProtectionPlans/join/action, or Network Contributor on the plan's resource group.&lt;/p&gt;

&lt;h3&gt;
  
  
  When the model breaks
&lt;/h3&gt;

&lt;p&gt;The pattern is simple as long as the platform owns the VNet. It breaks when workload teams deploy VNets themselves, for example because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application teams own their networking end to end,&lt;/li&gt;
&lt;li&gt;a workload team takes over the vended VNet in its own IaC and redeploys the whole resource, or&lt;/li&gt;
&lt;li&gt;landing zones are built by hand or per team instead of vended.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the VNet PUT runs as the workload team’s service principal. Modify adds the plan reference, ARM runs the join check against a principal with no rights in Connectivity (and it shouldn’t have any), and the deployment fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LinkedAuthorizationFailed: The client '&amp;lt;sp-object-id&amp;gt;' has permission to perform action
'Microsoft.Network/virtualNetworks/write' on scope 'vnet-spoke-01'; however, it does not have
permission to perform action 'Microsoft.Network/ddosProtectionPlans/join/action' on the
linked scope(s) 'ddos-central'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy that was meant to protect the landing zone now blocks the landing zone team from deploying its own network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three ways out
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Put the ownership boundary back. The platform owns the VNet resource, and workload teams manage subnets, NSGs and route tables inside it. Subnets are child resources with their own PUT, so adding one doesn’t redeploy the VNet. This is the CAF answer, and it keeps the simple Modify pattern.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grant join rights to workload deployers. Give each landing zone’s deploying principal a custom role with only ddosProtectionPlans/join/action on the plan (most network built-in roles will be too broad). The downside here is it adds permission on a platform resource across every landing zone, and it has to be granted again for each new one. Vending could automate this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Switch to DeployIfNotExists. DINE doesn’t touch the caller’s request. The VNet is created as normal, the policy evaluates it afterwards, and a deployment runs as the policy assignment’s managed identity to enroll it. Only one identity ever holds join rights. The trade-offs:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;The gap: a new VNet is briefly unprotected (typically minutes).&lt;/li&gt;
&lt;li&gt;Complexity: a VNet PUT replaces the whole resource, so the template must carry across the existing address space, subnets, peerings and tags.&lt;/li&gt;
&lt;li&gt;Flapping: if the workload’s IaC redeploys the VNet without the DDoS properties, the enrollment is removed and DINE adds it back afterwards. Each redeploy opens a short unprotected window.&lt;/li&gt;
&lt;li&gt;A policy Denying resources without DDoS protection won't be happy&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What the cloud operating model tells you
&lt;/h3&gt;

&lt;p&gt;This is the real lesson, and it’s why the series is called &lt;em&gt;Why the CAF works&lt;/em&gt;. Protecting the estate from DDoS is simple (one plan, one built-in policy, one assignment) because the operating model puts VNet ownership with the platform team. The policy effect, the permissions and the failure modes all follow from that one decision.&lt;/p&gt;

&lt;p&gt;Move VNet ownership to workload teams and the same control needs extra permissions, a different policy effect, or both. That’s not a flaw in the built-in policy. It’s the cost of changing the operating model. Decide who owns the network first, then pick the mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost notes
&lt;/h3&gt;

&lt;p&gt;A Network Protection plan is a flat monthly charge covering up to 100 public IPs, with a per-IP charge above that. A single plan can be shared across subscriptions in the same tenant. For any estate with more than a few Online landing zones, one shared plan plus policy enrollment is both cheaper and safer than per-IP protection or a manual process. Check current pricing for your region: Azure DDoS Protection pricing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;DDoS attacks at multi-terabit scale are now commonplace. Azure DDoS Network Protection mitigates L3/L4 attacks against public IPs in enrolled VNets. In the CAF model, the platform vends each spoke VNet already attached to a central plan in Connectivity, and the built-in Modify policy at the Online Management Group catches anything that drifts.&lt;/p&gt;

&lt;p&gt;It works because the platform owns the VNet. When workload teams deploy VNets themselves, Modify runs as their principal and fails the join check on the plan. You can restore the ownership boundary, grant workload deployers join rights, or switch to a DINE policy running as a single managed identity. Pick based on who owns the network.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloudadoptionframewo</category>
      <category>automation</category>
      <category>governance</category>
    </item>
    <item>
      <title>What is an Azure deployment?</title>
      <dc:creator>Jonny Diamond</dc:creator>
      <pubDate>Mon, 01 Jun 2026 08:22:36 +0000</pubDate>
      <link>https://dev.to/jonny_diamond/what-is-an-azure-deployment-4741</link>
      <guid>https://dev.to/jonny_diamond/what-is-an-azure-deployment-4741</guid>
      <description>&lt;p&gt;I've been thinking recently about what a deployment actually is. When you deploy a resource via the portal, ARM, or Bicep, you'll notice under the deployment scope that there's something called a deployment. This is also apparent when using Bicep modules and CLI deployments.&lt;br&gt;
For example, nesting Bicep modules like this—with a main.bicep file calling modules that call other modules. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70xjzjmpfqe259wq64rb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70xjzjmpfqe259wq64rb.png" alt=" " width="654" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When deployed using the az deployment CLI command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# log into azure &lt;/span&gt;
az login

&lt;span class="c"&gt;# create a resource group&lt;/span&gt;
az group create &lt;span class="nt"&gt;-n&lt;/span&gt; rg &lt;span class="nt"&gt;-l&lt;/span&gt; uksouth

&lt;span class="c"&gt;# create deployment&lt;/span&gt;
az deployment group create &lt;span class="nt"&gt;-n&lt;/span&gt; my-deployment &lt;span class="nt"&gt;-g&lt;/span&gt; rg  &lt;span class="nt"&gt;-f&lt;/span&gt; main.bicep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3fv7jkntcovh8arbzwn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3fv7jkntcovh8arbzwn.png" alt=" " width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is ARM
&lt;/h2&gt;

&lt;p&gt;First, let's talk about what Azure Resource Manager (ARM) is. It's the management layer that takes requests for provisioning resources and actions them. When we use the portal, CLI, ARM templates, or Bicep, we're ultimately interacting with ARM. It acts as the management layer, checking the validity of requests and ensuring sufficient permissions (using its own permissions system). All requests for control plane activities and updates are sent to ARM at &lt;a href="https://management.azure.com" rel="noopener noreferrer"&gt;https://management.azure.com&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs65u7rzuqeqpqyzbszew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs65u7rzuqeqpqyzbszew.png" alt=" " width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram demonstrates this nicely—no matter how the request for resources is made, it's ARM that acts as the management layer for requests, the gateway to the resource providers. &lt;br&gt;
Resource providers are another important component of provisioning Azure services. They're the way ARM distributes requests to the relevant APIs that know how to provision the required resources. For example, a request for a virtual machine like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight biml"&gt;&lt;code&gt;resource vm 'Microsoft.Compute/virtualMachines@2024-03-01' = {
  name: 'myExampleVM'
  location: 'uksouth'
  properties: {
    hardwareProfile: {
      vmSize: 'Standard_B2s'
    }
    osProfile: {
      computerName: 'myvm'
      adminUsername: 'azureuser'
      adminPassword: 'P@ssw0rd123!'  
    }
    storageProfile: {
      imageReference: {
        publisher: 'Canonical'
        offer: 'Ubuntu2204'
        sku: '22_04-lts-gen2'
        version: 'latest'
      }
      osDisk: {
        createOption: 'FromImage'
        managedDisk: {
          storageAccountType: 'Standard_LRS'
        }
      }
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: networkInterface.id  // Reference to a NIC resource (not shown for brevity)
        }
      ]
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sent to the Microsoft.KeyVault namespace will fail because Microsoft.KeyVault doesn't know what to do with &lt;em&gt;properties.osProfile.computerName&lt;/em&gt; or any Microsoft.Compute properties—as this isn't a Key Vault property (or in the Key vault schema). Only Microsoft.Compute/ knows how to handle it.&lt;/p&gt;

&lt;p&gt;Pretty much every resource has a resource provider, which is accessed via ARM.&lt;/p&gt;

&lt;p&gt;Microsoft.Compute/virtualMachines&lt;br&gt;
Microsoft.ContainerInstance&lt;br&gt;
Microsoft.ContainerRegistry&lt;br&gt;
Microsoft.AzureStack&lt;/p&gt;

&lt;p&gt;These resource providers know how to handle requests for provisioning the resources they control. The resource providers define the operations for managing the resource and its configuration.&lt;/p&gt;

&lt;p&gt;They also represent the AUTH layer. You might notice that any resource provider URL looks like an API—that's because it is an API. It has permissions to perform activities or verbs like write, read, and delete. An object in a tenant's directory can have the authorization to perform actions on resource providers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Microsoft.ResourceProvider/resource/action&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is how authorisation to perform actions on Azure resources—including creating them—works.&lt;/p&gt;

&lt;p&gt;Subscriptions need resource providers registered to them, so we can conclude resource providers live in subscriptions. Not enabling resource providers by default is a good security practice.&lt;/p&gt;

&lt;p&gt;So ARM takes requests for resources, and resource providers are APIs used to control and manage types of resources. Authorisation is accomplished using permissions over exposed RP APIs. But what is a deployment?&lt;/p&gt;

&lt;p&gt;Even the Resource Manager documentation doesn't explicitly call out what a deployment is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;resource&lt;/strong&gt; - A manageable item that's available
through Azure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;resource group&lt;/strong&gt; - A container that holds related
resources for an Azure solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;resource provider&lt;/strong&gt; - A service that supplies Azure resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;declarative syntax&lt;/strong&gt; - Syntax that lets you state, "Here's what I intend to create,"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARM template&lt;/strong&gt; - A JavaScript Object Notation
(JSON) file that defines one or more resources to deploy to a resource
group, subscription, management group, or tenant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bicep file&lt;/strong&gt; - A file for declaratively deploying
Azure resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;extension resource&lt;/strong&gt; - A resource that adds to
another resource's capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a resource is a manageable item, a resource group is a logical container for resources and a resource provider is a service that supplies resources - but still no mention of a deployment. &lt;/p&gt;

&lt;p&gt;If we check a subscription's resource providers, we can see a resource provider called Microsoft.Resources, which is "registration free." This means its status—whether registered or unregistered—doesn't matter. You also can't unregister it. &lt;/p&gt;

&lt;p&gt;More clues can be found in the output of our deployment above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/x/resourceGroups/rg/providers/Microsoft.Resources/deployments/my-deployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-deployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"51e7f54e-9367-49bb-8a6e-613c00de3884"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"debugSetting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PT7.8943772S"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Incremental"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"onErrorDeployment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputResources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"greeting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Help Im stuck!"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parametersLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Resources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"providerAuthorizationConsentState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"registrationPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"registrationState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiProfiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultApiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"locationMappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"locations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deployments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"zoneMappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provisioningState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Succeeded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templateHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"439532930182033670"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templateLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-11-23T11:49:51.148630+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validatedResources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resourceGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Resources/deployments"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see for the fields ‘type’ and ‘id’ the provider is ‘providers/Microsoft.Resources’ and ‘Microsoft.Resources/deployments’ we can also see in the API reference documentation that a deployment is a resource &lt;/p&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt="image.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Okay so what happens when we use this Microsoft.Resources resources to deploy our main.bicep? So the first step is to convert our bicep main.bicep into json&lt;/p&gt;

&lt;h1&gt;
  
  
  build our arm template
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;az bicep build --file main.bicep&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next step we create a new file deployment.bicep using the Microsoft.Resources/deployments resource type&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight biml"&gt;&lt;code&gt;resource nested 'Microsoft.Resources/deployments@2021-04-01' = {
  name: 'nestedDeployment'
  properties: {
    mode: 'Incremental'
    template: loadJsonContent('main.json')
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and we can now deploy this again using az cli &lt;/p&gt;

&lt;p&gt;&lt;code&gt;az deployment group create -n my-deployment -g rg  -f deployment.bicep --debug&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;he result is we have another deployment in the portal, but this one contains all our previously nested deployments nestedDeployment, my-deployment then level 1 - level 4. So using Bicep we created a deployment to deploy our deployment which contains 4 other nested deployments. It seems that deployments are how we package requests to ARM for the provisioning of resources and this is automatically done for us by az cli. &lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and safety nets
&lt;/h2&gt;

&lt;p&gt;So now we know, a deployment is created as a resource itself—specifically a &lt;code&gt;Microsoft.Resources/deployments&lt;/code&gt; resource—whose sole job is to orchestrate the creation of other resources defined in its template property. It is the 'container' for the instructions we send to ARM, and because it is a resource, it can be nested inside other deployments just like any other Azure resource.&lt;/p&gt;

&lt;p&gt;We can provision resources which aren't wrapped in a deployment by using rest calls for example using the az rest command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  what-is-a-deployment az rest &lt;span class="nt"&gt;--method&lt;/span&gt; put &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://management.azure.com/subscriptions/xxxx/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/sa4d56frv67tbyiun?api-version=2023-01-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s1"&gt;'{
    "location": "uksouth",
    "sku": {
      "name": "Standard_LRS"
    },
    "kind": "StorageV2",
    "properties": {
      "minimumTlsVersion": "TLS1_2"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can bypass the deployment resource and ask the Microsoft.Storage/storageAccounts resource provider to provision a storage account for us, no need to get the deployments resource involved, and we can check this in the portal too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxsxm2lvws26ianpzq6af.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxsxm2lvws26ianpzq6af.png" alt=" " width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0s7ygwr1vk2o10iwmui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0s7ygwr1vk2o10iwmui.png" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Summary &lt;br&gt;
An Azure deployment is a special type of resource which acts as a wrapper for requests to ARM. it provides deployment validation, auditability and monitors the progress of a deployment. We can’t know if our template will fail or succeed until we ask the resource provider. &lt;/p&gt;

</description>
      <category>azure</category>
      <category>arm</category>
      <category>microsoft</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Least privilege as a service for Azure deployments — part 1</title>
      <dc:creator>Jonny Diamond</dc:creator>
      <pubDate>Thu, 26 Mar 2026 20:55:04 +0000</pubDate>
      <link>https://dev.to/jonny_diamond/least-privilege-as-a-service-for-azure-deployments-part-1-1e8g</link>
      <guid>https://dev.to/jonny_diamond/least-privilege-as-a-service-for-azure-deployments-part-1-1e8g</guid>
      <description>&lt;p&gt;Least privilege is a concept that garners a lot of attention from Azure practitioners. It means using the exact required permissions to perform a task, and no more. This sounds great, but in practice it is difficult to achieve and is often added to security assurances as a tick box: “we will follow least privilege.” But when it comes to Azure IaC or template deployments, how can we ever know if a deploying security principal is using the least amount of privilege?&lt;/p&gt;

&lt;p&gt;Let’s take an example of an Azure DevOps service connection, created using workload identity federation, which is the recommended approach for creating service connections with ARM roles at scopes such as subscriptions or resource groups. By default, the service principal automatically created at the scope of a subscription has the Contributor role, which in terms of ARM roles is second only to Owner (which can create role assignments). If the IaC template deployed by the service connection only has a storage account and an app service (and only ever will), then it is not following a least-privilege approach. It only needs to deploy an app service and a storage account, no more than this.&lt;/p&gt;

&lt;p&gt;Another example might be a Cloud Adoption Framework landing zone scenario. This is where a workload team has a landing zone subscription assigned to it. Then, through proper controls and governance, the workload team can deploy the Azure services they require into this landing zone. What role should the deploying principal get in this scenario? Contributor? Or Owner? Or should it be exactly what it needs to be?&lt;/p&gt;

&lt;p&gt;Tracking down the precise permissions a template requires would be fairly arduous, and maybe through a mix of guesswork and trial and error, a lesser amount of privileges could be found. Or we could use a tool that can do this for us.&lt;/p&gt;

&lt;p&gt;Azure Minimum Permission Finder (MPF) is a tool we can use to find exactly what permissions IaC templates need to deploy resources. It works by creating a test deployment at the resource group scope. This deployment is used to find the exact permissions ARM needs.&lt;/p&gt;

&lt;p&gt;To install azmpf, follow the instructions here: &lt;a href="https://github.com/Azure/mpf/blob/main/docs/installation-and-quickstart.md#quickstart--usage" rel="noopener noreferrer"&gt;https://github.com/Azure/mpf/blob/main/docs/installation-and-quickstart.md#quickstart--usage&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installation requires downloading and installing the azmpf utility and creating a dedicated MPF service principal.&lt;/p&gt;

&lt;p&gt;Let’s create a main.bicep template and parameter file (MPF doesn’t support bicepparam files) which we can point MPF at.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;param&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parStorageAccountName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;param&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parLocation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'uksouth'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;storageAccount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Microsoft.Storage/storageAccounts@&lt;/span&gt;&lt;span class="mi"&gt;2023-05-01&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parStorageAccountName&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;location:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parLocation&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;sku:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Standard_LRS'&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'StorageV&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contentVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parStorageAccountName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sa5crtvuybghkujk"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we have created these files we can invoke MPF using the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./azmpf bicep &lt;span class="nt"&gt;--bicepFilePath&lt;/span&gt; main.bicep &lt;span class="nt"&gt;--parametersFilePath&lt;/span&gt; params.parameters.json &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More details on how MPF works can be found here: &lt;a href="https://github.com/Azure/mpf/tree/main#how-it-works" rel="noopener noreferrer"&gt;https://github.com/Azure/mpf/tree/main#how-it-works&lt;/a&gt;. The long and short of it is that MPF will perform dummy deployments of a template until it gets no authorisation errors, at which point it displays the exact permissions required. Below we can see the output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;------------------------------------------------------------------------------------------------------------------------------------------
Permissions Required:
------------------------------------------------------------------------------------------------------------------------------------------
Microsoft.Resources/deployments/read
Microsoft.Resources/deployments/write
Microsoft.Storage/storageAccounts/read
Microsoft.Storage/storageAccounts/write
------------------------------------------------------------------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what now? Now that we know the permissions required, we can create a role containing these permissions and assign a service principal this role. Then, using that service principal for the Bicep deployment, we will actually be using least privilege.&lt;/p&gt;

&lt;p&gt;But there’s an important distinction to make here. MPF tells us the permissions the template needs, but the template isn’t deploying itself.&lt;/p&gt;

&lt;p&gt;Deployment tooling such as the Azure CLI has its own permission requirements on top of what the template needs. MPF works by calling the ARM deployment API directly (PUT on the deployments endpoint). But when you run &lt;em&gt;az deployment group create&lt;/em&gt;, the command performs a pre-flight validation (POST on /validate) and checks deployment status (GET on /operationStatuses). These additional API calls require additional permissions that MPF never triggers. This may vary for Azure DevOps built-in tasks, GitHub Actions, or Terraform deployments.&lt;/p&gt;

&lt;p&gt;In practice, for our &lt;em&gt;az deployment group create&lt;/em&gt; command, this means we need to add these permissions to the MPF output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.Resources/deployments/validate/action
Microsoft.Resources/deployments/operationStatuses/read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without these, the deployment will fail with AuthorizationFailed errors even though the template permissions are correct.&lt;/p&gt;

&lt;p&gt;So the full set of permissions we actually need is the MPF output plus the deployment tooling permissions. This is like two layers: the template permissions (what MPF finds) and the deployment permissions (what the tooling needs to orchestrate the deployment). Both are required for a successful deployment.&lt;/p&gt;

&lt;p&gt;So let’s create the role with the complete set of permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;​az role definition create &lt;span class="nt"&gt;--role-definition&lt;/span&gt; &lt;span class="s1"&gt;'{
 "Name": "Storage deployer",
 "Description": "Deploy a storage account.",
 "Actions": [
     "Microsoft.Resources/deployments/read",
     "Microsoft.Resources/deployments/write",
     "Microsoft.Resources/deployments/validate/action",
     "Microsoft.Resources/deployments/operationStatuses/read",
     "Microsoft.Storage/storageAccounts/read",
     "Microsoft.Storage/storageAccounts/write"
 ],
 "DataActions": [],
 "NotDataActions": [],
 "AssignableScopes": ["/subscriptions/SUB_ID"] 
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then let’s create a service principal (take note of the app id, password, and tenant id).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad sp create-for-rbac &lt;span class="nt"&gt;-n&lt;/span&gt; app-storage-deployer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, assign the role to the app using the role’s GUID as the role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az role assignment create &lt;span class="nt"&gt;--assignee&lt;/span&gt; APP_ID &lt;span class="nt"&gt;--role&lt;/span&gt; 53f635da-1c50-4f76-8ad2-ba4ba2ac21e9 &lt;span class="nt"&gt;--scope&lt;/span&gt; /subscriptions/SUB_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now sign in as the service principal and perform the deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;login&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--service-principal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;APP_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SP_PSWD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--tenant&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;TENANT_ID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;deployment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;storage-deployer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;main.bicep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;param.parameter.json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/SUB_ID/resourceGroups/rg/providers/Microsoft.Resources/deployments/storage-deployer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storage-deployer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"e32d27b5-0d6b-4c40-9103-1ccdf29d896f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"debugSetting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PT2.0994047S"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Incremental"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"onErrorDeployment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputResources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/SUB_ID/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/sa5crtvuybghkujk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rg"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uksouth"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parStorageAccountName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sa5crtvuybghkujk"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parametersLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"providerAuthorizationConsentState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"registrationPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"registrationState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiProfiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultApiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"locationMappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"locations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"uksouth"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storageAccounts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"zoneMappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provisioningState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Succeeded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templateHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3036883372851194424"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templateLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-26T20:09:01.653200+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validatedResources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resourceGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Resources/deployments"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excellent: we’ve deployed our IaC using a service principal that has our custom role. The custom role, “Storage deployer,” has only the permissions required by the template for ARM and the Microsoft.Resources/deployments permissions required for the az deployment command (to validate our template and poll its status).&lt;/p&gt;

&lt;p&gt;This is great. We have achieved least privilege. But running azmpf manually every time we want to deploy anything can be pretty time-consuming. Could we automate this, or provide it as a service in a landing zone context?&lt;/p&gt;

</description>
      <category>azure</category>
      <category>identity</category>
      <category>security</category>
      <category>deployment</category>
    </item>
    <item>
      <title>How to validate an Azure deployment?</title>
      <dc:creator>Jonny Diamond</dc:creator>
      <pubDate>Sun, 08 Mar 2026 16:57:46 +0000</pubDate>
      <link>https://dev.to/jonny_diamond/how-to-validate-an-azure-deployment-1f7</link>
      <guid>https://dev.to/jonny_diamond/how-to-validate-an-azure-deployment-1f7</guid>
      <description>&lt;p&gt;In my previous blog &lt;em&gt;What is an Azure deployment?&lt;/em&gt; we talked about what a deployment was and concluded that a deployment is a container or wrapper around requests to ARM for resources. One of the key advantages of using a deployment to deploy resources is validation or the ability to understand before a request is made whether it is valid.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why should we validate a deployment?
&lt;/h4&gt;

&lt;p&gt;Just like the practice of defence in depth with security writing IAC should have a quality in depth approach, and just like applying security in a shift left way (doing security earlier rather than later in the process) we should catch quality issues in bicep as far ‘left’ as possible because these same issues are more problematic when caught later.&lt;/p&gt;

&lt;p&gt;So what are the layers of quality check we use to make sure Bicep code is high quality, error free and secure?&lt;/p&gt;

&lt;h4&gt;
  
  
  Lint
&lt;/h4&gt;

&lt;p&gt;Linting is the process of checking code for potential errors and best practice violations. The Bicep CLI has a built-in linter which can lint files we pass to it to find syntax errors and enforce coding standards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;az bicep lint -f main.bicep
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So let’s try this out on a Bicep file to see what it comes up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;param name string
param name string
param anotherParam string = 'usefulString'

resource sa 'Microsoft.Storage/storageAccounts@2032-06-01' = { 
  name: name
  location: 'uksmouth'
  kind: 'BlobStorage'
  sku: {
    name: 'ReallyGood'
  }
  properties: {
    accessTier: 'Hot'
    allowBlobPublicAccess: false
    defaultToOAuthAuthentication: true

    minimumTlsVersion: 'TLS1_8' 
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
      ipRules: []
      virtualNetworkRules: []
    }
    supportsHttpsTrafficOnly: true
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bearing in mind VS Code has the Bicep linter built in, the az bicep lint command is useful because it means that linting can be performed in a pipeline task for example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtfujd00otwnmcmi8c4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtfujd00otwnmcmi8c4q.png" width="798" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output shows two errors and two warnings: I’ve declared the ‘name’ param twice and declared ‘anotherParam’ but not used it in the code. So through linting I’ve caught code quality errors, and importantly I can also define what linting checks are performed.&lt;/p&gt;

&lt;p&gt;We can also run the build command, compiling Bicep into ARM JSON. This uses the same linting that the lint command uses but also produces an ARM template built from the Bicep passed to the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;az bicep build -f main.bicep
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;Custom linter rules&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The VS Code extension and the Bicep CLI use default linter rules based on the arm-ttk test cases (&lt;a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter&lt;/a&gt;). Custom linter rules can be added using a bicepconfig.json file placed in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"analyzers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"adminusername-should-not-be-literal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"no-unused-params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"what-if-short-circuiting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in the above config we are specifying that the rule ‘no-unused-params’ is off, and now our linter ignores the unused param. The lint command has no output either.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5y9qay67ptgtcyk4onv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5y9qay67ptgtcyk4onv3.png" width="637" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and the lint command has no output either&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wey3pp5p6bwjjgu9cq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wey3pp5p6bwjjgu9cq5.png" width="637" height="68"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Validate&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;From the example above it’s clear that linting can check our Bicep to ensure we’re following best practice and there are no syntactical errors. If we look at the Bicep above we can see the linter is happy with all the properties we have passed — for example the linter expects the minimumTlsVersion value to be a string and as long as a string is passed it’s happy. But we’ve passed ‘TLS1_8’ and TLS 1.8 does not exist. The Bicep is syntactically correct, but it isn’t valid.&lt;/p&gt;

&lt;p&gt;In a previous post we talked about what a deployment is (&lt;a href="https://medium.com/@jonny%5C_11585/what-is-an-azure-deployment-e327e4ebc120)%E2%80%8A%E2%80%94%E2%80%8Ait%E2%80%99s" rel="noopener noreferrer"&gt;https://medium.com/@jonny\_11585/what-is-an-azure-deployment-e327e4ebc120) — it’s&lt;/a&gt; a wrapper around a request to ARM. The az deployment group command has several subcommands such as validate.&lt;/p&gt;

&lt;p&gt;Let's run the validate command on our template.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;az deployment group validate -g rg -f main.bicep -p name=name
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the output of this command shows us our location is invalid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LocationNotAvailableForResourceType"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The provided location 'uksmouth' is not available for resource type 'Microsoft.Storage/storageAccounts'. 
List of available regions for the resource type is 'eastus,eastus2,westus,westeurope,eastasia,southeastasia,japaneast,japanwest,northcentralus,southcentralus,centralus,northeurope,brazilsouth,australiaeast,australiasoutheast,southindia,centralindia,westindia,canadaeast,canadacentral,westus2,westcentralus,uksouth,ukwest,koreacentral,koreasouth,francecentral,australiacentral,southafricanorth,uaenorth,switzerlandnorth,germanywestcentral,norwayeast,westus3,jioindiawest,swedencentral,qatarcentral,polandcentral,italynorth,israelcentral,mexicocentral,spaincentral,newzealandnorth,indonesiacentral,malaysiawest,chilecentral,austriaeast,belgiumcentral'."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So lint and build missed the invalid location but validate has picked up on it. If we change the location to ‘uksouth’ our Bicep passes validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/sub_id/resourceGroups/rg/providers/Microsoft.Resources/deployments/main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6d51362d-0ed0-479c-9e00-6d0a04c7df48"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"debugSetting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PT0S"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Incremental"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"onErrorDeployment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputResources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"anotherParam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usefulString"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parametersLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"providerAuthorizationConsentState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"registrationPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"registrationState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiProfiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultApiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"locationMappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"locations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"uksouth"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storageAccounts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"zoneMappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provisioningState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Succeeded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templateHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12093554567674034375"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templateLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-08T06:52:44.305259+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validatedResources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/sub_id/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rg"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resourceGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Resources/deployments"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But even the validate command has missed many issues with the template: a resource provider API version that doesn’t exist (2032–06–01), the SKU ‘ReallyGood’, minimum TLS version ‘TLS1_8’, and the string ‘car’ in the ipRules array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;param name string
param anotherParam string = 'usefulString'

resource sa 'Microsoft.Storage/storageAccounts@2032-06-01' = {
  name: name
  location: 'uksouth'
  kind: 'BlobStorage'
  sku: {
    name: 'ReallyGood'
  }
  properties: {
    accessTier: 'Hot'
    allowBlobPublicAccess: false
    defaultToOAuthAuthentication: true

    minimumTlsVersion: 'TLS1_8'
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
      ipRules: ['car']
      virtualNetworkRules: []
    }
    supportsHttpsTrafficOnly: true
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using — debug we can see the validate command reaches out to a validate endpoint&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;az deployment group validate -g rg -f main.bicep -p name=name --debug
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;cli.azure.cli.core.sdk.policies: Request URL: ‘&lt;a href="https://management.azure.com/subscriptions/65098b6b-b71c-4374-bf9c-8a56f7d1acc4/resourcegroups/rg/providers/Microsoft.Resources/deployments/main/validate?api-version=2022-09-01'" rel="noopener noreferrer"&gt;https://management.azure.com/subscriptions/my_sub_id/resourcegroups/rg/providers/Microsoft.Resources/deployments/main/ &lt;strong&gt;validate?api-version=2022-09-01&lt;/strong&gt;'&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The REST API docs (&lt;a href="https://learn.microsoft.com/en-us/rest/api/resources/deployments/validate" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/rest/api/resources/deployments/validate&lt;/a&gt;) describe this endpoint as: “Validates whether the specified template is syntactically correct and will be accepted by Azure Resource Manager.” The key word here is “Azure Resource Manager” — not the resource provider. So the validate command doesn't actually check validity with the resource provider.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What-If&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Even the what-if command doesn’t catch these issues. What-if is fundamentally a diff tool, not a validation tool — it compares your template against the current state and tells you what would change, but it doesn’t validate whether those changes would actually succeed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  + Microsoft.Storage/storageAccounts/name [2032-06-01]

      apiVersion: "2032-06-01"
      id: "/subscriptions/sub_id/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/name"
      kind: "BlobStorage"
      location: "uksouth"
      name: "name"
      properties.accessTier: "Hot"
      properties.allowBlobPublicAccess: false
      properties.defaultToOAuthAuthentication: true
      properties.minimumTlsVersion: "TLS1_8"
      properties.networkAcls.bypass: "AzureServices"
      properties.networkAcls.defaultAction: "Deny"
      properties.networkAcls.ipRules: [
        0: "car"
      ]
      properties.supportsHttpsTrafficOnly: true
      sku.name: "ReallyGood"
      type: "Microsoft.Storage/storageAccounts"

  * Microsoft.Storage/storageAccounts/3d65fr67tvbiu

Resource changes: 1 to create, 1 to ignore.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The what-if happily echoes back all the invalid values — ‘ReallyGood’, ‘TLS1_8’, ‘car’ — without flagging any of them. It simply says “looks good to me”.&lt;/p&gt;

&lt;h4&gt;
  
  
  Validation gap
&lt;/h4&gt;

&lt;p&gt;So we have a significant gap: lint/build checks code quality and type structure, validate checks whether ARM will accept the template structure, and what-if shows what would change — but none of them validate whether the resource provider will accept the property values. The only thing that truly validates resource property values is the actual deployment itself.&lt;/p&gt;

&lt;p&gt;There are few Azure-native options to fully validate deployments before they run, but there are strategies to make sure our IaC is valid.&lt;/p&gt;

&lt;p&gt;Use Azure Verified Modules&lt;/p&gt;

&lt;p&gt;Azure Verified Modules (&lt;a href="https://azure.github.io/Azure-Verified-Modules/" rel="noopener noreferrer"&gt;https://azure.github.io/Azure-Verified-Modules/&lt;/a&gt;) are Microsoft’s official, Bicep modules that bake in best practices. They use typed parameters with constraints, so many invalid values are visible immediately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7k7sjajpvh8pan6hjli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7k7sjajpvh8pan6hjli.png" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the AVM module defines typed parameters with allowed values, passing ‘reallygood’ as a SKU name or ‘TLS1_8’ as a TLS version would be caught at build time — the Bicep compiler knows these aren’t valid values for the module’s parameters. You could also write your own modules that perform similar type validation using @allowed() decorators or user-defined types.&lt;/p&gt;

&lt;p&gt;Use PSRule&lt;/p&gt;

&lt;p&gt;PSRule for Azure (&lt;a href="https://azure.github.io/PSRule.Rules.Azure/" rel="noopener noreferrer"&gt;https://azure.github.io/PSRule.Rules.Azure/&lt;/a&gt;) is an open-source tool that validates ARM/Bicep templates against hundreds of rules covering Azure best practices. It runs offline and will catch all the issues in the above template.&lt;/p&gt;

&lt;p&gt;PSRule runs on PowerShell and is a good candidate to be integrated into CI pipelines via GitHub Actions or Azure DevOps tasks without needing a local install.&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;Validating Bicep deployments requires a layered approach because no single tool catches everything. Lint and build catch code quality issues and type errors. ARM validate catches structural problems and invalid regions. What-if shows what would change but doesn’t validate values. And the resource provider only validates property values at actual deployment time.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>iac</category>
      <category>deployment</category>
      <category>codequality</category>
    </item>
    <item>
      <title>Azure DevOps Build Agents: Microsoft-Hosted vs. Self-Hosted vs. Managed Pools</title>
      <dc:creator>Jonny Diamond</dc:creator>
      <pubDate>Thu, 19 Feb 2026 06:54:18 +0000</pubDate>
      <link>https://dev.to/jonny_diamond/azure-devops-build-agents-microsoft-hosted-vs-self-hosted-vs-managed-pools-2hi4</link>
      <guid>https://dev.to/jonny_diamond/azure-devops-build-agents-microsoft-hosted-vs-self-hosted-vs-managed-pools-2hi4</guid>
      <description>&lt;h4&gt;
  
  
  What's the difference between these options and how do they work?
&lt;/h4&gt;

&lt;h3&gt;
  
  
  Hosting options
&lt;/h3&gt;

&lt;p&gt;When we want to build code, run code or even deploy some IaC in an automated way, we need somewhere for these commands to run. This is usually just some compute to run CLI commands, and the best choice is to use pipelines. With pipelines we can integrate source control, event triggers and security scanning.&lt;/p&gt;

&lt;p&gt;With Azure DevOps we have a few options to host our pipelines, basically we have:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Microsoft hosted Agents&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Self-Hosted Agents&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;These options represent the compute that runs our jobs and tasks.&lt;/p&gt;

&lt;p&gt;An Azure DevOps pipeline is broken down into tasks, jobs and stages — each job requires an agent to run tasks, which can be inline scripts or prebuilt tasks we can consume like &lt;strong&gt;AzureCLI@2&lt;/strong&gt; which can run az cli commands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho8fn7gb9o2r7ugsd154.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho8fn7gb9o2r7ugsd154.png" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Jobs are an extremely important concept for agent hosting. Microsoft provides a free job with up to 1,800 minutes a month, but the catch is only one job can be run simultaneously. If you want to run different jobs at the same time you need to purchase this — so you purchase concurrency. This is also the case for self-hosted agents, which we will go into more detail on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcriid1zk76fpl5t7a55t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcriid1zk76fpl5t7a55t.png" width="660" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is similar to GitHub repositories which provide free GitHub runners, but only if your project is a public one where people can browse to the repo from the internet. Microsoft have recently removed the option to have public Azure DevOps projects, so the table above only really applies if you already have publicly visible ADO projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fccsbeeq0gdjn6lrymjop.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fccsbeeq0gdjn6lrymjop.png" width="454" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Microsoft hosted build agents
&lt;/h3&gt;

&lt;p&gt;How do I set this up? — Create a pipeline and specify a well-known image in the pipeline, then run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first type of agent hosting is the Microsoft-hosted agent, where Microsoft manages the infrastructure, networking and agent provisioning, and you pass the agent your job. The Microsoft documentation states that each time you run a pipeline you get a fresh virtual machine for each job in a pipeline. This is probably achieved by pre-warmed virtual machine scale set instances which are waiting for job allocation after an instance runs the job it’s then destroyed and replaced by a new instance.&lt;/p&gt;

&lt;p&gt;We can do some exploring of a Microsoft hosted image by creating a new pipeline in an azure devops project and running some commands to see what it tells us.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forensicInspection&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inspecting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;microsoft&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hosted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agents"&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;bash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;echo "env with grep"&lt;/span&gt;
      &lt;span class="s"&gt;env | sort | grep -E 'SYSTEM_|BUILD_|AGENT_|GITHUB_' || true&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;variables&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;filtered&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;grep"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;bash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;echo "env no grep"&lt;/span&gt;
      &lt;span class="s"&gt;env &lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;variables"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output will give some additional insight into the agent&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AGENT_ACCEPTTEEEULA=True
AGENT_BUILDDIRECTORY=/home/vsts/work/1
AGENT_ID=8
AGENT_ISSELFHOSTED=0
SYSTEM_ISAZUREVM=1
SYSTEM_ISDOCKERCONTAINER=0
SYSTEM_TASKINSTANCENAME=Bash1
VSTSAGENT_CONTINUE_AFTER_CANCEL_PROCESSTREEKILL_ATTEMPT=true
VSTSAGENT_DOCKER_ACTION_RETRIES=true
BUILD_ADVANCEDSECURITYENABLED=false
BUILD_QUEUEDBY=Jonny Diamond
BUILD_REPOSITORY_PROVIDER=TfsGit
BUILD_SOURCEBRANCH=refs/heads/main
BUILD_SOURCEBRANCHNAME=main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The different environment variables have prefixes which give insight into what they are for. For example, &lt;strong&gt;BUILD_xx&lt;/strong&gt; supplies information like the repository, who queued the job and whether Advanced Security is enabled (that’s probably GitHub Advanced Security). &lt;strong&gt;SYSTEM_TASKINSTANCENAME&lt;/strong&gt; tells the agent you are running bash tasks, so expect bash commands. We can also see &lt;strong&gt;AGENT_ISSELFHOSTED&lt;/strong&gt; , &lt;strong&gt;SYSTEM_ISAZUREVM&lt;/strong&gt; and &lt;strong&gt;SYSTEM_ISDOCKERCONTAINER&lt;/strong&gt;  — these tell us that we aren’t using a self-hosted agent, it’s an Azure virtual machine, and that it’s not a Docker container. These variables are provided to an agent at execution runtime; it’s a bit like saying “here is the repo, here is the code and this is what you’re doing.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Managed DevOps Pools
&lt;/h3&gt;

&lt;p&gt;How do I set this up? — Deploy network resources, Dev Center, Dev Center project, then the &lt;strong&gt;Microsoft.DevOpsInfrastructure/pools&lt;/strong&gt; resource. Add the pool in ADO.&lt;/p&gt;

&lt;p&gt;The other flavour of Microsoft-managed agents is Managed DevOps Pools. These are agents which are managed on behalf of the customer by Microsoft. The compute is actually hosted in a Microsoft subscription, with the option to VNet-inject into a subnet of your choice by delegating the subnet to the Microsoft.DevOpsInfrastructure/pools service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34cd9d2kdbymqnnxb22u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34cd9d2kdbymqnnxb22u.png" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a pipeline is triggered and Azure DevOps requests an agent, a virtual machine agent is provisioned in a Microsoft-managed subscription. The VM’s NIC is injected into your delegated subnet as below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzdsmm18gd5qy1w5444o1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzdsmm18gd5qy1w5444o1.png" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see this in our lab by viewing the connected devices in our delegated subnet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flww4t6puf21f0y3cai2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flww4t6puf21f0y3cai2t.png" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The subnet is delegated to the resource provider &lt;strong&gt;Microsoft.DevOpsInfrastructure/pools&lt;/strong&gt;. This allows virtual machine agents in Microsoft’s managed subscription to have their NICs injected into this subnet, effectively surfacing the Managed DevOps Pool resource inside our virtual network.&lt;/p&gt;

&lt;p&gt;In fact, if we click on the virtual machine in the portal under connected devices, we can see some more interesting information. We can see the Microsoft subscription it lives in and the tenant too. I am unable to list the resource as the issuer of the access token is not the correct tenant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;{&lt;br&gt;&lt;br&gt;
“code”: “InvalidAuthenticationTokenTenant”,&lt;br&gt;&lt;br&gt;
“message”: “The access token is from the wrong issuer ‘&lt;a href="https://sts.windows.net/my_tenant_id/'" rel="noopener noreferrer"&gt;https://sts.windows.net/my_tenant_id/'&lt;/a&gt;. It must match the tenant ‘&lt;a href="https://sts.windows.net/microsoft_tenant_id/'" rel="noopener noreferrer"&gt;https://sts.windows.net/microsoft_tenant_id/'&lt;/a&gt; associated with this subscription. Please use the authority (URL) ‘&lt;a href="https://login.windows.net/microsoft_tenant_id'" rel="noopener noreferrer"&gt;https://login.windows.net/microsoft_tenant_id'&lt;/a&gt; to get the token. Note, if the subscription is transferred to another tenant there is no impact to the services, but information about new tenant could take time to propagate (up to an hour). If you just transferred your subscription and see this error message, please try back later.”&lt;br&gt;&lt;br&gt;
}&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This demonstrates the VM exists in Microsoft’s subscription, but its network interface is injected into our subnet. All traffic flows through our network infrastructure, giving us full control over routing, NSGs and firewall rules. It also demonstrates that Managed DevOps Pools run on virtual machine scale sets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Hosted agents
&lt;/h3&gt;

&lt;p&gt;How do I set this up? — In ADO &amp;gt; Project Settings &amp;gt; Add Agent Pool &amp;gt; Self-Hosted. Generate the zip and put it on your compute of choice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frizwtpz3au2ldn7fnsmg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frizwtpz3au2ldn7fnsmg.png" width="473" height="1174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fforommw6j7es0xm7gamz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fforommw6j7es0xm7gamz.png" width="792" height="962"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The third option is self-hosted agents. In this scenario you host the agent yourself on your compute of choice: virtual machines, VMSS, Azure Container Apps or even AKS. The key difference between Microsoft-hosted or Managed DevOps Pools and self-hosted is that, as opposed to job sessions being provisioned on demand, self-hosted agents are instead long-running processes which poll Azure DevOps for jobs — they are always running and always listening for jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Virtual Machine Scale Sets
&lt;/h3&gt;

&lt;p&gt;How do I set this up? — Deploy a VMSS, navigate to “Add Pool” in ADO, select the VMSS.&lt;/p&gt;

&lt;p&gt;The final option is self-hosted on VMSS. This was Microsoft’s preferred choice for self-hosted agents, which allowed agents to be provisioned on an existing VMSS service to simplify deployment — no manual deploying of the agent. It’s also the inspiration behind Managed DevOps Pools. In this model, ADO manages the scaling when there are no agents waiting to pick up jobs or there are fewer agents than there should be. Like other self-hosted options, you can decide on the size and image you are running too, unlike the Microsoft-hosted agents which probably use Dasv5-size VMs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;So to host our ADO pipeline agents we have two main options: Microsoft-hosted and self-hosted. Managed DevOps Pools provide managed-on-behalf-of infrastructure injected into your network. Microsoft-hosted agents are fast to provision and useful in most cases, but they don’t have line of sight to private resources. Self-hosted agents maximise customisability but come with obvious management overhead, and you will still need to pay to run more than two jobs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fni6o70ghz8jcb84o2gai.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fni6o70ghz8jcb84o2gai.png" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devopstool</category>
      <category>azure</category>
      <category>computes</category>
      <category>azuredevops</category>
    </item>
  </channel>
</rss>
