Skip to content

Tools

Using Enterprise Azure Policy as Code (EPAC)

In this blog post, we will show how to use Enterprise Azure Policy as Code (EPAC) to manage your Azure environment.

Use case

  • Determine desired state strategy.
  • We have some existing Azure Policies that we want to manage as code.
  • For simplicity, we will suppose that we have a unique Centralized Team that manages the policies.
  • We will use a Git repository to store the policies and the CI/CD process to deploy them.
  • We doesn't have any exclude resources in the environment.
  • How to handle Defender for Cloud Policy Assignments:
  • We will use Defender for Cloud to manage the Policy Assignments for Defender Plans when a plan is enabled.
  • EPAC will manage Defender for Cloud Security Policy Assignments at the management group level. This is the default behavior.
  • Design your CI/CD process:
  • We will use Release Flow

Management Groups for Enterprise Scale Landing Zone

This is the common structure for the Management Groups in the Enterprise Scale Landing Zone, now Accelerator Landing Zone:

    graph TD
        A[Root Management Group] --> B[Intermediary-Management-Group]
        B --> C[Decommissioned]
        B --> D[Landing Zones]
        B --> E[Platform]
        B --> F[Sandboxes]
        D --> G[Corp]
        D --> H[Online]
        E --> I[Connectivity]
        E --> J[Identity]
        E --> K[Management]

For this use case, we will use the Landing Zones Management Group for duplicate and old Management Group hierarchy (manage-azure-policy):

  graph TD
      A[Root Management Group] --> B[epac-dev]
      B --> C[dev-decommissioned]
      B --> D[dev-landingzones]
      B --> E[dev-platform]
      B --> F[dev-sandbox]
      D --> G[dev-corp]
      D --> H[dev-online]
      E --> I[dev-connectivity]
      E --> J[dev-identity]
      E --> K[dev-management]
      A[Root Management Group] --> L[epac-prod]
      L --> M[prod-decommissioned]
      L --> N[prod-landingzones]
      L --> O[prod-platform]
      L --> P[prod-sandbox]
      N --> Q[prod-corp]
      N --> R[prod-online]
      O --> S[prod-connectivity]
      O --> T[prod-identity]
      O --> U[prod-management]
      A[Root Management Group] --> V[manage-azure-policy]

      classDef dev fill:#f90,stroke:#333,stroke-width:2px;
      classDef prod fill:#f9f,stroke:#333,stroke-width:2px;
      class dev A,B,C,D,E,F,G,H,I,J,K;
      class prod L,M,N,O,P,Q,R,S,T,U;   

Note

You could also use two different tenants for the different environments, but this is not the case for this use case.

You can create this Management Groups hierarcly using the Azure CLI with the following commands:

az account management-group create --name "MyManagementGroup"
az account management-group move --name "ChildGroup" --new-parent "NewParentGroup"

For the use case, we will use the following commands:

az account management-group create --name "epac-dev"
az account management-group create --name "dev-decommissioned" --parent "epac-dev"
az account management-group create --name "dev-landingzones" --parent "epac-dev"
az account management-group create --name "dev-platform" --parent "epac-dev"
az account management-group create --name "dev-sandbox" --parent "dev-landingzones"
az account management-group create --name "dev-corp" --parent "dev-landingzones"
az account management-group create --name "dev-online" --parent "dev-landingzones"
az account management-group create --name "dev-connectivity" --parent "dev-platform"
az account management-group create --name "dev-identity" --parent "dev-platform"
az account management-group create --name "dev-management" --parent "dev-platform"
az account management-group create --name "epac-prod"
az account management-group create --name "prod-decommissioned" --parent "epac-prod"
az account management-group create --name "prod-landingzones" --parent "epac-prod"
az account management-group create --name "prod-platform" --parent "epac-prod"
az account management-group create --name "prod-sandbox" --parent "prod-landingzones"
az account management-group create --name "prod-corp" --parent "prod-landingzones"
az account management-group create --name "prod-online" --parent "prod-landingzones"
az account management-group create --name "prod-connectivity" --parent "prod-platform"
az account management-group create --name "prod-identity" --parent "prod-platform"
az account management-group create --name "prod-management" --parent "prod-platform"

Installation

