Obed Owusu
Azure Policy · Bicep · Log Analytics · KQL · GitHub Actions

Cloud Policy Compliance Dashboard - Engineering Deep Dive.

How one audit requirement grew into a full Azure governance baseline: mg-platform policies and initiatives defined in Bicep, centralized deployments through GitHub Actions, a shared rg-governance-core pattern per subscription and an mg-level telemetry pipeline powering Workbooks and alerts across Dev/Test/Prod.

Scope: mg-platform + Dev/Test/Prod subscriptions Signals: Policy, AzureActivity, KQL Outputs: Workbook dashboards + governance alerts
mg-platform baseline Cloud-Governance-Baseline (mg-platform) rg-governance-core (per subscription) Dev → Test → Prod pipeline
Cloud Policy Compliance Dashboard architecture diagram

GitHub repo → GitHub Actions CI/CD (Build · Validate · Deploy to Dev/Test/Prod) → Cloud-Governance-Baseline initiative at mg-platformrg-governance-core in each subscription → policy & activity logs into Log Analytics → Workbook dashboard → alerts and notifications.

From mg-platform policies to Workbook insights and alerts..

This section walks through the exact engineering artefacts shown in the architecture: mg-level Bicep modules for policies and the Cloud-Governance-Baseline initiative, mg-platform assignment, the rg-governance-core pattern deployedto each subscription (Dev/Test/Prod), the three-stage GitHub Actions pipeline, and the Workbook + KQL queries that surface policy signals into dashboards and alerts.

Bicep Cloud Governance Baseline at mg-platform.

The governance baseline is defined at the management group level, not just a single subscription. A custom Storage network policy is created and then wrapped in the Cloud-Governance-Baseline initiative at mg-platform, ready to grow with additional controls: NSG hygiene, tag enforcement, security policies, and “diagnostic settings required” rules for core resources.

targetScope = 'managementGroup' mg-platform as control plane Custom Storage network policy Initiative ready for NSG/tags/encryption/diagnostics
mg/policy-public-network-access.bicep
targetScope = 'managementGroup'

@description('Name of the management group where the baseline will live.')
param managementGroupName string = 'mg-platform'

@description('Display name for the custom Storage network policy.')
param policyDisplayName string = 'Audit public network access on Storage Accounts'

@description('Policy definition category.')
param policyCategory string = 'Network'

resource policy 'Microsoft.Authorization/policyDefinitions@2022-06-01' = {
  name: 'audit-public-network-access-storage'
  properties: {
    displayName: policyDisplayName
    mode: 'Indexed'
    policyType: 'Custom'
    metadata: {
      category: policyCategory
    }
    policyRule: {
      if: {
        allOf: [
          {
            field: 'type'
            equals: 'Microsoft.Storage/storageAccounts'
          }
          {
            field: 'Microsoft.Storage/storageAccounts/networkAcls.defaultAction'
            equals: 'Allow'
          }
        ]
      }
      then: {
        effect: 'audit'
      }
    }
  }
}

output policyDefinitionId string = policy.id
mg/initiative-cloud-governance.bicep
targetScope = 'managementGroup'

@description('Management group for the Cloud Governance baseline.')
param managementGroupName string = 'mg-platform'

@description('Display name for the Cloud Governance baseline initiative.')
param initiativeDisplayName string = 'Cloud-Governance-Baseline'

@description('ID of the custom policy definition to include in the initiative.')
param policyDefinitionId string

resource initiative 'Microsoft.Authorization/policySetDefinitions@2022-06-01' = {
  name: 'cloud-governance-baseline'
  properties: {
    displayName: initiativeDisplayName
    policyType: 'Custom'
    metadata: {
      category: 'Cloud Governance'
    }
    parameters: {}
    policyDefinitions: [
      {
        policyDefinitionId: policyDefinitionId
        parameters: {}
      }
      // Future: NSG hygiene, tag enforcement, security policies, diagnostic settings
    ]
  }
}

