Azure Policy at Scale, Part 4: Hunting for the EPAC-only case
Written in a personal capacity. I work as a Microsoft Cloud Solution Architect, but this series is my own research and my own opinion. It is not an official Microsoft position.
Part 4 of Azure Policy at Scale: Native ALZ Governance vs EPAC is the part I set up in Part 1 as the attempt to prove myself wrong.
Part 2 removed the support argument in the form people usually make it. Part 3 removed the content argument and the cleanup argument. Between them they retired most of the reasons I hear for reaching for EPAC, which leaves the honest question: what is actually left?
Everything below reflects what I could verify on 28 August 2026, the day this was published. Repositories move, so check the sources rather than the dates if you are reading this later.
What I looked for, and how
I started from EPAC’s strongest capability claims rather than from a feature list, took each one seriously, and went looking for an equivalent in the standard Azure Landing Zone operating model.
Two rules made the exercise harder on myself than the usual comparison, and both were necessary.
I compared against the current Accelerator generation, not the one EPAC was originally introduced against. That distinction is not cosmetic. The ALZ PowerShell module decides which repository the Accelerator clones from a literal map of iacType to repository: terraform gets the Terraform accelerator, bicep gets the Bicep accelerator, and bicep-classic gets the classic ALZ-Bicep repository. The classic implementation is reachable only through that third value. A capability found in Azure/Enterprise-Scale, which is the legacy portal reference implementation and the upstream home of ALZ policy engineering, is not evidence about what the current Accelerator gives a customer. Applying that rule properly changed several of my own verdicts partway through the research.
I distinguished a capability from a primitive. Azure exposes a great deal that nobody has integrated. A published Bicep or Terraform AVM module, a provider resource or a possible pipeline design does not create parity with a first-class part of an operating model, any more than the existence of Microsoft.Authorization makes every policy tool equivalent.
I kept Azure Verified Modules visible as its own layer. This series is framed as a comparison of two operating models, and running it that way makes it easy to lose sight of where the native side gets its parts. The current Bicep path composes AVM pattern modules published in the Bicep public registry, and the current Terraform path consumes AVM pattern modules from the Terraform registry, so a good deal of what looks like an ALZ capability is an AVM module that ALZ happens to call. That matters twice over. It means “can ALZ do X?” frequently resolves to “AVM publishes a module for X, and no ALZ layer calls it”, which is a different answer. And because the two AVM language surfaces are maintained separately and do not advance together, a module being available in one is not evidence about the other. The exemptions section below turns on exactly that distinction.
It also changes where a search has to look, and getting this wrong is easy because the two paths are not shaped the same. AVM ships through a different channel per language. The Bicep modules come from the Bicep public registry as br/public:avm/..., built out of Azure/bicep-registry-modules. The Terraform modules are published under the Azure namespace on the Terraform registry, each backed by its own terraform-azurerm-avm-* repository. AVM maintains a full index of both, Bicep and Terraform, which is the reliable way to check whether a module for some capability exists. Microsoft publishes both catalogues, and neither has ALZ in its name.
Laid out by layer, with the current generation on each side:
| Layer | Bicep path | Terraform path |
|---|---|---|
| Accelerator orchestration and bootstrap | ALZ-PowerShell-Module and accelerator-bootstrap-modules |
ALZ-PowerShell-Module and accelerator-bootstrap-modules |
| Starter and composition | alz-bicep-accelerator |
alz-terraform-accelerator |
| AVM deployment pattern | avm/ptn/alz in bicep-registry-modules |
Azure/avm-ptn-alz/azurerm, from terraform-azurerm-avm-ptn-alz |
| Library reader | inside the Bicep pattern module | terraform-provider-alz, a provider with no Bicep counterpart |
| Policy and architecture data | Azure-Landing-Zones-Library |
Azure-Landing-Zones-Library |
The AVM deployment-pattern row is where the Azure Policy resources are declared on both paths, and it is the row with no ALZ repository name on it. The two Bicep pattern modules hold 226 references to policyDefinition, 81 to policySetDefinition and 293 to policyAssignment. The Terraform module splits the same work across main.policy_assignments.tf, main.policy_definitions.tf, main.policy_set_definitions.tf and main.policy_role_assignments.tf.
The starter row above it does not declare those resources, but it is not empty either, and on the Bicep path it supplies inputs the pattern module cannot derive. The static role definition map discussed under remediation lives there rather than in AVM. Attributing a starter decision to AVM, or an AVM behaviour to the Accelerator, gets the comparison wrong in both directions.
The library-reader row has no symmetry at all. terraform-provider-alz is a Go provider registering an alz type with an architecture data source and a metadata data source, so on the Terraform path the Library is resolved by a provider and the resources are declared by the AVM module, while the Bicep path does both inside the module. Searching only the repositories with ALZ in the name would miss the AVM row entirely and understate the Bicep path. Every absence claim below covers it, and says so where the result is interesting.
Every claim of absence below comes from a repository-wide search at a pinned commit rather than from failing to spot something while browsing. Where the exact snapshot matters to the claim I name the commit, and where it does not, the date at the top of this article is the pin.
What “EPAC-only” means here
The term needs pinning down, because used loosely it produces an argument nobody can win.
For this article:
EPAC-only does not mean the outcome is impossible without EPAC. It means EPAC provides the capability as a first-class part of its documented policy operating model, and I found no equivalent first-class capability in the current standard ALZ operating model.
You could build most of what follows yourself with the Azure APIs, a few hundred lines of PowerShell and a pipeline. That is true of nearly everything in this field and it is not a useful answer. The question is what each operating model hands you, because that is what determines who maintains it in three years.
Starting with what did not survive
If I only reported the capabilities that held up, this would be a list assembled to reach a conclusion I already had. The more interesting result is what fell off.
Policy testing is not an EPAC differentiator, and it is not native parity either
Policy testing needs going through carefully, because the answer sits between the two positions I expected to find.
The current ALZ documentation publishes a Policy Testing Framework, and it is a real methodology rather than a paragraph of advice. It describes itself as “a set of tools and scripts that can be used to test Azure Policies do what is expected and prevent breaking regressions”, built on Pester, and credits its origin: “based on the work done by @fawohlsc in this repo azure-policy-testing”. The scope is deliberate:
“For ALZ, the focus is on testing Azure Policy definitions that have a DENY effect, as these can be very disruptive to organizations if a regression is introduced”
It wants “an empty (dedicated) Azure subscription” placed in the Corp management group, so the policy under test is evaluated in isolation. A companion page publishes a complete pull-request-triggered GitHub Actions workflow, with OIDC and client-secret login paths, ending in Invoke-Pester and exit $result.FailedCount.
So far this reads like a native win. It is not, and the reason is in the page’s own instructions. The reader is told to “create a new GitHub Action workflow in the .github/workflows folder of your repository” and to “create a new Pester test file in the tests/policy folder of your repository”. The framework itself is placed somewhere else: “the testing framework is located in the ALZ repository in the tests folder”, made up of a policy folder and a utils folder. A later paragraph sends you to the same repository for the policy definition behind its worked example.
Follow that link and the trail goes cold. On 28 August 2026 aka.ms/alz/repo returns a 301 to Azure/Azure-Landing-Zones, and at commit 9c788b2d72 that repository contains no tests folder and no file matching *.Tests.ps1 anywhere in the tree. The documentation describes the layout of a framework accurately and points at a repository that does not contain it.
The code itself is real, and it is in the legacy reference implementation. Azure/Enterprise-Scale at commit 4fad64a4f7 ships 21 files matching *.Tests.ps1 under a policy test folder, two more parked with a .TODO extension, and four shared modules under a utils folder, which is exactly the policy and utils pair the page describes. It also contains Deny-MgmtPorts-From-Internet.Tests.ps1, the worked example the page walks through. The documentation places the framework in its own voice, and the pronouns matter: it “helps us improve the quality of the policies we are developing and deploying to production environments”.
Those tests do run, and tracing what runs them settles the question of whose capability this is. A workflow named test-portal.yml deploys a full landing zone into live subscriptions on a pull request, then calls .github/actions-pester/PTF-TestPolicies.ps1, which runs the Pester tests for the policy files that pull request modified, and then tears the deployment down again. It is gated on github.event.pull_request.head.repo.full_name == 'Azure/Enterprise-Scale', so it does not run for forks, and it depends on a csu-rw environment and on subscription and tenant secrets nobody outside Microsoft holds. Two details are worth noting for anyone planning to copy the pattern: the runner pins the Az module to version 10.1.0, and the exit $result.FailedCount line at the end of its RunPester function is commented out, so a failing effect test does not fail the step.
Microsoft’s own policy engineering pipeline, running in Microsoft’s own tenant, is good evidence that the ALZ policy assets are tested, and it is not a capability that lands in a customer’s repository.
I am not going to guess at the history behind the redirect. What I can say as of 28 August 2026 is that the documented route to the ALZ policy testing framework does not lead to it, and that the code matching the description sits in the legacy reference implementation rather than in anything the current Accelerator clones.
Neither current Accelerator wires any of it in. A repository-wide search for the framework’s terminology, covering Pester and its invocation, the test file and folder names the documentation specifies, the two upstream project names and the REST helper the tests call, returns no matches in either current Accelerator, in the shared bootstrap modules or in the Terraform ALZ provider. The Bicep accelerator does have a tests directory and the name is misleading: it holds two deployment templates that call br/public:avm/ptn/alz/empty against a test management group, which is a deployment smoke test. Its only test workflow runs bicep build over every Bicep file and fails on lint errors.
Now the half that surprised me. EPAC does not productise policy effect testing either. It ships nine Pester files: four covering the ALZ library sync, one covering deployment plan syntax and four covering helper functions, with a dedicated workflow that runs the sync set. Where they touch the Azure REST helper they mock it. Testing your own code is exactly what a well-run project should do, and none of it tests what your deployed policies do to a real resource. What EPAC offers a customer instead is Build-DeploymentPlans, which validates structure and diffs against live state, together with a documented practice of deploying to a live epac-dev environment on a feature branch. Plan-level and structural validation is a different category from effect testing, and both are worth having.
The verdict: the current ALZ documentation publishes the stronger documented pattern for policy effect testing, the link it gives you for the framework itself no longer reaches the code, neither current Accelerator wires it in, and EPAC does not offer it. “We need to test our policies” is not a reason to choose either one. It is a reason to build something, and ALZ has at least written down the method.
Custom policy versioning splits into two different arguments
I expected ALZ to be thin here. It is the opposite, and the finding cuts against my hypothesis in one direction and for it in another.
The custom policy guidance sets out a full semantic versioning convention for custom definitions and sets: major for rule logic and existence condition changes and for adding or removing a definition from a set, minor for new parameters with defaults and new allowed values, patch for display names, descriptions and other metadata. It defines a -preview and a -deprecated suffix with the example 1.3.2-deprecated, requires a supersededBy metadata property naming the replacement, and fixes the one thing that breaks tooling downstream:
“The
namevalue must not change in the file through deprecation or preview.”
I searched EPAC’s documentation for supersededBy and for a deprecation suffix and found neither. EPAC tells you to put a semantic version in metadata and stops there. On authoring convention the current ALZ guidance is ahead by a distance, and I would not have predicted that.
Two qualifications keep it accurate. The examples on that page carry "source": "https://github.com/Azure/Enterprise-Scale/", so the convention comes out of Microsoft’s own ALZ policy authoring practice rather than out of the Accelerator. And I found nothing that validates or enforces it anywhere in the current generation, checking every row of the layer table above. Not in either Accelerator. Not in either AVM pattern module: both return nothing for supersededBy and nothing for semantic-versioning terminology, and the places where a search for a deprecation or preview suffix does return matches are something else entirely, the Wait Module’s own description and API versions on the Bicep side, AVM’s module variable lifecycle on the Terraform side. Not in terraform-provider-alz, which resolves the Library without checking how anything in it is versioned.
The one version-aware parameter in that layer sharpens the point rather than softening it. avm/ptn/alz/empty exposes definitionVersion on a policy assignment, documented as “the policy definition version to use for the policy assignment”, and it also accepts an override of kind: 'definitionVersion'. That is Azure Policy’s own definition version selection, which is the feature the same ALZ page says supports built-ins only. The AVM layer implements the platform’s version mechanism, and nothing implements the authoring convention that exists because the platform’s mechanism does not cover custom policies. A published convention is guidance for a human, not a gate in a pipeline.
The platform sits underneath both tools in the same place. ALZ’s versioning page still states, unchanged as of 28 August 2026:
“As of March 2025, Azure Policy version only supports built-in policies and initiatives. Custom policies and initiatives are not supported but are planned.”
and clears up a confusion worth repeating, that the version property under metadata “serves no functional purpose in the Azure Policy engine”. The same page is candid about the implementation gap in ALZ’s own engines:
“Due to upstream dependencies on product SDKs, Azure Policy versioning is not yet supported in Terraform Azure Verified Modules for Platform landing zone (ALZ) and Bicep accelerator.”
Where EPAC is genuinely ahead is the update itself. ALZ’s guidance on updating custom policies says of itself that it “describes manual steps for performing the update”, and the procedure runs to finding every scope an assignment exists at through Resource Graph, deleting the assignments, deleting the definition, then redeploying and reassigning. EPAC computes the diff with Build-DeploymentPlans and applies it with Deploy-PolicyPlan.
Note also that the update path is asymmetric between the two native paths, which is the Part 3 distinction showing up again. The versioning page splits it explicitly: on Bicep “you will need to update your policies/initiatives and assignments manually”, and on Terraform you “update to the latest version of the policy library and apply the changes using your normal Terraform workflow”.
So the honest split is: authoring convention to the current ALZ guidance, enforcement to nothing I could find, platform versioning for custom definitions to nobody until Azure Policy supports it, and update automation to EPAC.
Cleanup and the reviewable plan stay retired
Part 3 covered these properly and I am not going to re-run them. Terraform state removes dropped policy resources on the Terraform path, Deployment Stacks with ActionOnUnmanage = "DeleteAll" do the equivalent on the current Bicep path, and what-if for deployment stacks went generally available on 18 August 2026, which closes the last structural gap in the plan-before-apply comparison at platform level. As of 28 August 2026 the Bicep accelerator had not adopted stack what-if in its validation stage, so the platform gap is closed and the Accelerator integration gap is open.
Where “native ALZ” splits in half
Brownfield is the clearest example of a question that has no single native answer, and treating it as native versus EPAC produces a wrong answer whichever way you resolve it.
On the current Terraform path there is a purpose-built migration workflow. It lives in the ALZ layer rather than the AVM layer, which is worth stating precisely given how easily the two get merged. The ALZ documentation carries a whole migration section with a dedicated page for management groups and policy. It uses a Microsoft-authored tool, Azure/terraform-state-importer, driven by an ALZ-specific configuration shipped in that repository, and it generates Terraform import blocks after a human triage pass. The AVM module is the target of the migration rather than the source of the tooling: the Terraform pattern module ships no adoption workflow of its own, and a search for migration terminology there does return matches, but they are AVM’s own module authoring skills, about migrating module code between AVM versions.
What it covers for policy is broader than I expected. Its Resource Graph queries discover, at management group scope: management groups, subscription placement, microsoft.authorization/policydefinitions, microsoft.authorization/policysetdefinitions, microsoft.authorization/policyassignments, custom role definitions, and the role assignments tied to policy assignment identities. It deliberately ignores policy assignments at subscription scope and the module’s own policy role assignments.
Scope it accurately, though. The section describes itself as guidance for “migrating from the CAF Enterprise Scale module to the new Azure Verified Modules (AVM) for Platform landing zone (ALZ)”. The ALZ FAQ does go one step further and says the tooling generalises: “We provide guidance and tooling for migrating from the classic Terraform module, but the same tooling and process could be applied to importing an ARM based deployment.” I have not tested that against an estate with no ALZ heritage at all, so I will quote it rather than extend it.
Two documented limits matter for the comparison. The tool maps live resources onto what the target module already declares, and anything it finds that the module does not declare surfaces as an UnusedResourceID, described as “a resource that is not used in the AVM module”, where the operator’s options are to ignore it, leaving it unmanaged, or to destroy it. There is no path that turns an undeclared existing policy assignment into code. And policy remediation role assignments do not survive the trip:
“You will see a lot of issues of type
microsoft.authorization/roleassignments. These are the role assignments created for policy remediation. These all need to be set toDestroyas there is no way we can map them and they will cause a conflict if not removed and recreated.”
On the current Bicep path I found no equivalent at either layer. A repository-wide search for brownfield, decompile, migration and adoption-workflow terminology, covering both “existing policy” and “existing policies”, returns nothing in the Bicep accelerator or in the shared bootstrap modules. The AVM layer has to be checked on its own, since the Accelerator is not where the policy resources are declared, and avm/ptn/alz returns nothing for the same terms. It does return matches for import, but those are Bicep type imports rather than resource adoption. The documentation has a Terraform migration section and no Bicep counterpart: the Bicep section holds a getting-started page and four how-tos, covering the management group hierarchy, policy assets, policy assignments and updates. Deployment Stacks offer nothing here either, since the documented model is forward-only with no adopt or import operation that I could find. The only brownfield mention on the Bicep side is in the cleanup FAQ, about which resources to retain when deleting.
For an ARM-based estate the FAQ points somewhere else again, at AzOps: “once configured, AzOps connects to your Azure tenant, scans it, and then pulls individual ARM templates into your repository in a structure that represents the four Azure scopes.” AzOps emits ARM JSON in a scope-shaped tree rather than ALZ library or archetype format, and it is a separate tool with its own accelerator rather than a step in either current operating model.
EPAC’s route is policy-shaped rather than platform-shaped. Export-AzPolicyResources “extracts existing Policies, Policy Sets, and Policy Assignments and Exemptions outputting them in EPAC format into subfolders in folder $outputFolders/Definitions”, and in a new instance “these folders can be directly copied to the Definitions folder enabling an initial transition from a pre-EPAC to EPAC environment”. It also collects ownership information into a CSV for you to analyse. Then ownedOnly holds the line while you work through it, with the documentation recommending a move to full after a transition period measured in weeks.
Three different things, then. The current Terraform path reshapes a documented prior implementation into a module it already declares, with the tooling supplied by the ALZ layer and the AVM module as its target. The current Bicep path has no documented policy adoption workflow that I could find at either layer. EPAC writes what it finds into its own definition files, so the repository comes to represent the estate rather than the estate being reshaped to fit the repository. Anyone answering “can it do brownfield?” with a yes or a no is answering a different question from the one that was asked.
Exemptions: the resource is not the point
Exemptions are the series thesis in miniature, and the example I would use if I only had one.
Start at resource level, where the argument is over on one path and not on the other. avm/ptn/authorization/policy-exemption is published in the Bicep registry, with a README titled “Policy Exemptions (All scopes)” and the line “this module deploys a Policy Exemption at a Management Group, Subscription, Resource Group, or Resource scope”. It carries four scope modules and the 2024-12-01-preview API version. “We need to deploy exemptions” is not an EPAC justification on the Bicep path and has not been for a while.
The Terraform surface is not at the same point, and this is where keeping AVM in view changes the answer. AVM publishes a module index per language, one for Bicep and one for Terraform, each listing every module with a lifecycle status. Reading them against the index data at commit ce104d84af, the Bicep pattern module above is listed Available with a first publication in December 2024. Its nearest Terraform counterpart, avm-ptn-alz-policy-exemptions, is listed Proposed, with no owner, no first publication date and no published version. The Terraform index does carry avm-ptn-policyassignment as Available since October 2024, so policy assignment has a published Terraform pattern module and policy exemption does not. Two AVM language surfaces, the same governance domain, and a module that exists on one and is a proposal on the other.
One layer up the picture changes. A repository-wide search for exemption terminology, in both Bicep and Terraform casing and including the exemption category and expiry properties, across every layer in the table above returns nothing usable on 28 August 2026. Most of those layers return no matches at all. The rest return the RBAC action string Microsoft.Authorization/policyExemptions/* in an example role definition, and in the Bicep accelerator a handful of module description strings listing “policy exemptions” among what a module deploys, with no matching parameter behind them. The AVM deployment layer has to be checked separately, because the starter repository is not where the policy resources are declared, and avm/ptn/alz returns the same RBAC action string plus one parameter that goes out of its way to say it is not an exemption. managementGroupExcludedPolicyAssignments takes assignment names “to prevent from being assigned (created/updated from a CRUD perspective) at all (not a policy exclusion (notScope) or exemption)”. The ALZ Library’s archetype schema defines four arrays, for policy assignments, policy definitions, policy set definitions and role definitions, and none for exemptions. Nothing in either Accelerator or either AVM pattern module references the AVM exemption module, so the building block exists and nothing in the operating model calls it.
The ALZ guidance layer does address exemptions, once, in the FAQ, and the advice is good. When an enforced ALZ policy blocks a managed service, it suggests you “create a temporary policy exemption where you can exclude a specific scope of resources to be excluded from the policy assignment for the duration of deployment (recommended)”, and it takes the trouble to distinguish an exemption from an exclusion and from an exception. It tells an operator when to reach for one. It does not make the exemption an artefact that anyone later reviews, dates or removes.
EPAC treats exemptions as something you operate. They are a first-class asset type under Definitions/policyExemptions/<pacEnvironment>/, in JSON or CSV, against a published schema. Around the declaration sit the parts that make it a lifecycle:
Scripts/Helpers/Confirm-ActiveAzExemptions.ps1sorts every exemption into active, expiring, expired and orphaned, with a computedexpiresInDays.cleanupObsoleteExemptionsis a documented desired-state flag: “when set totrue, EPAC will delete Policy Exemptions that reference Policy Assignments no longer present in the environment. This prevents stale exemptions from accumulating over time as assignments are removed.” It defaults tofalse.Get-AzExemptionsround-trips the live estate back into committed files. From the operational scripts documentation: “pass-ExportForEpacto additionally produce anepac-exemptions.<ext>file that is already stripped of Azure-only fields and can be committed directly into the EPAC repo.”- Exemptions have their own release path.
Build-DeploymentPlanstakesBuildExemptionsOnly, which the documentation says is “useful to fast-track Exemption when utilizing Release Flow”,Deploy-PolicyPlantakesSkipExemptions, and the starter kit ships exemption-only pipelines triggered from areleases-exemptions-only/*branch. - The desired-state documentation covers splitting exemptions into their own repository under the same
pacOwnerId, so that the people who grant exemptions need not be the people who own the definitions.
Every one of those answers a question that arrives months after the exemption was created. Who approved it, when does it lapse, is it still attached to an assignment that exists, and who is allowed to grant the next one.
The differentiator is not the exemption resource. It is the exemption process, and I found no equivalent in either current native path.
Remediation: three questions that get answered as one
Start with the platform behaviour, because it decides what the question means. Microsoft Learn on the deployIfNotExists effect, checked 28 August 2026:
“During an evaluation cycle, policy definitions with a DeployIfNotExists effect that match resources are marked as non-compliant, but no action is taken on that resource.”
The next sentence points at the thing somebody has to create: “existing non-compliant resources can be remediated with a remediation task”. So having the identity and the permissions in place does not remediate anything that already exists. It only means the remediation would succeed if someone started it.
Three separable things get bundled into “remediation”, and the current answers differ for each.
Managed identity creation is at parity. EPAC applies managedIdentityLocation; the ALZ Library assignment data carries "identity": {"type": "SystemAssigned"}; the AVM Terraform module derives the identity and outputs policy_assignment_identity_ids; the AVM Bicep modules take an identity per assignment. All three get you an identity.
Role assignments are not at parity between the two native paths, and this was the finding that changed my mind about how tidy any of this is. The difference sits in the AVM layer on both sides, which is worth stating plainly because it is not an Accelerator difference and it is not a Library difference. The Terraform pattern module has a dedicated main.policy_role_assignments.tf creating azapi_resource "policy_role_assignments", carrying the description “Created by ALZ Terraform provider. Assignment required for Azure Policy.” It derives what to assign from the policy definition’s assignPermissions metadata, and exposes variables to set and unset that metadata per definition and parameter, so a custom policy you add can participate. The Bicep pattern module contains no occurrence of assignPermissions at all. It takes an explicit roleDefinitionIds array per assignment, and since the ALZ Library supplies roleDefinitionIds in none of the 80 platform ALZ policy assignment files, the composition layer has to provide them. It does so by declaring a static map in each of seven management group templates, above a comment that appears twelve times across the repository:
“Role definitions are hardcoded variables and cannot be overridden.”
Both paths create the role assignments the shipped ALZ policy set needs. Only one of them generalises to a custom policy you write yourself without editing the accelerator’s templates.
Task creation, scheduling and failure handling are EPAC’s. New-AzRemediationTasks is documented as creating “remediation tasks for all non-compliant resources for EPAC environments”, with -OnlyCheckManagedAssignments to stay inside what the repository owns and -OnlyDefaultEnforcementMode to skip assignments that are not enforcing. It submits in bulk, polls for completion, and hands failures to New-AzureDevOpsBug or New-GitHubIssue so that a failed remediation becomes a work item rather than a line in a log. The starter kit ships a remediation pipeline for Azure DevOps and GitHub Actions on a daily schedule.
A repository-wide search for remediation terminology, catching both remediate and remediation, returns no matches in either current Accelerator, the shared bootstrap modules, the Terraform ALZ provider or the ALZ PowerShell module, and none in either AVM pattern module, which is the layer it would be easiest to overlook. The nearest thing there is a single Microsoft.PolicyInsights/* RBAC action, in a role definition fixture under the empty module’s end-to-end test library. The current ALZ guidance does discuss remediation and sends the reader to Microsoft Learn for it. The azurerm provider exposes azurerm_management_group_policy_remediation and its siblings, and nothing in the current Terraform path uses them, which puts them in the same category as the AVM exemption module: a building block nobody has wired in.
If your DeployIfNotExists estate is small enough that somebody clicks “remediate” in the portal after each policy refresh, none of this matters. If it is not, the identities and role assignments both paths give you are the easy part of the problem.
Reporting, and the tool that sits beside the Accelerator rather than inside it
EPAC ships two reporting capabilities. Export-NonComplianceReports writes six CSV files, cutting the same non-compliance data by policy, by resource and by assignment at two levels of detail, filterable by definition, set, assignment and effect, and restrictable to assignments the repository owns. Build-PolicyDocumentation generates Markdown, CSV and JSONC from what is actually deployed, and can push the Markdown into an Azure DevOps wiki.
Its distinctive output is the one I have not seen elsewhere. The generated documentation carries a column per environment category, listing “the highest enforcement level across the Policy Sets assigned in this environment category”, so the question “is this policy auditing in development and denying in production, and through which assignment?” has a generated answer rather than an investigation attached to it.
Neither current Accelerator path nor its AVM deployment layer packages an equivalent. Re-running the reporting vocabulary across every layer in the table above, each match is either a comment about GitHub’s code scanning dashboard in a Scorecard workflow, or Resource Graph used for teardown in Remove-PlatformLandingZone.ps1, where it finds role assignments in order to delete them. The AVM layer is no different on either language: the Bicep pattern module has no workbook and no Resource Graph query, and every match it returns for compliance is a nonComplianceMessages entry, which is the text a deny shows a user rather than a report anyone reads, while the Terraform pattern module returns nothing for either term. I found no policy compliance workbook in any of them.
The alternative is real and it needs representing at the right layer. The current ALZ policy documentation recommends the Azure Governance Visualizer by name in four separate places, including the policy FAQ, which says to run it “against your Azure estate … and get a tailored report that includes unassigned policies”. It lives in the Azure organisation, it has an accelerator of its own, and it is listed in the Cloud Adoption Framework’s tools page under Govern. Calling it third-party would be wrong. Calling it Accelerator parity would also be wrong, because neither Accelerator ships it, generates it or invokes it. It also answers a different question: it reports the estate as it is, where EPAC’s documentation generation answers what an assignment does per environment tier and why.
Its own README settles what it is, and the answer bears directly on Part 2. Under a heading of “Closing Note”, at commit a1154f0641, it states: “Please note that while being developed by a Microsoft employee, Azure Governance Visualizer is not a Microsoft service or product. Azure Governance Visualizer is a personal/community driven project, there are no implicit or explicit obligations related to this project, it is provided ‘as is’ with no warranties and confer no rights.” Its SUPPORT.md routes users to GitHub Issues and suggests raising them on the upstream repository instead, and documents no Microsoft support path. Part 2 argued that support is a property of individual layers rather than of a stack. The reporting layer is a clean example: current ALZ documentation recommends a tool that disclaims being a Microsoft product in its own words, in the Azure organisation, on the CAF tools page. None of that is a criticism of the tool, which is well regarded and widely used. It is a reason to read the recommendation as guidance rather than as a supported component.
So the accurate statement is narrow. EPAC includes reporting and documentation inside its policy operating model. The current ALZ operating model points you at Azure-native views and at a community tool hosted in the Azure GitHub organisation and recommended by ALZ and CAF guidance, and leaves the assembly to you.
Multiple tenants and multiple owners
Four things get bundled here, and they separate cleanly. One of them I had to drop.
Multiple deployment environments is parity. EPAC has pacEnvironments, both Accelerators get CI/CD environments from the bootstrap, and Terraform has workspaces and backends. It does not belong on a differentiator list and I had it there.
Multiple tenants from one repository is EPAC. Each pacEnvironments entry carries its own tenantId, described in the settings documentation as enabling multi-tenant scenarios, and epacCloudEnvironments filters a file to AzureCloud, AzureChinaCloud or AzureUSGovernment, which exists because “not all policies, policy sets, and/or assignments will function in all tenants”. A repository-wide search for multi-tenant terminology, in both Bicep and Terraform casing, and for Lighthouse, returns no matches across every layer in the table above, on both languages, and the generated bootstrap pipelines authenticate with a single AZURE_TENANT_ID. The current ALZ documentation does point at Cloud Adoption Framework guidance for organisations with several Entra tenants, which is guidance rather than an Accelerator capability.
Multiple independent owners at one scope is EPAC, and it is the one I find architecturally most interesting. pacOwnerId is the mechanism, and the documented use case is explicit: “in a shared responsibility model multiple teams manage the same tenant(s) at the same scope”, with the note that “other Policy as Code solutions can also participate if the solution sets metadata.pacOwnerId”. Microsoft Defender for Cloud gets the same treatment through reserved owner values. A repository-wide search for pacOwnerId and for ownership terminology across every layer in the table above returns eleven matches, and every one is incidental: ten are contributor documentation about who owns which file or which module, and one is a substring inside a policy definition JSON.
I would not call that a failing. Neither Accelerator is trying to coexist with an independent peer at the same scope, so there is no reason for it to have an ownership protocol. What it means is that I found no documented native answer to the requirement, and a pipeline you can instantiate twice is not a multi-owner model. The question is not whether an assignment can be deployed. It is how several teams hold authority over one policy estate without deleting each other’s work.
Lighthouse is EPAC, with limits EPAC publishes itself. managedTenantId is a documented pacEnvironments field for an environment in a Lighthouse managed tenant, with a worked example. EPAC also says “EPAC does not yet support all Lighthouse scenarios”, and records a scaling constraint that a managed service provider should read before planning around it:
“Because Lighthouse does not allow grouping of ‘cast’ Lighthouse subscriptions in the managing tenant, and does not allow for management groups to be cast, each unique subscription must be a unique pacEnvironment.”
One pacEnvironment per managed subscription is a real cost at any scale, and presenting Lighthouse support as complete would misrepresent what EPAC says about it.
If you run policy across tenants you do not own, or several teams hold authority over the same scope, this group is where the argument for a separate policy operating model is strongest. If you run one tenant with one platform team, it is where the argument is weakest, because you are buying a coordination protocol for a coordination problem you do not have.
Release flow: a difference in packaging rather than a clean exclusive
I had this filed as EPAC-only, and after looking at the generated pipelines properly I do not think that survives.
EPAC ships a policy-first release model rather than describing one. Two branching strategies are documented with pipeline files behind them for Azure DevOps, GitHub Actions and GitLab: GitHub Flow for “simple EPAC deployments”, and Release Flow for when you “wish to utilize multiple additional environments, and/or deploy to those environments in a staged/ring-based fashion”. Ring promotion is a branch trigger on releases-prod/*, the exemption fast-track is another on releases-exemptions-only/*, and New-PipelinesFromStarterKit generates the set for you. The rollback guidance is one line and deserves quoting exactly rather than inflating: “keep n-1 releases-prod branches to allow for quick rollback in case of issues.” I searched for a rollback command and found none, so it is a re-run of the previous release branch rather than a feature.
The native side is not simply absent. The generated CD workflow in the shared bootstrap modules renders one boolean input per deployable component, each defaulting to true, and gates both the what-if job and the deploy job on it. The Bicep accelerator’s component list is per management group and governance-shaped, running through entries like governance-int-root, governance-platform and governance-landingzones-corp. A customer can dispatch a governance-only run today by deselecting the rest, which is manual but real, and my first pass missed it.
What is absent is everything above that. The CI workflow triggers on any pull request to main with no path filters, there is one plan environment and one apply environment, and a search of the 48 generated pipeline YAML files for policy, rollback and revert returns nothing. Policy is not a named concept in the generated pipelines, only a folder that one of the governance scripts happens to deploy.
The defensible sentence names the packaging, not the idea. The current native path can deploy governance components selectively. EPAC goes further by treating policy as the thing the release system is built around, and ships that as working files rather than as a design you assemble.
What the capabilities cost
A list of differentiators without the price attached is a sales document. EPAC’s own documentation is the fairest source for the other side of the ledger, so most of this comes from there.
Toolchain. The documented prerequisites are PowerShell 7.4 or higher, the Az PowerShell module and the EnterprisePolicyAsCode module. The starter kit pipelines run on ubuntu-latest, so there is no Windows agent requirement to worry about, but a team standardised on Bicep or Terraform is taking on a PowerShell-native pipeline and the skills to maintain it.
The development hierarchy. EPAC calls a separate epac-dev management group hierarchy “strongly recommended” and says it “typically mirrors your production Management Group structure”, which is infrastructure and operational overhead in its own right. It remains a recommendation rather than a requirement, and ownedOnly with pacOwnerId partitioning is documented as an alternative during transition and in shared responsibility setups.
Desired-state authority. The capability and the risk are the same feature. EPAC’s front page carries the caution: it “takes possession of all Policy Resources at the deploymentRootScope and its children” and “will delete any Policy resources not defined in the EPAC repo”. The full rule is narrower than the shorthand, and stating it without the qualifications is unfair: full deletes policy resources that carry no pacOwnerId and preserves those carrying a different one, ownedOnly deletes nothing it did not declare, and excludedScopes, excludedPolicyDefinitions, excludedPolicySetDefinitions and excludedPolicyAssignments narrow it further with wildcard support. A deploymentRootScope pointed at the wrong management group is still the largest single-keystroke risk in this comparison.
Breaking changes are a normal part of the deal. EPAC documents them rather than hiding them, and three are worth knowing before adopting: inheritedDefinitionsScopes was removed in v10.0.0, so cross-scope definition inheritance is now replicated by copying files between repositories; resource group assignments became managed in v10.0.0 with includeResourceGroups removed; and v11.0.0 changed Lighthouse role assignment handling in ways EPAC says will break existing configurations. Customer usage attribution telemetry has been on since v8.0.0 with a documented opt-out, which matters more to some readers than to others.
And EPAC tells you when not to use it, which is the single fairest thing in this whole comparison. Its documentation says it “is designed for medium and large organizations with a larger number of Policies, Policy Sets and Assignments and/or complex deployment scenarios, such as, multiple tenants, multiple teams managing Policies”, and then:
“If your DevOps (CI/CD) maturity is lower, Azure Landing Zones direct implementation of Policies might be a better choice.”
A project that routes part of its addressable audience elsewhere is worth reading carefully in both directions.
Where the hunt ended up
The compact version, with the two native paths kept apart wherever they differ.
| Capability | Result as of 28 August 2026 |
|---|---|
| Scope-wide authority over policy resources it did not create | EPAC only, through full, constrained by pacOwnerId ownership semantics and exclusions. Both native paths clean up their own prior output and neither goes looking |
| Exemption lifecycle | EPAC only. An AVM Bicep pattern module is published and its Terraform counterpart is still a proposal; neither Accelerator nor its AVM deployment layer integrates exemptions on either path |
| Remediation task creation and scheduling | EPAC only, against both native paths |
| Multi-tenant from one repository | EPAC only, against both native paths |
| Multi-owner protocol at one scope | EPAC only, against both native paths |
| Azure Lighthouse | EPAC only, with the scaling limit EPAC documents |
| Brownfield policy adoption | Asymmetric. Purpose-built ALZ-layer migration tooling on current Terraform, none found on current Bicep at either layer, policy-specific export in EPAC |
| Policy identity role assignments | Asymmetric. Derived and extensible on current Terraform, hardcoded and documented as not overridable on current Bicep |
| Custom policy update automation | Asymmetric. EPAC diffs and applies; current Bicep is a documented manual procedure; current Terraform is a library version bump and an apply |
| Policy-specific release train | Partial. EPAC packages ring promotion and an exemption fast-track; both Accelerators allow a governance-only manual dispatch |
| Packaged compliance reporting | EPAC packages it. Neither Accelerator does, and current ALZ documentation recommends the Azure Governance Visualizer, which disclaims being a Microsoft product |
| Policy effect testing | Neither operating model productises it. Current ALZ documentation publishes the stronger pattern, though its link to the framework no longer reaches the code |
| Custom policy versioning convention | Current ALZ guidance publishes it, neither Accelerator enforces it, EPAC does not publish one |
| Managed identity creation, cleanup of own resources, multiple environments | Neutralised. Addressed equivalently enough on all three |
Six rows are clean EPAC differentiators. Three split between the two native paths, so there is no single native answer to give. One is a difference in packaging rather than an exclusive, and one more is packaged by EPAC alone with a recommended community tool on the other side. Two are documented ALZ patterns that neither operating model has productised, and the last row retires three arguments outright.
Reading the table by layer rather than by row is worth doing as well. Several of the rows that look like ALZ gaps are AVM questions underneath: a pattern module exists and nothing in the operating model calls it, or a pattern module exists in one language and is a proposal in the other. AVM moves on its own cadence and does not advance both surfaces together, so any snapshot of this comparison is partly a snapshot of where the AVM catalogue happened to be on the day.
At the end of Part 3 I said every item on the remaining list would sit in the same layer. That holds for the six clean differentiators, all of which are policy operations rather than policy deployment. It was too tidy a promise for the list as a whole, because three of the more interesting cases split along the Bicep and Terraform line instead.
What I could not establish
Phrased as absences of documentation and unfinished checks, not as facts about the world:
- I could not establish whether the Terraform state importer works against a policy estate with no CAF Enterprise Scale heritage at all. The ALZ FAQ says the same tooling and process could be applied to an ARM based deployment, and the migration section documents the CAF Enterprise Scale case. I have not tested the wider claim.
- I found no documented way to make a Deployment Stack adopt a pre-existing resource, having checked the deployment stacks documentation and its known issues page and nothing else.
- I found no documented route by which the ALZ policy testing framework reaches a customer repository.
test-portal.ymlruns it inside Microsoft’s own tenant, aka.ms/alz/repo no longer resolves to the repository holding it, and neither Accelerator clones it. What a customer is expected to do with the published method, beyond reimplementing it, I could not see. - I have not found a published native-path implementation of the six capabilities marked EPAC only above. If one exists, several rows in that table move.
Next
Part 5 is The verdict, and it owns the decision. Part 4’s job was the evidence, and the evidence says something narrower than either side of this discussion usually claims.
The list of genuine EPAC differentiators is shorter than feature comparisons imply, and it got shorter while I was checking it. Several arguments I have heard recently describe an ALZ that the Accelerator no longer clones. Several native answers turn out to depend on whether you deploy with Bicep or with Terraform, which makes “native ALZ can do that” an incomplete sentence. What remained after all of that is not about deploying Policy resources at all: it is exemptions as a process, remediation as an orchestrated task, reporting as an output, and ownership as a protocol between teams.
If all you need is to define, assign, version and declaratively deploy Azure Policy, the hunt did not turn up a compelling capability that only EPAC has. The cases that survived start when an organisation needs to operate policy itself as a governed product, and whether that describes you is the question Part 5 tries to make answerable.
Revision log
Unchanged since publication on 28 Aug 2026. Anything I get wrong later lands here.