How to build an audit Azure Policy with multiple parameters
Published Nov 09 2020 02:00 AM 24.8K Views
Microsoft

It's recommended that you plan and implement a governance strategy before you start deploying cloud resources, but in reality it doesn't always work out like that. How can we find which existing resources don't meet our new standards?

 

There's a couple of ways to do it, including using a Kusto query or PowerShell, but today we'll look at using an Azure Policy definition in audit mode.

 

The scenario:
Tailwind Traders have decided that all of the resources in one subscription should have two resource tags applied to them: Dept (department) and Env (environment). This has almost become an accepted standard, as some teams have already tagged their resources, but not consistently. Tailwind Traders doesn't want to enforce that these tags are required, as they have some automated provisioning processes that will need to be updated first. They do want to see how many of the existing resources do not have these tags, and be able to check this easily on an ongoing basis.

 

The challenge:
The current built-in Azure Policy definitions for tags either require (on new resources), add, append or inherit tags. The DeployIfNotExists method can be use to retro-actively apply a require tags policy to existing resources, but Tailwind Traders only wants to report which resources are non-compliant, not modify them. Azure Policy can run in audit mode, and Tailwind Traders needs to create a policy definition that looks for two different tag names on each resource. If a resource doesn't have both tags, we want to know.


The process:
Start with what you know - I mentioned there's already an in-built Azure Policy definition for requiring a tag on resources. That's a good place to start to check for the schema of how our JSON should be written. If, like me, you're not the sort of person who writes JSON off the top of your head, find things that do incorporate some of what you need to apply and take it from there.

 

Here's the policy definition for "Require a tag on resources":

 

 

 

 

 

{
  "properties": {
    "displayName": "Require a tag on resources",
    "policyType": "BuiltIn",
    "mode": "Indexed",
    "description": "Enforces existence of a tag. Does not apply to resource groups.",
    "metadata": {
      "version": "1.0.1",
      "category": "Tags"
    },
    "parameters": {
      "tagName": {
        "type": "String",
        "metadata": {
          "displayName": "Tag Name",
          "description": "Name of the tag, such as 'environment'"
        }
      }
    },
    "policyRule": {
      "if": {
        "field": "[concat('tags[', parameters('tagName'), ']')]",
        "exists": "false"
      },
      "then": {
        "effect": "deny"
      }
    }
  },
  "id": "/providers/Microsoft.Authorization/policyDefinitions/871b6d14-10aa-478d-b590-94f262ecfa99",
  "type": "Microsoft.Authorization/policyDefinitions",
  "name": "871b6d14-10aa-478d-b590-94f262ecfa99"
}

 

 

 

 

 

The first edit is the easiest one - I need to change the line "effect": "deny" to read "effect": "audit"
This will show me if any existing resources are non-compliant and allows new resources to be created without enforcing these tags.

 

Now to figure out how to add an additional tag!

 

I know that there's an in-built policy called Allowed locations. This policy lets you select more than one location where a resource can be deployed. If we take a look at that definition:

 

 

 

 

 

{
  "properties": {
    "displayName": "Allowed locations",
    "policyType": "BuiltIn",
    "mode": "Indexed",
    "description": "This policy enables you to restrict the locations your organization can specify when deploying resources. Use to enforce your geo-compliance requirements. Excludes resource groups, Microsoft.AzureActiveDirectory/b2cDirectories, and resources that use the 'global' region.",
    "metadata": {
      "version": "1.0.0",
      "category": "General"
    },
    "parameters": {
      "listOfAllowedLocations": {
        "type": "Array",
        "metadata": {
          "description": "The list of locations that can be specified when deploying resources.",
          "strongType": "location",
          "displayName": "Allowed locations"
        }
      }
    },
    "policyRule": {
      "if": {
        "allOf": [
          {
            "field": "location",
            "notIn": "[parameters('listOfAllowedLocations')]"
          },
          {
            "field": "location",
            "notEquals": "global"
          },
          {
            "field": "type",
            "notEquals": "Microsoft.AzureActiveDirectory/b2cDirectories"
          }
        ]
      },
      "then": {
        "effect": "deny"
      }
    }
  },
  "id": "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c",
  "type": "Microsoft.Authorization/policyDefinitions",
  "name": "e56962a6-4747-49cd-b67b-bf8b01975c4c"
}

 

 

 

 

 