output initiativeId string = initiative.id

mg-platform assignment & rg-governance-core per subscription.

To match the diagram, the initiative is assigned at the mg-platform management group. Every subscription under mg-platform (sub-dev, sub-test, sub-prod) inherits the baseline. Separately, a subscription-scoped Bicep template creates rg-governance-core in each subscription with a Log Analytics workspace, Workbook, alert rules, Action Group and optional remediation Logic App.

Policy assignment at mg-platform sub-dev / sub-test / sub-prod inherit baseline rg-governance-core per subscription Workspace, Workbook, alerts, Action Group
mg/assignment-cloud-governance.bicep
targetScope = 'managementGroup'

@description('Management group that will host the Cloud Governance baseline.')
param managementGroupName string = 'mg-platform'

@description('Display name for the initiative assignment.')
param assignmentDisplayName string = 'Cloud-Governance-Baseline-mg-platform'

@description('ID of the initiative to assign.')
param initiativeId string

resource mgAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
  name: 'cloud-governance-baseline-mg-platform'
  scope: managementGroupResourceId(managementGroupName)
  properties: {
    displayName: assignmentDisplayName
    policyDefinitionId: initiativeId
    enforcementMode: 'Default'
    parameters: {}
  }
}
infra/governance-core-subscription.bicep
targetScope = 'subscription'

@description('Location for governance resources.')
param location string = 'westeurope'

@description('Name of the governance resource group.')
param governanceRgName string = 'rg-governance-core'

@description('Email address for the governance Action Group.')
param governanceEmail string

resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = {
  name: governanceRgName
  location: location
}

resource workspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
  name: 'law-governance'
  location: location
  kind: 'Workspace'
  sku: {
    name: 'PerGB2018'
  }
  properties: {
    retentionInDays: 30
  }
}

resource actionGroup 'Microsoft.Insights/actionGroups@2022-06-15' = {
  name: 'ag-cloud-policy-deny'
  location: 'global'
  properties: {
    groupShortName: 'GovDeny'
    enabled: true
    emailReceivers: [
      {
        name: 'PlatformTeam'
        emailAddress: governanceEmail
        useCommonAlertSchema: true
      }
    ]
  }
}

// Optional: Logic App for remediation, Workbook deployment & alert rules are added as separate modules
mg/main-mg-platform.bicep (wiring policy, initiative, mg assignment)
targetScope = 'managementGroup'

@description('Management group name for the governance baseline.')
param managementGroupName string = 'mg-platform'

module policy './policy-public-network-access.bicep' = {
  name: 'policy-storage-public-network'
  params: {
    managementGroupName: managementGroupName
  }
}

module initiative './initiative-cloud-governance.bicep' = {
  name: 'cloud-governance-initiative'
  params: {
    managementGroupName: managementGroupName
    policyDefinitionId: policy.outputs.policyDefinitionId
  }
}

module assignment './assignment-cloud-governance.bicep' = {
  name: 'cloud-governance-assignment'
  params: {
    managementGroupName: managementGroupName
    initiativeId: initiative.outputs.initiativeId
  }
}

CI/CD – multi-stage GitHub Actions across Dev, Test, Prod.

The CI/CD pipeline mirrors the left side of the architecture diagram: a developer pushes to main, a GitHub Actions workflow builds and validates the Bicep templates, then deploys to three environments – sub-dev, sub-test and sub-prod. The management group baseline is deployed once to mg-platform, while the rg-governance-core resources and Workbook are deployed into each subscription.

GitHub → Azure OIDC trust Build & validate Bicep Deploy mg-platform baseline Deploy rg-governance-core to Dev/Test/Prod
.github/workflows/deploy-cloud-policy-dashboard.yml
name: deploy-cloud-policy-dashboard

on:
  push:
    branches: [ "main" ]
  workflow_dispatch: {}

permissions:
  id-token: write
  contents: read