To install EPAC, follow these steps:

    Install-Module Az -Scope CurrentUser
    Install-Module EnterprisePolicyAsCode -Scope CurrentUser

Create an empty repository in github and clone it

Create a repository in github and clone it

    git clone https://github.com/user/demo-EPAC.git

Create a branch for the feature/firstcommit

    git checkout -b feature/firstcommit

Note

From this moment on, we will execute all commands within the repository directory.

Create Definitions

New-EPACDefinitionFolder -DefinitionsRootFolder Definitions

This command creates a folder structure for the definitions. The Definitions folder Structure is as follows:

  • Define the Azure environment(s) in file global-settings.jsonc
  • Create custom Policies (optional) in folder policyDefinitions
  • Create custom Policy Sets (optional) in folder policySetDefinitions
  • efine the Policy Assignments in folder policyAssignments
  • Define the Policy Exemptions (optional) in folder policyExemptions
  • Define Documentation in folder policyDocumentations]

Configure global-settings.jsonc

global-settings.jsonc is the file where you define the Azure environment(s) that you want to manage with EPAC. The file should be located in the Definitions folder. Here is an example of the content of the file:

{
    "$schema": "https://raw.githubusercontent.com/Azure/enterprise-azure-policy-as-code/main/Schemas/global-settings-schema.json",
    "pacOwnerId": "ff2ce5e1-da8a-4cfb-883b-aee9fbfb85d6",
    "pacEnvironments": [
        {
            "pacSelector": "epac-dev",
            "cloud": "AzureCloud",
            "tenantId": "e18e4e7e-d0cc-40af-9907-84923ca55499",
            "deploymentRootScope": "/providers/Microsoft.Management/managementGroups/epac-dev",
            "desiredState": {
                "strategy": "full",
                "keepDfcSecurityAssignments": false
            },
            "managedIdentityLocation": "france"
        },
        {
            "pacSelector": "tenant",
            "cloud": "AzureCloud",
            "tenantId": "e18e4e7e-d0cc-40af-9907-84923ca55499",
            "deploymentRootScope": "/providers/Microsoft.Management/managementGroups/epac-prod",
            "desiredState": {
                "strategy": "full",
                "keepDfcSecurityAssignments": false
            },
            "managedIdentityLocation": "france",
            "globalNotScopes": [
                "/providers/Microsoft.Management/managementGroups/mg-Epac-Dev",
                "/providers/Microsoft.Management/managementGroups/manage-azure-policy"
            ]
        },
        {
            "pacSelector": "manage-azure-policy",
            "cloud": "AzureCloud",
            "tenantId": "e18e4e7e-d0cc-40af-9907-84923ca55499",
            "deploymentRootScope": "/providers/Microsoft.Management/managementGroups/manage-azure-policy",
            "desiredState": {
                "strategy": "full",
                "keepDfcSecurityAssignments": false
            },
            "managedIdentityLocation": "france"
        }
    ]

}

Info

The pacOwner helps to identify who or what owns an Assignment or Policy definition deployment and needs to be unique to your EPAC environment. The pacOwnerId is used to identity policy resources that are deployed by your EPAC repository, or another EPAC isntance, legacy or another solution entirely.

You can generate a new id with New-Guid

Extracting existing Policy Resources

Export-AzPolicyResources

This command extracts all existing Policy Resources in the Azure environment(s) defined in the global-settings.jsonc file. The extracted resources are saved in the Output/Definitions folder.

You needs review the extracted resources and move them to the correct folder in the Definitions folder.

Syncing ALZ Definitions

Sync-ALZPolicies -DefinitionsRootFolder .\Definitions -CloudEnvironment AzureCloud # Also accepts AzureUSGovernment or AzureChinaCloud

You can sync the ALZ Definitions manually or use a GitHub action creating .github\workflows\alz-sync.yaml in your repository with the following content:

name: Sync ALZ Policy Objects

env:
  REVIEWER: anwather # Change this to your GitHub username
  DefinitionsRootFolder: Definitions # Change this to the folder where your policy definitions are stored

on:
  workflow_dispatch