The key thing is the use of the "allOf" expression after the "if". This means that all of the conditions that follow have to be satisfied for the "then" effect to kick in. They are encapsulated by the square brackets [ ].

 

It's also worth noting that the "listOfAllowedLocations" parameter is only mentioned once, as it's using an array. That gives you a list of the Azure regions that you can multi-select from. In our case, we want to specify the two tag names when we apply the policy, so we will need to duplicate the parameters entries.

 

The result:
Merging the above together, we can create the custom policy we want - "Audit if two tags exist":

 

 

 

 

{
  "mode": "Indexed",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "[concat('tags[', parameters('tagName1'),  ']')]",
          "exists": "false"
        },
        {
          "field": "[concat('tags[', parameters('tagName2'),  ']')]",
          "exists": "false"
        }
      ]
    },
    "then": {
      "effect": "audit"
    }
  },
  "parameters": {
    "tagName1": {
      "type": "String",
      "metadata": {
        "displayName": "Tag Name 1",
        "description": "Name of first tag, such as 'environment'"
      }
    },
    "tagName2": {
      "type": "String",
      "metadata": {
        "displayName": "Tag Name 2",
        "description": "Name of second tag, such as 'owner'"
      }
    }
  }
}

 

 

 

 

We've added a second tag as a parameter, so both tag names can be specified when the policy is assigned to a scope. This means you could use the same definition to search for different tags when it is assigned to different scopes (e.g. Resource groups or subscriptions).

 

We have our "allOf" statement to look for both tag name 1 and tag name 2.

 

And we have our effect in Audit mode.

Tag_Policy_compliance.png

 

Troubleshooting:
The trickiest part of merging concepts and duplicating sections is keeping all of your brackets and commas in the right places! In this image, I've removed a bracket on purpose.

Tag_Policy_error.png

The Azure Policy definition editor has thrown some grey, yellow and red boxes down the right margin to warn me that something isn't right. And if I mouse-over a bracket, it will automatically highlight the corresponding bracket pair, so I can also see I'm missing one that should "close off" the parameters section. That final bracket should correspond to the opening bracket at the very top.

 

Some JSON errors will prevent you from saving your edits, or will generate an error in the notifications.

Tag_Policy_notification_error.png

 

Some errors won't be obvious until the policy has had time to run against your resources. For testing, it's worth having a couple of known resources that you know should pass and some you know should fail against your policy. If these don't show up with the correct compliance state, double-check both your JSON and your policy assignment - for example, is there a typo in the tag name?

 

And if all else fails - ask someone else to help you!

 

Now, it's over to you!
Don't be afraid to learn the basics concepts, take what you know, and explore what you can create, even when it's not explicitly documented. Share with us if you've built some advanced Azure Policy definitions - you can even share your work with the community by adding to the Azure Community Policy GitHub repository.

 

Learn more:

Docs - What is Azure Policy?

Docs - Tutorial: Manage tag governance with Azure Policy

Docs - Azure Policy pattern: tags

Docs - Understand Azure Policy effects

MS Learn - Build a cloud governance strategy on Azure

 

-SCuffy

 

 

7 Comments
Iron Contributor

Very helpful Sonia, thank you! This is something that directly correlates to an upcoming initiative at work, great timing! :stareyes:

Microsoft

Thanks @PBradz and good luck with your work initiative! Let me know if you come up with any questions during your process.

Copper Contributor

I attempted to implement this in my Azure environment. I added a 3rd tag as we are looking to audit 3 tags in particular. I set the scope to a specific Resource Group which has two storage account resources "testresource1" and "testresource2". One of the resources does not have all three tags along with it having a completely different tag. And one has the required 3 tags. 

However, no matter what I do they always show up as Compliant for both. I cannot get it to show Non-Complaint even though it clearly has the incorrect tags. I've looked over spelling and syntax mistakes in the definition along with the spelling of the tags. 

 

Here is the defintion:

 