env:
  AZURE_TENANT_ID:       ${{ secrets.AZURE_TENANT_ID }}
  AZURE_CLIENT_ID:       ${{ secrets.AZURE_CLIENT_ID }}
  MG_PLATFORM:           "mg-platform"
  SUB_DEV:               ${{ secrets.AZURE_SUB_DEV_ID }}
  SUB_TEST:              ${{ secrets.AZURE_SUB_TEST_ID }}
  SUB_PROD:              ${{ secrets.AZURE_SUB_PROD_ID }}

jobs:
  build-and-validate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ env.AZURE_CLIENT_ID }}
          tenant-id: ${{ env.AZURE_TENANT_ID }}
          subscription-id: ${{ env.SUB_DEV }} # any subscription for validation

      - name: Install Bicep CLI
        run: |
          az bicep install
          az bicep version

      - name: Validate mg-platform baseline (what-if)
        run: |
          az deployment mg what-if \
            --name mg-platform-whatif \
            --location westeurope \
            --management-group-id $MG_PLATFORM \
            --template-file ./mg/main-mg-platform.bicep

      - name: Validate governance-core for subscriptions (what-if dev)
        run: |
          az account set --subscription $SUB_DEV
          az deployment sub what-if \
            --name gov-core-dev-whatif \
            --location westeurope \
            --template-file ./infra/governance-core-subscription.bicep \
            --parameters governanceEmail='platform-team@contoso.com'

  deploy-mg-platform:
    runs-on: ubuntu-latest
    needs: build-and-validate
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ env.AZURE_CLIENT_ID }}
          tenant-id: ${{ env.AZURE_TENANT_ID }}
          subscription-id: ${{ env.SUB_DEV }} # any sub; mg deployments ignore subscription

      - name: Deploy Cloud-Governance-Baseline to mg-platform
        run: |
          az deployment mg create \
            --name mg-platform-governance \
            --location westeurope \
            --management-group-id $MG_PLATFORM \
            --template-file ./mg/main-mg-platform.bicep

  deploy-subscriptions:
    runs-on: ubuntu-latest
    needs: deploy-mg-platform
    strategy:
      matrix:
        env:
          - name: dev
            sub: ${{ env.SUB_DEV }}
          - name: test
            sub: ${{ env.SUB_TEST }}
          - name: prod
            sub: ${{ env.SUB_PROD }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ env.AZURE_CLIENT_ID }}
          tenant-id: ${{ env.AZURE_TENANT_ID }}
          subscription-id: ${{ matrix.env.sub }}

      - name: Deploy rg-governance-core to ${{ matrix.env.name }}
        run: |
          az deployment sub create \
            --name gov-core-${{ matrix.env.name }} \
            --location westeurope \
            --template-file ./infra/governance-core-subscription.bicep \
            --parameters governanceEmail='platform-team@contoso.com'

This matches the diagram: a single pipeline builds and validates the templates, deploys the management group baseline once, then rolls the governance core out to Dev, Test and Prod subscriptions.

KQL queries & Workbook JSON – the governance scoreboard.

The right-hand column of the diagram shows the telemetry flow. Policy evaluation and DENY events surface in AzureActivity and policy resource tables. The Workbook running in rg-governance-core turns those signals into a governance scoreboard for mg-platform: donuts by policy, trend lines over time, and drill-down tables that show which subscriptions and resource groups are the noisiest.

AzureActivity + PolicyResources Workbook JSON as code Non-compliant resources by policy & subscription Alert queries reused across Dev/Test/Prod
KQL – policy DENY events (alert + donut)
AzureActivity
| where CategoryValue == "Policy"
| where ActivityStatusValue == "Failure"
| where OperationNameValue contains "deny" or OperationNameValue contains "DENY"
| project TimeGenerated,
          SubscriptionId,
          ResourceGroup = tostring(ResourceGroup),
          Caller,
          OperationNameValue,
          ActivityStatusValue
