Validate Azure Policy compliance inside a GitHub Actions pipeline before or after deploying infrastructure,

To validate Azure Policy compliance inside a GitHub Actions pipeline before or after deploying infrastructure, you can integrate static IaC linting (pre-deployment) alongside the Azure Policy Compliance State API (post-deployment validation).

This is a post-deployment compliance gate: the pipeline deploys, asks Azure Policy to evaluate, then passes or fails based on the result.

Points to make in an interview:

  • It complements Deny, it doesn’t replace it. Deny blocks a bad deployment at ARM. This gate catches what Deny can’t: Audit and AuditIfNotExists findings, and DeployIfNotExists conditions that remain unmet.
  • Evaluation is not instant. Compliance data is eventually consistent, so the pipeline should trigger an on-demand scan (az policy state trigger-scan) and then poll with a timeout. Without a timeout, a stuck evaluation hangs the pipeline. Without a trigger, you may read stale results and wrongly pass.
  • Scope the check to what was deployed. Query compliance for the target resource group or the specific resources, not the whole subscription, or unrelated legacy non-compliance will fail every run.
  • The report is part of the control. Output the policy name, resource ID, and reason so developers can fix it without a ticket. Failing with no explanation drives people to ask for exemptions.
  • Decide the failure policy up front. Hard-fail on high-severity or security baselines, warn on low-severity items, and route exceptions through time-boxed policy exemptions, not pipeline overrides.

Order matters for safety. If the deployment itself creates the non-compliant resource, the gate fails after it exists. For sensitive controls, pair this with what-if and IaC scanning (Checkov) before the deploy step, so you fail early

Architecture Flow

GitHub Actions Workflow Example

This pipeline performs three actions:

  1. Logs in via OIDC (OpenID Connect) using short-lived credentials.
  2. Triggers an on-demand Azure Policy evaluation scan on the target Resource Group.
  3. Polls the Azure Policy Insights API to verify that zero resources are in a NonCompliant state. If non-compliant resources exist, it fails the workflow and prints a structured summary report.

YAML

name: Azure Policy Compliance Check
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
id-token: write
contents: read
jobs:
validate-policy-compliance:
name: Validate Azure Policy Compliance
runs-on: ubuntu-latest
env:
RESOURCE_GROUP: 'rg-devsecops-prod'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Azure Login via OIDC
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Trigger On-Demand Policy Evaluation Scan
shell: pwsh
run: |
Write-Host "Triggering Azure Policy scan for Resource Group: ${{ env.RESOURCE_GROUP }}"
$job = Start-AzPolicyComplianceScan -ResourceGroupName "${{ env.RESOURCE_GROUP }}" -AsJob
$job | Wait-Job
Write-Host "Policy evaluation scan complete."
- name: Check Compliance State & Fail Pipeline if Non-Compliant
shell: pwsh
run: |
$rgName = "${{ env.RESOURCE_GROUP }}"
# Query non-compliant policy states for the specific resource group
$nonCompliantResults = Get-AzPolicyState -ResourceGroupName $rgName -Filter "IsCompliant eq false"
if ($nonCompliantResults) {
Write-Error "CRITICAL: Azure Policy compliance failure detected in Resource Group '$rgName'."
Write-Host "`n=== NON-COMPLIANT RESOURCES REPORT ===" -ForegroundColor Red
foreach ($item in $nonCompliantResults) {
[PSCustomObject]@{
ResourceId = $item.ResourceId
PolicyDefinition = $item.PolicyDefinitionName
PolicyAssignment = $item.PolicyAssignmentName
ComplianceState = $item.ComplianceState
} | Format-List
}
exit 1
} else {
Write-Host "SUCCESS: All resources in '$rgName' comply with assigned Azure Policies!" -ForegroundColor Green
}

Pre-Deployment Alternative: Shift-Left Static Bicep/ARM Policy Check

For a true “Shift-Left” experience that flags violations before sending deployment requests to Azure ARM, you can run the az deployment group validate command inside your pipeline.

If an assigned policy has a Deny effect, ARM validation catches and blocks it prior to deployment:

      - name: Pre-Flight Azure Policy Gate (ARM Validation)
        uses: azure/cli@v2
        with:
          azcliversion: latest
          inlineScript: |
            az deployment group validate \
              --resource-group "rg-devsecops-prod" \
              --template-file "./infra/main.bicep" \
              --parameters "./infra/main.bicepparam"

Key Best Practices

  • Use Short-Lived Scans: On-demand scans using Start-AzPolicyComplianceScan typically complete in 1–3 minutes when scoped to a single Resource Group, making them suitable for PR gating.
  • Granular Exclusions: If a specific step in your pipeline purposefully triggers a policy violation (e.g., temporary staging environments), create a temporary Policy Exemption with an expiration date rather than disabling the policy rule globally.

Leave a Reply