Managing Azure governance across multiple subscriptions can quickly become overwhelming. As environments grow, enforcing policies manually leads to inconsistencies, delays, and operational risk. This blog shares a practical, hands-on perspective — not just what EPAC is, but how we implemented it, challenges faced, and lessons learned.
đź§© The Challenge: Governance at Scale
Managing Azure environments manually introduces:
- ❌ Policy drift across subscriptions
- ❌ Inconsistent naming conventions
- ❌ Delays in compliance enforcement
- ❌ Human errors in deployments
🔍 Insight: Governance gaps are often not due to lack of policies—but lack of automation.
Solution Overview: EPAC
Enterprise Policy as Code (EPAC) to bring governance into the DevOps workflow.
EPAC helped us:
- Treat policies like code
- Automate deployments
- Standardize governance
- Maintain audit history
Architecture Overview
Our EPAC setup included:
- Management Groups hierarchy for governance scope
- Policy Definitions & Initiatives (JSON/Bicep)
- Azure DevOps Pipeline for deployment
- Managed Identity for secure execution
Flow Explanation
- Azure DevOps Pipeline
- Triggers CI/CD process
- Executes deployment scripts
- Authenticates securely using Managed Identity
- EPAC Framework
- Stores policies as code
- Enables version control and validation
- Acts as the central governance engine
- Azure Policy Engine
- Evaluates resources against defined policies
- Enforces compliance automatically
- Target Environments
- Policies applied across:
- Management Groups
- Subscriptions
- Resource Groups
- Policies applied across:
Why Policy-as-Code Matters
âś… Standardized governance across environments
âś… Faster onboarding of subscriptions
âś… Improved audit readiness
âś… Repeatable and reliable policy deployment
âś… Seamless DevOps integration
Security & Compliance Benefits
- Enforces least privilege access
- Prevents misconfigured deployments
- Supports continuous compliance
- Aligns with standards like ISO 27001
Implementation Walkthrough
âś… Step 1: Structuring Policy Repository
This structure ensured:
- Clear separation of concerns
- Reusability
- Easy onboarding for new contributors
âś… Step 2: Defining Policies
We created custom policies and reused built-in ones.
Example: Restrict allowed regions
{
"if": {
"field": "location",
"notIn": ["eastus", "westeurope"]
},
"then": {
"effect": "deny"
}
}
âś… Step 3: Creating Initiatives
Instead of assigning individual policies, we grouped them into initiatives:
- Security baseline
- Tagging compliance
- Cost optimization
👉 This reduced duplication and simplified assignments.
âś… Step 4: Pipeline Automation
We built an Azure DevOps pipeline to:
- Validate policy templates
- Deploy definitions to management groups
- Assign initiatives automatically
Example pipeline flow:
This is example pipeline structure for deploying epac policies targeted to single environment
parameters:
- name: forceDeployment
displayName: 'Force deployment (ignore change detection)'
type: boolean
default: false
- name: clearAgentCache
displayName: 'Clear agent container cache (recommended for troubleshooting)'
type: boolean
default: true
variables:
# Pipeline per il deployment delle Policy in ambiente Canary/Test
PAC_OUTPUT_FOLDER: ./Output
PAC_DEFINITIONS_FOLDER: ./Definitions
# Service connection per l'ambiente di test/canary
serviceConnection: "SC-EPAC-CONTRIBUTOR-TST-001"
# Environment selector per canary
pacEnvironmentSelector: canary
# Trigger: deploy solo manualmente o da branch specifici
trigger: none
# PR trigger per validazione
pr:
branches:
include:
- main
- feature/*
paths:
include:
- src/IaC/Infrastructure/epac/Definitions/*
pool:
name: "TST-AgentPool-01"
stages:
- stage: Plan
displayName: "Plan Canary Environment"
jobs:
- job: Plan
displayName: "Generate Deployment Plan"
steps:
- template: templates/plan.yml
parameters:
serviceConnection: $(serviceConnection)
pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }}
- stage: Deploy
displayName: "Deploy to Canary (Audit Mode)"
dependsOn: Plan
condition: and(not(failed()), not(canceled()), or(eq('${{ parameters.forceDeployment }}', 'true'), and(eq('${{ parameters.forceDeployment }}', 'false'), or(eq(dependencies.Plan.outputs['Plan.Plan.deployPolicyChanges'], 'yes'), eq(dependencies.Plan.outputs['Plan.Plan.deployRoleChanges'], 'yes')))))
variables:
PAC_INPUT_FOLDER: "$(Pipeline.Workspace)/plans-${{ variables.pacEnvironmentSelector }}"
localDeployPolicyChanges: $[stageDependencies.Plan.Plan.outputs['Plan.deployPolicyChanges']]
localDeployRoleChanges: $[stageDependencies.Plan.Plan.outputs['Plan.deployRoleChanges']]
jobs:
- deployment: DeployPolicy
displayName: "Deploy Policy Changes (Audit Mode)"
environment: PAC-CANARY
condition: and(not(failed()), not(canceled()), or(eq('${{ parameters.forceDeployment }}', 'true'), and(eq('${{ parameters.forceDeployment }}', 'false'), eq(variables.localDeployPolicyChanges, 'yes'))))
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-policy.yml
parameters:
serviceConnection: $(serviceConnection)
pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }}
forceDeployment: ${{ parameters.forceDeployment }}
- deployment: DeployRoles
displayName: "Deploy Role Assignments"
dependsOn: DeployPolicy
environment: PAC-CANARY
condition: and(not(failed()), not(canceled()), eq(variables.localDeployRoleChanges, 'yes')) # Riabilitato per AMBA managed identity
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy-roles.yml
parameters:
serviceConnection: $(serviceConnection)
pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }}
# Stage opzionale per validazione post-deployment
- stage: Validate
displayName: "Validate Canary Deployment"
dependsOn: Deploy
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
jobs:
- job: ValidateCompliance
displayName: "Validate Policy Compliance"
steps:
- task: PowerShell@2
displayName: "Check Policy Compliance Status"
inputs:
targetType: 'inline'
script: |
Write-Host "##[section]Canary deployment completed successfully"
Write-Host "##[warning]Remember: All policies are in AUDIT mode - monitor compliance dashboard"
Write-Host "##[task.complete result=Succeeded;]Canary validation completed"
âś… Step 5: Secure Deployment using Managed Identity
We used:
- System-assigned Managed Identity
- RBAC roles (Policy Contributor / Reader)
âś… Benefit:
- No secrets in pipeline
- Improved security posture
âś… Step 6: Policy Assignment at Scale
Policies were assigned at:
- Root Management Group
- Subscription level (when needed)
This ensured:
- Consistent enforcement
- Centralized control
Real Use Cases Implemented
Using EPAC, we solved real scenarios:
- 🔹 Enforcing naming conventions
- 🔹 Ensuring mandatory resource tagging
- 🔹 Restricting deployment regions
- 🔹 Enforcing backup policies on disks
- 🔹 Preventing creation of non-compliant resources
Best Practices
- Start with baseline policies first
- Use initiatives instead of individual assignments
- Enable PR-based approvals
- Always test policies in lower environments
- Maintain clear documentation
Conclusion
Implementing EPAC transformed our governance model from manual and reactive → automated and proactive.
For teams managing complex Azure environments, EPAC provides:
- Scalability
- Consistency
- Security
If you are still managing policies manually, this is the right time to: 👉 Move to Policy as Code