| summarize DenyCount = count() by bin(TimeGenerated, 15m), SubscriptionId, ResourceGroup, Caller
| order by TimeGenerated desc
KQL – non-compliance summary by policy
PolicyResources
| where isnotempty(ComplianceState)
| summarize
    NonCompliantResources = countif(ComplianceState == "NonCompliant"),
    CompliantResources    = countif(ComplianceState == "Compliant")
  by PolicyDefinitionName = tostring(PolicyDefinitionName),
     SubscriptionId
| order by NonCompliantResources desc
workbooks/policy-dashboard.bicep (Workbook into rg-governance-core)
targetScope = 'resourceGroup'

@description('Name of the Workbook.')
param workbookName string = 'Cloud Policy Compliance Dashboard'

@description('Log Analytics workspace resource ID.')
param workspaceResourceId string

@description('Location for the Workbook.')
param location string

@description('Exported workbook JSON (trimmed for brevity).')
@secure()
param workbookJson string

resource workbook 'microsoft.insights/workbooks@2022-04-01' = {
  name: guid(workbookName)
  location: location
  kind: 'shared'
  properties: {
    displayName: workbookName
    serializedData: workbookJson
    sourceId: workspaceResourceId
    category: 'workbook'
  }
}

// The exported JSON defines tiles that mirror the diagram:
// - Donut: Non-compliant resources by policy
// - Column chart: DENY events over time
// - Table: Drill-down by subscription, RG, caller

Testing & validation – proving the mg-platform baseline works.

To align with the diagram, I validated the baseline from mg-platform down into each subscription: create misconfigurations in sub-dev, watch them surface in the shared Workbook, and confirm that alerts fire via the Action Group. The same pattern applies to sub-test and sub-prod, ensuring that governance behaves consistently before anything touches production.

Misconfig in sub-dev Policy evaluation at mg-platform Workbook & alerts via rg-governance-core Repeat for Test & Prod
Test scenario 1 – public Storage in sub-dev
// 1. In sub-dev, create a Storage Account with default network access = Allow.
// 2. Wait for Azure Policy evaluation (Cloud-Governance-Baseline at mg-platform).
// 3. Confirm non-compliance + DENY events in AzureActivity and PolicyResources.
// 4. Validate Workbook donut/table + alert firing to the Action Group.

AzureActivity
| where CategoryValue == "Policy"
| where SubscriptionId == ""
| where OperationNameValue contains "StorageAccounts"
| project TimeGenerated, Caller, OperationNameValue, ResourceGroup
| order by TimeGenerated desc
Test scenario 2 – future NSG hygiene policy (Dev/Test/Prod)
// Example KQL once an NSG hygiene policy is added to the initiative.
// Checks for wide-open inbound NSG rules (0.0.0.0/0) on admin ports
// across all subscriptions under mg-platform.

AzureNetworkAnalytics_CL
| where Direction_s == "Inbound"
| where RemoteIP_s == "0.0.0.0/0"
| where DstPort_d in (22, 3389)
| summarize OpenRules = count() by NSG = tostring(SecurityRule_s), SubscriptionId
High-level mg-platform test checklist
- [x] mg-platform baseline deploys without errors (what-if + create).
- [x] Cloud-Governance-Baseline appears at mg-platform with expected policy definitions.
- [x] sub-dev, sub-test, sub-prod all inherit the mg-platform assignment.
- [x] rg-governance-core is deployed in each subscription with workspace & Action Group.
- [x] Non-compliant resources appear in Policy blade and Workbook for each sub.
- [x] DENY events are written to AzureActivity and visible in Log Analytics.
- [x] Alert rule fires for DENY events in each environment.
- [x] Emails from the Action Group land in the governance mailbox.
- [x] After remediation, compliance improves and alerts stop firing.

This gives mg-platform a repeatable way to prove that the baseline works in all three environments before onboarding additional subscriptions or adding more policies (NSG, Key Vault, tags, diagnostics).