jobs:
    sync:
        runs-on: ubuntu-latest
        steps:
        - name: Checkout
          uses: actions/checkout@v4
        - shell: pwsh
          name: Install Required Modules
          run: |
            Install-Module EnterprisePolicyAsCode -Force
            Sync-ALZPolicies -DefinitionsRootFolder $env:DefinitionsRootFolder
            $branchName = "caf-sync-$(Get-Date -Format yyyy-MM-dd-HH-mm)"
            git config user.name "GitHub Actions Bot"
            git config user.email "<>"
            git checkout -b $branchName
            git add .
            git commit -m "Updated ALZ policy objects"
            git push --set-upstream origin $branchName
            gh pr create -B main -H $branchName --title "Verify Synced Policies - $branchName" -b "Checkout this PR branch and validate changes before merging." --reviewer $env:REVIEWER
        env:
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CI/CD with Github Flow

We will use the Github Flow to manage the CI/CD process. We will create a Github Actions to deploy the policies to the Azure environment(s) defined in the global-settings.jsonc file.

We can open a second terminal and execute the following command to create the Github Actions in one upper level folder of our repository. This command will create the Github Actions in the .github\workflows folder of the repository. :

git clone https://github.com/Azure/enterprise-azure-policy-as-code
cd enterprise-azure-policy-as-code
New-PipelinesFromStarterKit -StarterKitFolder .\StarterKit -PipelinesFolder ..\global-azure-2024-demo-EPAC\.github\workflows -PipelineType GitHubActions -BranchingFlow github -ScriptType Module 

Now, we need to create some environments with secrets in the repository to use in the Github Actions. We need to create the following environments:

Environment Purpose App Registration (SPN)
EPAC-DEV Plan and deploy to epac-dev ci-cd-epac-dev-owner
TENANT-PLAN Build deployment plan for tenant ci-cd-root-policy-reader
TENANT-DEPLOY-POLICY Deploy Policy resources for tenant ci-cd-root-policy-contributor
TENANT-DEPLOY-ROLES Deploy Roles for tenant ci-cd-root-user-assignments
TENANT-REMEDIATE-POLICY Remediate Policy resources for tenant ci-cd-root-policy-contributor

You need to Configure a federated identity credential on an app too.

First Commit

Now we can commit the changes to the repository and make a pull request to the main branch.

References

Enterprise Azure Policy as Code (EPAC)

Enterprise Azure Policy as Code (EPAC) is a powerful tool that allows organizations to manage Azure Policies as code in a git repository. It's designed for medium and large organizations with a larger number of Policies, Policy Sets, and Assignments, and/or complex deployment scenarios.

Key Features of EPAC

  • Single and multi-tenant policy deployment: EPAC supports both single and multi-tenant policy deployments, making it versatile for different organizational structures.
  • Easy CI/CD Integration: EPAC can be easily integrated with any CI/CD tool, which makes it a great fit for DevOps environments.
  • Operational scripts: EPAC includes operational scripts to simplify operational tasks.
  • Integration with Azure Landing Zones: EPAC provides a mature integration with Azure Landing Zones. Utilizing Azure Landing Zones together with EPAC is highly recommended.

Who Should Use EPAC?

EPAC is designed for medium and large organizations with a larger number of Policies, Policy Sets, and Assignments, and/or complex deployment scenarios. However, smaller organizations implementing fully-automated DevOps deployments of every Azure resource (known as Infrastructure as Code) can also benefit from EPAC.

How Does EPAC Work?

EPAC works by deploying all policies and policy assignments defined in the EPAC repository to the deploymentRootScope and its children. It takes possession of all Policy Resources at the deploymentRootScope and its children.

Alt text

The process depicted in the image involves three key scripts that manage a deployment sequence. Here's a breakdown of the process:

  1. Definition Files: The process begins with various definition files in JSON, CSV, or XLSX formats. These files contain policy definitions, policy set (initiative) definitions, assignments, exemptions, and global settings.

  2. Planning Script: The Build-DeploymentPlans.ps1 script uses these definition files to create a deployment plan. This script requires Resource Policy Reader privileges.

  3. Deployment Scripts: The deployment plan is then used by two deployment scripts:

  4. Deploy-PolicyPlan.ps1: This script deploys Policy resources using the policy-plan.json file from the deployment plan. It requires Resource Policy Contributor privileges.
  5. Deploy-RolesPlan.ps1: This script deploys Role Assignments using the roles-plan.json file from the deployment plan. It requires User Access Administrator privileges.