{
  "properties": {
    "displayName""Audit Required Tags on Resources",
    "policyType""Custom",
    "mode""Indexed",
    "description""Enforces existence of a tag. Does not apply to resource groups.",
    "metadata": {
      "category""Tags",
    },
    "parameters": {
      "tagName1": {
        "type""String",
        "metadata": {
          "displayName""Tag Name 1",
          "description""Name of first tag, such as 'Application'"
        }
      },
      "tagName2": {
        "type""String",
        "metadata": {
          "displayName""Tag Name 2",
          "description""Name of second tag, such as 'Business Unit'"
        }
      },
      "tagName3": {
        "type""String",
        "metadata": {
          "displayName""Tag Name 3",
          "description""Name of third tag tag, such as 'Environment'"
        }
      }
    },
    "policyRule": {
      "if": {
        "allOf": [
          {
            "field""[concat('tags[', parameters('tagName1'),  ']')]",
            "exists""false"
          },
          {
            "field""[concat('tags[', parameters('tagName2'),  ']')]",
            "exists""false"
          },
          {
            "field""[concat('tags[', parameters('tagName3'),  ']')]",
            "exists""false"
          }
        ]
      },
      "then": {
        "effect""audit"
      }
    }
  },
}
Microsoft

@fletcherexe  I'm taking a look now, but my gut feeling is that because your script does not specify what things to query, and you've applied the policy at the Resource Group level, it's querying the RG itself for the existence of the tags, not the resources in the RG. Just let me confirm with some testing.

 

OK new theory: Your script works for me as provided, though if you did want to make sure RGs are excluded and only the resources are queried, you could add into the policyRule:

            {
                "field""type",
                "notEquals""Microsoft.Resources/subscriptions/resourceGroups"
            },

I'm now testing with other tag combinations to see if I can reproduce what you're seeing.
Copper Contributor

@Sonia Cuff this is great, thanks for the info. I have tried to use it to create a slightly more complex policy but I don't think I have it right. I want my policy to perform an audit action if multiple options are met.

 

The policy will audit for storage accounts which should have infrastructure encryption enabled if the tag is present and the value is set to true. I have put the code below, could you provide some guidance on why this doesn't work?

 

{
  "mode""Indexed",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field""type",
          "equals""Microsoft.Storage/storageAccounts"
        },
        {
          "field""Microsoft.Storage/storageAccounts/encryption.requireInfrastructureEncryption",
          "notEquals""true"
        },
        {
          "field""[concat('tags[', parameters('tagName'), ']')]",
          "equals""[parameters('tagValue')]"
        }
      ]
    },
    "then": {
      "effect""[parameters('effect')]"
    }
  },
  "parameters": {
    "tagName": {
      "type""String",
      "metadata": {
        "displayName""PII",
        "description""PII"
      }
    },
    "tagValue": {
      "type""String",
      "metadata": {
        "displayName""True",
        "description""True"
      }
    },
    "effect": {
      "type""String",
      "metadata": {
        "displayName""Effect",
        "description""Enable or disable the execution of the audit policy"
      },
      "allowedValues": [
        "Audit",
        "Deny",
        "Disabled"
      ],
      "defaultValue""Audit"
    }
  }
}
Copper Contributor

Hey @Sonia Cuff

 

Thanks for the article. I have a problem, though, which I am not sure if technical or in my expectation. 

My expectation is that it would not allow creation of new resources, unless both tags are present and the resources that are already in place would be marked as non-compliant. Is that correct? 

I am using the code as provided, only changed "effect": "deny" and added defaultValue for the parameters - tagName1 = "environment" and tagName2 = "project". 

 

The problem that I am having is that I am able to create a new resource (storage account in my test) with only a single tag, i.e. Project, without providing tag Environment (or vice versa). 

Also the policy shows a resource (the storage account) as compliant, despite having only one of the two tags. 

 

So is my understanding not correct that both tags should be present? 

 

Best regards, 

Ivan 

Copper Contributor

@Ivan_Mirchev, you need to use anyOf instead of allOf.

Version history
Last update:
‎Nov 10 2020 03:51 PM
Updated by: