Terraform is a popular Infrastructure as Code solution. Did you know that you can manage other Azure resources, such as policy definitions and assignments with Terraform? In this tutorial, you will learn how to use Terraform to manage Azure Policy by creating a policy definition for a storage account naming standard. You will then assign the policy to a subscription and test the policy's effectiveness.
Latest posts by Jeff Brown (see all)

Prerequisites

To follow along with this tutorial, you will need:

  • An Azure tenant and access to a subscription, like Owner or Contributor rights.
  • VS Code or other IDE. However, VS Code has a Terraform extension to improve the authoring process.
  • Terraform open-source command-line interface (install guide here).
  • Azure CLI (download). This tutorial uses version 2.32.0.

Reviewing Azure Policy

Azure Policy enforces standards and assesses compliance in your cloud environment. You can perform actions such as limiting where you can deploy resources or enforcing tags. Azure Policy doesn't always have to prevent an activity; it can also perform audits. You can view resource compliance for things like storage accounts enabled for HTTPS traffic or whether SQL auditing is enabled.

Azure Policy's primary component is policy definitions. Policy definitions represent business rules that the environment should follow. You use a JSON-formatted language to describe the business rules and actions to take if the resource is noncompliant.

To learn more about Azure Policy, check out these 4Sysops articles:

Creating the Azure Policy

Like any other object in Terraform, you first define the resource—in this case, a policy definition. Before defining the policy definition, set up the foundations of a Terraform configuration by configuring the AzureRM provider. The AzureRM provider interacts with the Azure Resource Manager APIs to create and manage Azure resources. To see the completed tutorial code, reference the 4sysops_tf_azpolicy GitHub repository.

Configure the AzureRM Provider

In a file named main.tf, create a Terraform configuration block with a required_providers section. This example sets the version to 3.13.0, but you can choose a newer or older version as needed. In the next section, you'll learn about some crucial differences HashiCorp made regarding Azure Policy between versions 2.0.0 and 3.0.0.

Next, create an azuremrm provider block with an empty features block. This example does not set any options, such as the subscription or storing authentication information. You will use the Azure CLI later in the tutorial to authenticate and select a subscription.

terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      verversion = "3.13.0"
     }
  }
}

provider "azurerm" {
  features {}
}

Create the policy definition

Next, create the policy definition using the azurerm_policy_definition resource type. This example policy enforces a naming convention for storage accounts. The policy definition has four required arguments:

  • name: Policy definition short name ("StorageAccountNamingConvention").
  • display_name: Policy definition display name ("Storage Accounts should follow naming convention").
  • mode: Policy mode to specify which resource types Azure evaluates the policy against ("Indexed").
  • policy_type: Policy type of "BuiltIn," "Custom," or "NotSpecified." This example uses "Custom."
resource "azurerm_policy_definition" "sa-naming-convention" {
  name         = "StorageAccountNamingConvention"
  display_name = "Storage Accounts should follow naming convention"
  mode         = "Indexed"
  policy_type  = "Custom"

  # Addition policy definition code...  
}

Define metadata

While not required, you can define metadata for the policy definition. Metadata includes a version and a category. You use the Azure Policy JSON syntax inside the HashiCorp Terraform Language (HCL) when defining a policy. You place the JSON code inside a jsonencode() function. This function allows Terraform to interpret the JSON code and guarantee the correct syntax.

This example sets the policy version to "1.0.0" and the category to "Storage." Place this code in the same resource definition block as in the previous section.

  metadata = jsonencode({
    "version" : "1.0.0",
    "category" : "Storage"
    }
  )

Define the parameters

