- Manage Azure Policy using Terraform - Tue, Aug 2 2022
- Getting started with Terraform in Azure - Tue, Jul 12 2022
- Azure Bicep: Getting started guide - Fri, Nov 19 2021
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.
- azurerm_management_group_policy_assignment for assigning to management groups
- azurerm_subscription_policy_assignment for assigning to subscriptions
- azurerm_resource_group_policy_assignment for assigning to resource groups
- azurerm_resource_policy_assignment for assigning to a single resource
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!
- Use the az login command to log in to your Azure tenant.
- 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" - Use the terraform init command to initialize your Terraform working directory.
- Use the terraform plan command to validate and see the planned resource deployment.
- Once satisfied with the plan output, use the terraform apply command to deploy the configuration. When prompted, enter yes to authorize the deployment.
- Navigate to the Azure portal (https://portal.azure.com) and search for "policy." Select the Policy service from the results.
- Under Authoring, select Definitions. In the Search box, search for the policy using its name. Select the policy from the results.
- 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.
- 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.
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.