Azure Policy as Code: Governance im großen Maßstab durchsetzen, ohne Deployments zu blockieren
Ein umfassender Leitfaden zur Implementierung von Azure Policy as Code mit Lifecycle Management, Policy-Definitionen in Bicep und Terraform, Initiative Bundles, Exemption Management, Compliance Dashboards und Remediation Tasks.
Azure Policy ist das Leitplanken-System für Ihre Cloud-Umgebung. Es kann Tagging-Standards durchsetzen, unsichere Konfigurationen blockieren und die Einhaltung regulatorischer Anforderungen sicherstellen — alles automatisch. Aber die meisten Unternehmen implementieren es falsch: Sie erstellen Policies über das Portal, vergessen sie zu testen und wundern sich dann, warum ein kritisches Deployment am Freitagnachmittag fehlschlägt, weil jemand den Effect einer Policy von Audit auf Deny geändert hat, ohne jemanden zu informieren.
Dieser Beitrag behandelt, wie Sie Azure Policy als Code verwalten: Authoring, Testing, Deployment, Monitoring und den Umgang mit den unvermeidlichen Exemptions.
Der Policy-as-Code-Lebenszyklus
Policy Management folgt demselben Lebenszyklus wie Anwendungscode:
Jede Phase hat spezifische Praktiken:
Authoring — Schreiben Sie Policy-Definitionen in Ihrem IaC-Tool (Bicep oder Terraform). Speichern Sie sie in einem dedizierten Repository oder einem policies/-Verzeichnis in Ihrem Landing Zone Repo.
Review — Pull-Request-Review mit Genehmigung sowohl vom Platform Team als auch vom Security Team erforderlich. Policy-Änderungen betreffen jedes Team in der Organisation.
Test — Deployment in eine Test Management Group und Überprüfung, ob die Policy wie erwartet funktioniert. Bestätigung, dass sie Verstöße erkennt, ohne False Positives.
Deploy (Audit) — Deployment in Production Management Groups im Audit-Modus. Compliance 2-4 Wochen lang überwachen.
Monitoring — Compliance-Daten überprüfen. Werden legitime Ressourcen markiert? Policy-Regel anpassen oder Exemptions hinzufügen.
Enforce (Deny) — Auf Deny umstellen, sobald die Compliance über 95% liegt und alle legitimen Exemptions dokumentiert sind.
Wartung — Policies vierteljährlich überprüfen. Veraltete Policies entfernen. Regeln aktualisieren, wenn Azure-Dienste sich weiterentwickeln.
Policy-Definitionen in Bicep
Hier sind praxisnahe Policy-Definitionen für gängige Enterprise-Governance-Regeln.
Tags auf Resource Groups vorschreiben
// policies/require-tags-rg.bicep
targetScope = 'managementGroup'
@description('Liste der erforderlichen Tag-Namen')
param requiredTags array = [
'Environment'
'CostCenter'
'Owner'
'Application'
]
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
name: 'require-tags-on-resource-groups'
properties: {
displayName: 'Bestimmte Tags auf Resource Groups vorschreiben'
description: 'Stellt sicher, dass Resource Groups erforderliche Tags für Kostenmanagement und Ownership-Tracking haben'
policyType: 'Custom'
mode: 'All'
metadata: {
category: 'Tags'
version: '1.2.0'
}
parameters: {
effect: {
type: 'String'
metadata: {
displayName: 'Effect'
description: 'Deny oder Audit der Policy'
}
allowedValues: ['Audit', 'Deny']
defaultValue: 'Audit'
}
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Resources/subscriptions/resourceGroups'
}
{
anyOf: [for tag in requiredTags: {
field: 'tags[\'${tag}\']'
exists: 'false'
}]
}
]
}
then: {
effect: '[parameters(\'effect\')]'
}
}
}
}Public IP-Adressen verbieten (mit Ausnahmen)
// policies/deny-public-ip.bicep
targetScope = 'managementGroup'
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
name: 'deny-public-ip-addresses'
properties: {
displayName: 'Erstellung von Public IP-Adressen verbieten'
description: 'Verhindert die Erstellung von Public IP-Adressen außer in genehmigten Resource Groups'
policyType: 'Custom'
mode: 'All'
metadata: {
category: 'Network'
version: '2.0.0'
}
parameters: {
effect: {
type: 'String'
allowedValues: ['Audit', 'Deny']
defaultValue: 'Deny'
}
excludedResourceGroups: {
type: 'Array'
metadata: {
displayName: 'Ausgeschlossene Resource Groups'
description: 'Resource Groups, in denen Public IPs erlaubt sind (z.B. DMZ, Bastion)'
}
defaultValue: []
}
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Network/publicIPAddresses'
}
{
field: 'Microsoft.Network/publicIPAddresses/publicIPAllocationMethod'
exists: 'true'
}
{
value: '[resourceGroup().name]'
notIn: '[parameters(\'excludedResourceGroups\')]'
}
]
}
then: {
effect: '[parameters(\'effect\')]'
}
}
}
}TLS 1.2 Minimum auf Storage Accounts erzwingen
// policies/enforce-tls-storage.bicep
targetScope = 'managementGroup'
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
name: 'enforce-tls12-storage'
properties: {
displayName: 'TLS 1.2 Minimum auf Storage Accounts erzwingen'
description: 'Storage Accounts müssen TLS 1.2 oder höher verwenden'
policyType: 'Custom'
mode: 'Indexed'
metadata: {
category: 'Storage'
version: '1.0.0'
}
parameters: {
effect: {
type: 'String'
allowedValues: ['Audit', 'Deny', 'Modify']
defaultValue: 'Modify'
}
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Storage/storageAccounts'
}
{
field: 'Microsoft.Storage/storageAccounts/minimumTlsVersion'
notEquals: 'TLS1_2'
}
]
}
then: {
effect: '[parameters(\'effect\')]'
details: {
roleDefinitionIds: [
'/providers/Microsoft.Authorization/roleDefinitions/17d1049b-9a84-46fb-8f53-869881c3d3ab'
]
conflictEffect: 'audit'
operations: [
{
operation: 'addOrReplace'
field: 'Microsoft.Storage/storageAccounts/minimumTlsVersion'
value: 'TLS1_2'
}
]
}
}
}
}
}Policy-Definitionen in Terraform
Wenn Ihre Landing Zone Terraform verwendet, hier der äquivalente Ansatz:
# policies/deny_public_ip/main.tf
resource "azurerm_policy_definition" "deny_public_ip" {
name = "deny-public-ip-addresses"
display_name = "Erstellung von Public IP-Adressen verbieten"
description = "Verhindert die Erstellung von Public IP-Adressen außer in genehmigten Resource Groups"
policy_type = "Custom"
mode = "All"
management_group_id = var.management_group_id
metadata = jsonencode({
category = "Network"
version = "2.0.0"
})
parameters = jsonencode({
effect = {
type = "String"
metadata = {
displayName = "Effect"
}
allowedValues = ["Audit", "Deny"]
defaultValue = "Deny"
}
excludedResourceGroups = {
type = "Array"
metadata = {
displayName = "Ausgeschlossene Resource Groups"
}
defaultValue = []
}
})
policy_rule = jsonencode({
if = {
allOf = [
{
field = "type"
equals = "Microsoft.Network/publicIPAddresses"
},
{
field = "Microsoft.Network/publicIPAddresses/publicIPAllocationMethod"
exists = "true"
},
{
value = "[resourceGroup().name]"
notIn = "[parameters('excludedResourceGroups')]"
}
]
}
then = {
effect = "[parameters('effect')]"
}
})
}Terraform Policy Assignment
# assignments/production.tf
resource "azurerm_management_group_policy_assignment" "deny_public_ip" {
name = "deny-public-ip-prod"
management_group_id = data.azurerm_management_group.production.id
policy_definition_id = azurerm_policy_definition.deny_public_ip.id
parameters = jsonencode({
effect = { value = "Deny" }
excludedResourceGroups = { value = ["rg-dmz-prod", "rg-bastion-prod"] }
})
non_compliance_message {
content = "Public IP-Adressen sind nicht erlaubt. Verwenden Sie stattdessen Private Endpoints oder Azure Front Door. Kontaktieren Sie platform-team@company.com für Ausnahmen."
}
identity {
type = "SystemAssigned"
}
location = "westeurope"
}Initiative Bundles
Gruppieren Sie verwandte Policies in Initiatives (Policy Sets) für einfachere Zuweisung:
# initiatives/security-baseline.tf
resource "azurerm_policy_set_definition" "security_baseline" {
name = "security-baseline-initiative"
display_name = "Enterprise Security Baseline"
description = "Kern-Sicherheitspolicies, die auf alle Subscriptions angewendet werden"
policy_type = "Custom"
management_group_id = var.root_management_group_id
metadata = jsonencode({
category = "Security"
version = "3.1.0"
})
parameters = jsonencode({
storageEffect = {
type = "String"
defaultValue = "Deny"
}
networkEffect = {
type = "String"
defaultValue = "Deny"
}
})
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.enforce_tls_storage.id
parameter_values = jsonencode({
effect = { value = "[parameters('storageEffect')]" }
})
reference_id = "enforceTlsStorage"
}
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.deny_public_ip.id
parameter_values = jsonencode({
effect = { value = "[parameters('networkEffect')]" }
})
reference_id = "denyPublicIp"
}
policy_definition_reference {
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/404c3081-a854-4457-ae30-26a93ef643f9"
reference_id = "secureTransferStorage"
}
}Weisen Sie die Initiative einmal einer Management Group zu, und alle Mitglieds-Subscriptions erben die Policies.
Exemption Management
Exemptions sind unvermeidlich. Der Schlüssel ist, sie als Code mit Ablaufdatum zu verwalten:
# exemptions/payment-team-public-ip.tf
resource "azurerm_resource_policy_exemption" "payment_gateway_public_ip" {
name = "payment-gateway-public-ip-exemption"
resource_id = data.azurerm_resource_group.payment_gateway.id
policy_assignment_id = azurerm_management_group_policy_assignment.deny_public_ip.id
exemption_category = "Waiver"
description = "Payment Gateway benötigt Public IP für PCI DSS-konformen externen Endpoint. Genehmigt vom Security Team in JIRA-SEC-1234."
expires_on = "2026-06-30T00:00:00Z"
metadata = jsonencode({
approvedBy = "security-team"
ticketNumber = "JIRA-SEC-1234"
reviewDate = "2026-06-15"
justification = "PCI DSS Anforderung für Payment Processor Callback Endpoint"
})
}Governance-Regeln für Exemptions:
- Jede Exemption muss eine JIRA/ADO-Ticket-Referenz haben
- Maximale Exemption-Dauer: 6 Monate (verlängerbar mit erneutem Review)
- Exemptions erfordern die Genehmigung des Security Teams im Pull Request
- Ein monatlicher Report listet alle aktiven Exemptions auf, die sich dem Ablauf nähern
- Abgelaufene Exemptions werden automatisch von der Pipeline entfernt
# azure-pipelines.yml — Exemption Cleanup
schedules:
- cron: '0 8 * * 1'
displayName: 'Wöchentlicher Exemption Review'
branches:
include: [main]
steps:
- script: |
# Exemptions finden, die in den nächsten 14 Tagen ablaufen
az policy exemption list \
--query "[?properties.expiresOn < '$(date -d '+14 days' -u +%Y-%m-%dT%H:%M:%SZ)']" \
--output table
displayName: 'Ablaufende Exemptions melden'Compliance Dashboards
Azure Policy bietet integrierte Compliance-Ansichten, aber für Enterprise-Reporting brauchen Sie mehr:
Azure Resource Graph Queries
// Gesamtcompliance nach Management Group
PolicyResources
| where type == 'microsoft.policyinsights/policystates'
| where properties.complianceState != 'Compliant'
| summarize NonCompliantCount = count() by
ManagementGroup = tostring(properties.managementGroupIds),
PolicyName = tostring(properties.policyDefinitionName),
Category = tostring(properties.policyDefinitionCategory)
| order by NonCompliantCount desc// Nicht-konforme Ressourcen mit Details
PolicyResources
| where type == 'microsoft.policyinsights/policystates'
| where properties.complianceState == 'NonCompliant'
| project
ResourceId = tostring(properties.resourceId),
ResourceType = tostring(properties.resourceType),
PolicyName = tostring(properties.policyDefinitionName),
Subscription = tostring(properties.subscriptionId),
Timestamp = todatetime(properties.timestamp)
| order by Timestamp desc
| take 100Remediation Tasks
Policies mit Modify- oder DeployIfNotExists-Effect können nicht-konforme Ressourcen automatisch remediaten:
# remediation/tls-remediation.tf
resource "azurerm_resource_group_policy_remediation" "tls_remediation" {
name = "remediate-tls-storage"
resource_group_id = data.azurerm_resource_group.example.id
policy_assignment_id = azurerm_management_group_policy_assignment.security_baseline.id
policy_definition_reference_id = "enforceTlsStorage"
resource_discovery_mode = "ReEvaluateCompliance"
}Vorsicht bei Remediation: Testen Sie Remediation Tasks immer zuerst in einer Non-Production Subscription. Eine Modify-Policy, die das falsche Feld aktualisiert, kann laufende Services beschädigen. Verwenden Sie resource_discovery_mode = "ReEvaluateCompliance", um frische Compliance-Daten vor der Remediation zu erhalten.
CI/CD-Pipeline für Policy Deployment
# azure-pipelines.yml — Policy Deployment
trigger:
branches:
include: [main]
paths:
include: [policies/**, initiatives/**, assignments/**]
stages:
- stage: Validate
jobs:
- job: PolicyTest
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
# Alle Policy-JSON-Dateien syntaktisch validieren
for f in policies/**/policy-rule.json; do
jq empty "$f" || exit 1
done
displayName: 'Policy JSON validieren'
- script: terraform plan -target=module.policies
displayName: 'Terraform Plan — nur Policies'
- stage: DeployTest
dependsOn: Validate
jobs:
- deployment: DeployToTestMG
environment: policy-test
strategy:
runOnce:
deploy:
steps:
- script: |
terraform apply -target=module.policies -auto-approve
displayName: 'Policies in Test Management Group deployen'
- stage: DeployProd
dependsOn: DeployTest
jobs:
- deployment: DeployToProdMG
environment: policy-production
strategy:
runOnce:
deploy:
steps:
- script: |
terraform apply -target=module.policies -auto-approve
displayName: 'Policies in Production Management Groups deployen'Häufige Fallstricke
1. Mit Deny-Effect starten. Beginnen Sie immer mit Audit. Messen Sie die Auswirkungen. Dann erzwingen Sie. Eine Deny-Policy, die ohne Tests deployt wird, blockiert Deployments und erzeugt Notfall-Tickets.
2. Policies nicht versionieren. Fügen Sie ein version-Feld in die Policy-Metadata ein. Wenn Sie eine Policy aktualisieren, erhöhen Sie die Version. Dies ermöglicht es nachzuverfolgen, auf welche Version einer Policy sich ein Compliance-Finding bezieht.
3. Evaluierungsverzögerung ignorieren. Die Azure Policy-Evaluierung ist nicht sofort. Neue Ressourcen werden innerhalb von 15 Minuten evaluiert. Bestehende Ressourcen werden alle 24 Stunden neu evaluiert. Erwarten Sie keine Echtzeit-Compliance-Daten.
4. Zu viele Custom Policies. Prüfen Sie vor dem Schreiben einer Custom Policy die über 800 integrierten Policies. Viele gängige Governance-Regeln existieren bereits und werden von Microsoft gewartet.
5. Keine Non-Compliance-Messages. Eine generische Fehlermeldung "Policy hat die Anfrage abgelehnt" verschwendet Entwicklerzeit. Fügen Sie immer eine non_compliance_message ein, die erklärt, was falsch ist und wie man es behebt.
Fazit
Azure Policy as Code transformiert Governance von einer ad-hoc Portal-Aktivität in einen disziplinierten, überprüfbaren, testbaren Prozess. Die Investition in die Repository-Struktur, CI/CD-Pipeline und den Exemption-Prozess amortisiert sich beim ersten Mal, wenn eine Policy-Änderung ein ordentliches Review durchläuft, anstatt freitags um 16 Uhr in die Produktion geklickt zu werden.
Beginnen Sie mit drei bis fünf Policies, die Ihre höchsten Risiko-Fehlkonfigurationen adressieren. Deployen Sie sie im Audit-Modus. Bauen Sie das Compliance Dashboard. Erweitern Sie dann schrittweise die Abdeckung und wechseln Sie zu Deny, während sich die Teams anpassen.
Wenn Sie Hilfe bei der Gestaltung Ihres Azure Governance Frameworks oder der Implementierung von Policy as Code für Ihre Landing Zone benötigen, kontaktieren Sie uns unter mbrahim@conceptualise.de. Wir helfen Unternehmen dabei, Governance aufzubauen, die skaliert, ohne Engpässe zu erzeugen.
Themen