Next, define any parameters used by the policy. In this example, there are two:

  • effectAction: The effect action is a string type that defines what action the policy takes. The two options are "Audit" or "Deny." Set the default value to "Audit."
  • namingPattern: This parameter is a string and sets the naming pattern the storage account should follow. Use a question mark (?) for letters and a pound symbol (#) for numbers. Set the default value to "4sysops???####", which translates to the prefix "4sysops" followed by three letters and four numbers.

Here is the parameters argument code block, which you place below the metadata argument code block. Continue to use the jsonencode() function so Terraform can validate the JSON syntax.

  parameters = jsonencode({
    "namingPattern" : {
      "type" : "String",
      "metadata" : {
        "displayName" : "Naming Pattern",
        "description" : "Storage Account naming pattern. Using ? for letters, # for numbers."
      },
      "defaultValue" : "4sysops???####"
    },

    "effectAction" : {
      "type" : "String",
      "metadata" : {
        "displayName" : "Effect Action",
        "description" : "The effect action for the policy (Audit or Deny)."
      },
      "allowedValues" : [
        "Audit",
        "Deny"
      ],
      "defaultValue" : "Audit"
    }
    }
  )

Define the policy rule

Next is the policy rule. The policy rule is the core of the definition, as it describes the resource conditions to match and the effect to take. Policy rules compare a resource property field or value to the required value.

In this example, the policy rule contains two conditions, both of which the resource must match for the policy to apply:

  • The resource type must be of type "Microsoft.Storage/storageAccounts".
  • The resource name does not match the value in the namingPattern parameter.

If both conditions are true, then the policy performs that action set in the effectAction parameter (remember, the default effect action is "Audit").

Here is the example code for the policy rule, which goes below the parameters argument code block. Again, use the jsonencode() function to wrap the JSON code.

  policy_rule = jsonencode({
    "if" : {
      "allOf" : [
        {
          "field" : "type",
          "equals" : "Microsoft.Storage/storageAccounts"
        },
        {
          "field" : "name",
          "notmatch" : "[parameters('namingPattern')]"
        }
      ]
    },
    "then" : {
      "effect" : "[parameters('effectAction')]"
    }    
  })

Deploying the Azure Policy

Once your policy is defined, assigning it is next. You can set a policy for multiple scopes, such as a management group, subscription, resource group, or even an individual resource.

HashiCorp recently changed the method for creating assignments using the AzureRM provider. The sections below outline the two different ways, in case you run into code using an older version and need to understand it.

AzureRM Provider before 3.0

Before version 3.0, the AzureRM provider had a single resource for assigning policies, called azurerm_policy_assignment. You used this resource to assign policies to each scope. The resource had a single argument named scope that accepted a resource ID at the assignment level.

Here is some example code showing the syntax and how this worked. Do not add this to your main.tf file from the rest of this tutorial. This code is just to show how this assignment worked previously. To see a complete example, check out the HashiCorp documentation here.

resource "azurerm_policy_assignment" "example" {
  name                 = "example-policy-assignment"
  scope                = "/subscriptions/00000000-0000-0000-000000000000"
  policy_definition_id = "/subscriptions//00000000-0000-0000-000000000000/providers/Microsoft.Authorization/policyDefinitions/StorageAccountNamingConvention"
  
  # ... additional code here
}

AzureRM Provider after 3.0

With version 3.0, AzureRM has multiple resources for applying policy definitions to different scopes.

In this example, assign the policy to the subscription scope using azurermsubscriptionpolicyassignment. Give the policy assignment a name followed by the subscription ID (replace zeroes with your actual subscription ID).

Reference the policy defined earlier in the configuration using the resource type (azurerm_policy_definition), the symbolic name (sa-naming-convention), and the ID output from the resource. There are additional arguments you can use for policy assignment; review the documentation here.

resource "azurerm_subscription_policy_assignment" "demo" {
  name                 = "storage-account-naming-standard-demo"
  subscription_id      = "/subscriptions/00000000-0000-0000-000000000000"
  policy_definition_id = azurerm_policy_definition.sa-naming-convention.id  
}

Parameter values

Remember that this policy has two parameters: namingPattern and effectAction. Since you defined these parameters with a default vault, there is no need to pass a value when assigning the policy to a scope. However, if a parameter does not have a default value or you don't want to use the default, you set parameter values in this assignment resource declaration.

Use the parameters argument followed by the jsonencode() function, much like when you defined the metadata, parameters, and policy rule sections. This example sets the parameter effectAction to "Deny" instead of the default of "Audit."

  parameters = jsonencode({
    "effectAction": {
      "value": "Deny"
    }
  })

Terraform configuration deployment

With the Terraform configuration written, it is time to deploy the policy definition and assignment to your tenant.

Subscribe to 4sysops newsletter!

  1. Use the az login command to log in to your Azure tenant.
  2. If necessary, select the subscription where you want to deploy the resources using the az account set command. This example sets the context to a subscription named "Demo."
    az account set --subscription "Demo"
  3. Use the terraform init command to initialize your Terraform working directory.
  4. Use the terraform plan command to validate and see the planned resource deployment.
  5. Once satisfied with the plan output, use the terraform apply command to deploy the configuration. When prompted, enter yes to authorize the deployment.
  6. Navigate to the Azure portal (https://portal.azure.com) and search for "policy." Select the Policy service from the results.

    Searching in the Azure portal

    Searching in the Azure portal

  7. Under Authoring, select Definitions. In the Search box, search for the policy using its name. Select the policy from the results.

    Searching the policy

    Searching the policy

  8. On the policy definition page, review the Essentials section for the definition metadata; most of it comes from your Terraform configuration. You can review the definition code on the Definition tab and view where the policy is assigned under the Assignments tab.

    Viewing the policy definition

    Viewing the policy definition

  9. Try creating a storage account using an incorrect naming convention. This example swaps the numbers ("1234") with the three-letter identifier ("abc"). A policy validation error should appear, stating that the name does not meet the requirements.

    Testing the naming policy

    Testing the naming policy

Summary

In this tutorial, you learned how to define an Azure Policy and assign the policy to a scope using Terraform. Terraform doesn't just have to be for virtual machines and storage accounts. Terraform can also control your governance strategy by codifying Azure Policy definitions and assignments. By defining the policies as code, you can quickly restore definitions and assignments if someone changes them outside Terraform.

avatar
0 Comments

Leave a reply

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2023

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account