1 min read
Azure Policy as Code: Governance at Scale
I wrote “Azure Policy as Code: Governance at Scale” to share practical, production-minded guidance on this topic.
Policy Structure
A policy definition consists of rules that evaluate resources:
{
"properties": {
"displayName": "Require HTTPS for Storage Accounts",
"description": "Ensures storage accounts only accept HTTPS traffic",
"mode": "Indexed",
"metadata": {
"category": "Storage",
"version": "1.0.0"
},
"parameters": {},
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Storage/storageAccounts"
},
{
"field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"notEquals": true
}
]
},
"then": {
"effect": "deny"
}
}
}
}
Deploying Policies with Bicep
// policies/require-https-storage.bicep
targetScope = 'subscription'
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'require-https-storage'
properties: {
displayName: 'Require HTTPS for Storage Accounts'
description: 'Storage accounts must only accept HTTPS traffic'
policyType: 'Custom'
mode: 'Indexed'
metadata: {
category: 'Storage'
version: '1.0.0'
}
parameters: {}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Storage/storageAccounts'
}
{
field: 'Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly'
notEquals: true
}
]
}
then: {
effect: 'deny'
}
}
}
}
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2021-06-01' = {
name: 'require-https-storage-assignment'
properties: {
displayName: 'Require HTTPS for Storage'
policyDefinitionId: policyDefinition.id
enforcementMode: 'Default'
}
}
Policy Initiatives (Policy Sets)
Group related policies together:
// policies/security-initiative.bicep
targetScope = 'subscription'
resource initiative 'Microsoft.Authorization/policySetDefinitions@2021-06-01' = {
name: 'security-baseline'
properties: {
displayName: 'Security Baseline'
description: 'Core security policies for all resources'
policyType: 'Custom'
metadata: {
category: 'Security'
version: '1.0.0'
}
parameters: {
allowedLocations: {
type: 'Array'
metadata: {
displayName: 'Allowed Locations'
description: 'The list of allowed locations for resources'
}
defaultValue: [
'australiaeast'
'australiasoutheast'
]
}
}
policyDefinitions: [
{
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/e765b5de-1225-4ba3-bd56-1ac6695af988' // Built-in: Allowed locations
parameters: {
listOfAllowedLocations: {
value: '[parameters(\'allowedLocations\')]'
}
}
}
{
policyDefinitionReferenceId: 'requireHttpsStorage'
policyDefinitionId: policyDefinition.id
}
{
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/404c3081-a854-4457-ae30-26a93ef643f9' // Built-in: Secure transfer for storage
}
]
}
}
Custom Policy for Tags
{
"properties": {
"displayName": "Require specific tags on resources",
"mode": "Indexed",
"parameters": {
"requiredTags": {
"type": "Array",
"metadata": {
"displayName": "Required Tags",
"description": "List of required tag names"
},
"defaultValue": ["environment", "costCenter", "owner"]
}
},
"policyRule": {
"if": {
"anyOf": [
{
"count": {
"value": "[parameters('requiredTags')]",
"name": "tagName",
"where": {
"field": "[concat('tags[', current('tagName'), ']')]",
"exists": false
}
},
"greater": 0
}
]
},
"then": {
"effect": "deny"
}
}
}
}
Testing Policies
# Test policy compliance
$testCases = @(
@{
Name = 'Compliant Storage Account'
Resource = @{
type = 'Microsoft.Storage/storageAccounts'
properties = @{
supportsHttpsTrafficOnly = $true
}
}
Expected = 'Compliant'
}
@{
Name = 'Non-Compliant Storage Account'
Resource = @{
type = 'Microsoft.Storage/storageAccounts'
properties = @{
supportsHttpsTrafficOnly = $false
}
}
Expected = 'NonCompliant'
}
)
foreach ($test in $testCases) {
$result = Test-AzPolicyEvaluation `
-PolicyDefinition $policyDefinition `
-Resource $test.Resource
if ($result.ComplianceState -eq $test.Expected) {
Write-Host "PASS: $($test.Name)" -ForegroundColor Green
} else {
Write-Host "FAIL: $($test.Name)" -ForegroundColor Red
}
}
CI/CD for Policy Deployment
# .github/workflows/policy-deployment.yml
name: Deploy Policies
on:
push:
branches: [main]
paths:
- 'policies/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy Policy Definitions
run: |
for policy in policies/definitions/*.json; do
name=$(basename "$policy" .json)
az policy definition create \
--name "$name" \
--rules "$policy" \
--subscription ${{ secrets.SUBSCRIPTION_ID }}
done
- name: Deploy Policy Assignments
run: |
az deployment sub create \
--location australiaeast \
--template-file policies/assignments.bicep
Azure Policy as Code ensures consistent, auditable governance across your entire Azure estate.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n