The process includes optional approval gates after each deployment step. These are typically used in production environments to ensure each deployment step is reviewed and approved before moving to the next.

Warning

EPAC is a true desired state deployment technology. It takes possession of all Policy Resources at the deploymentRootScope and its children. It will delete any Policy resources not defined in the EPAC repo.

Conclusion

EPAC is a robust solution for managing Azure Policies as code. It offers a high level of assurance in highly controlled and sensitive environments, and a means for the development, deployment, management, and reporting of Azure policy at scale.

References

Manage Azure Policy GitHub Action

It's recommended to review:

Overview

The Manage Azure Policy GitHub Action empowers you to enforce organizational standards and assess compliance at scale using Azure policies. With this action, you can seamlessly integrate policy management into your CI/CD pipelines, ensuring that your Azure resources adhere to the desired policies.

Info

This project does not have received any updates since some time, but it is still a simple option to develop your Azure Policies. As everything cannot be good to say that this deployment method has a major drawback, deletions must be done by hand :S

Key Features

  1. Customizable Workflows: GitHub workflows are highly customizable. You have complete control over the sequence in which Azure policies are rolled out. This flexibility enables you to follow safe deployment practices and catch regressions or bugs well before policies are applied to critical resources.

  2. Azure Login Integration: The action assumes that you've already authenticated using the Azure Login action. Make sure you've logged in using an Azure service principal with sufficient permissions to write policies on selected scopes. Refer to the full documentation of Azure Login Action for details on permissions.

  3. Policy File Structure: Your policy files should be organized in a specific directory structure within your GitHub repository. Here's how it should look:

    |- policies/
       |- <policy1_name>/
          |- policy.json
          |- assign.<name1>.json
          |- assign.<name2>.json
          ...
       |- <policy2_name>/
          |- policy.json
          |- assign.<name1>.json
          |- assign.<name2>.json
          ...
    
    • Each policy resides in a subfolder under the policies/ directory.
    • The policy.json file contains the policy definition.
    • Assignment files (e.g., assign.<name1>.json) specify how the policy is applied.
  4. Inputs for the Action:

    • Paths: Specify the mandatory path(s) to the directory containing your Azure policy files.

Sample Workflow

Here's an example of how you can apply policies at the Management Group scope using the Manage Azure Policy action:

name: 'Test Policy'
on:
  push:
    branches: 
    - "*" 
    paths: 
     - 'policies/**'
     - 'initiatives/**'
  workflow_dispatch:

jobs:
  apply-azure-policy:    
    runs-on: ubuntu-latest
    steps:
    # Azure Login
    - name: Login to Azure
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}
        allow-no-subscriptions: true

    - name: Checkout
      uses: actions/checkout@v2 

    - name: Create or Update Azure Policies
      uses: azure/manage-azure-policy@v0
      with:      
        paths:  |                
          policies/**
          initiatives/**
        assignments:  |
          assign.*_testRG_*.json

Remember to replace the placeholder values (such as secrets.AZURE_CREDENTIALS) with your actual configuration, you can follow this instructions to create a service principal and get the credentials: Create a service principal and get the credentials

Example of use for Policy

In this example we define all our policies and initiatives at management group level and assign to resource group, and we have a policy that requires a specific tag and its value.

You need to create a folder structure like this:

|- policies/
   |- require-tag-and-its-value/
      |- policy.json
      |- assign.testRG_testazurepolicy.json
|- initiatives/
   |- initiative1/
      |- policyset.json
      |- assign.testRG_testazurepolicy.json

policies

Info

  • The policy.json file contains the policy definition, and the assign.<name>.json file specifies how the policy is applied.
policy.json

Info

  • The id value specifies where you are going to define the policy.
policy.json
{
    "id": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/requite-tag-and-its-value",
    "type": "Microsoft.Authorization/policyDefinitions",
    "name": "requite-tag-and-its-value",
    "properties": {
        "displayName": "Require a tag and its value",
        "policyType": "Custom",
        "mode": "Indexed",
        "description": "This policy requires a specific tag and its value.",
        "metadata": {
            "category": "Tags"
        },
        "parameters": {
            "tagName": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Name",
                    "description": "Name of the tag, such as 'environment'"
                }
            },
            "tagValue": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Value",
                    "description": "Value of the tag, such as 'production'"
                }
            }
        }
        },
        "policyRule": {
            "if": {
                "not": {
                    "field": "[concat('tags[', parameters('tagName'), ']')]",
                    "equals": "[parameters('tagValue')]"
                    }
                },
            "then": {
                "effect": "deny"
            }
        }
    }
assign.testRG_testazurepolicy.json

Info

  • Change the id and scope values in the assign.<name>.json file to match your Azure subscription and resource group.
  • id specifies where you are going to deploy the assignment.
  • id and name are related, name can not be any value, it should be the same as the last part of the id. You can generete a new GUID and use it as name with (1..24 | %{ '{0:x}' -f (Get-Random -Max 16) }) -join ''
  • name and id are related.
  • The policyDefinitionId value should match the id value in the policy.json file.
assign.testRG_testazurepolicy.json
{
    "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-testazurepolicy/providers/Microsoft.Authorization/policyAssignments/599a2c3a1a3b1f8b8e547b3e",
    "type": "Microsoft.Authorization/policyAssignments",
    "name": "599a2c3a1a3b1f8b8e547b3e",     
    "properties": {
        "description": "This policy audits the presence of a specific tag and its value.",
        "displayName": "Require a tag and its value",
        "parameters": {
            "tagName": {
              "value": "environment"
            },
            "tagValue": {
              "value": "production"
            }
          },
          "nonComplianceMessages": [
            {
              "message": "This resource is not compliant with the policy. Please apply the required tag and its value."
            }
          ],
          "enforcementMode": "Default",
          "policyDefinitionId": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/requite-tag-and-its-value",
          "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-testazurepolicy"
    }    
}

initiatives

Info

  • The policyset.json file contains the policy definition, and the assign.<name>.json file specifies how the initiative is applied.
policyset.json

Info

  • The id value specifies where you are going to define the initiative.
  • The policyDefinitions array contains the policy definitions that are part of the initiative.
  • The parameters object defines the parameters that can be passed to the policies within the initiative.
  • The policyDefinitionId value should match the id value in the policy.json file of the policy.
policyset.json
{
    "id": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policySetDefinitions/initiative1",
    "type": "Microsoft.Authorization/policySetDefinitions",
    "name": "initiative1",
    "properties": {
        "displayName": "Initiative 1",
        "description": "This initiative contains a set of policies for testing.",
        "metadata": {
            "category": "Test"
        },
        "parameters": {
            "tagName": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Name",
                    "description": "Name of the tag, such as 'environment'"
                }
            },
            "tagValue": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Value",
                    "description": "Value of the tag, such as 'production'"
                }
            }
        },
        "policyDefinitions": [
            {
                "policyDefinitionId": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/requite-tag-and-its-value",
                "parameters": {
                    "tagName": {
                        "value": "[parameters('tagName')]"
                    },
                    "tagValue": {
                        "value": "[parameters('tagValue')]"
                    },
                    "effect": {
                        "value": "Deny"
                    }
                }
            }
        ]
    }
}
assign.testRG_testazurepolicyset.json
assign.testRG_testazurepolicyset.json
{
    "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-testazurepolicy/providers/Microsoft.Authorization/policyAssignments/ada0f4a34b09cf6ad704cc62",
    "type": "Microsoft.Authorization/policyAssignments",
    "name": "ada0f4a34b09cf6ad704cc62",     
    "properties": {
        "description": "This initiative audits the presence of a specific tag and its value.",
        "displayName": "Require a tag and its value",
        "parameters": {
            "tagName": {
              "value": "environment"
            },
            "tagValue": {
              "value": "production"
            }
          },
          "nonComplianceMessages": [
            {
              "message": "This resource is not compliant with the policy. Please apply the required tag and its value."
            }
          ],
          "enforcementMode": "Default",
          "policyDefinitionId": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policySetDefinitions/requite-tag-and-its-value",
          "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-testazurepolicy"
    }    
}

Conclusion

By incorporating the Manage Azure Policy action into your GitHub workflows, you can seamlessly enforce policies, maintain compliance, and ensure the robustness of your Azure resources, although it has its drawbacks, it is one more step compared to a portal. Later we will see the deployment with a more robust tool: EPAC

Learn more about Azure Policies and explore the action on the GitHub Marketplace.