Why might you want to create a Terraform module for Azure Resource Group?

At first, creating a Terraform module for a such a simple service like Resource Group seems like a bad idea. However, there are cases where you might want to do so to do more than just deploy a resource group.

Resource group service

A resource group is a service, like any other. Even though there is not much that can be set, a module just for that might be useful. Let's see what can be configured by Terraform.

resource "azurerm_resource_group" "cloudoing_rg" {
  name     = "rg-cloudoing"
  location = "Poland Central"
}
sample code creating resource group

Documentation of AzureRM provider says that's it—there are no options to configure more.

Let's take a step further and think what else you might want to do during resource group creation.

Standardizing IAM

Standardizing a way of granting access to Azure resources is not an easy task in big environments. Especially when there are already numerous services deployed. Good practice stands that access should be granted by a group membership, but what to do if the team does not always follow that requirement? In that case, the groups are created for some resource groups and for some are not. If there was only such a thing that can solve that issue.

There is – a terraform module!

Standardizing resource group IAM using terraform

The terraform resource azurerm_role_assignment helps to manage roles' assignment on all resources' organization levels. Using that resource, we can assign a role to a user or group on management group, subscription, resource group or even a single resource.

resource "azurerm_role_assignment" "cloudoing_role_assignment" {
  scope                = <subscription-id>
  role_definition_name = "Reader"
  principal_id         = <object-id>
}

It is compelling and supports various configurations. It is worth taking a closer look at the azurerm_role_assignment resource's documentation.

To manage AAD groups, we have to look beyond resource group, even beyond azurerm provider. For that requirement, let's use an additional provider - azuread.

The Azure AD Provider can be used to configure infrastructure in Azure Active Directory using the Microsoft Graph API.

The azuread provider allows creation of azuread_group resource. A sample definition looks like that.

resource "azuread_group" "cloudoing_aad_group" {
  display_name     = "cloudoing"
  owners           = "00000000-0000-0000-0000-000000000000"
  security_enabled = true
}

Ok, now we know that resource group, role assignment and groups management can be handled by Terraform. How to combine all of that to a module that standardizes resource group creation?

Resource Group module

Our first focus needs to be a code that provisions resource groups. A following simple code does the job.

resource "azurerm_resource_group" "cloudoing_resource_group" {
  name     = var.resource_group_object.name
  location = var.resource_group_object.location
}
main.tf

Next we need to define variables. Our resource group module requires a name and a location to be provisioned.

variable "resource_group_object" {
  type = object({
    name             = string
    location         = string
  })
}
variables.tf
💡
Just a note - I do prefer to use object when defining variables. It helps maintain code more readable at later stage. You do not have to follow that principle

The next thing we need is a part that will create AAD group. Let's start by creating a group that will be used to grant owner rights.

The group display name will be generated automatically, based on subscription name and resource group name. Use any combination of data that will address your naming convention or create a variable(s) to define group names. My group name starts with AAD prefix, followed by dynamically pulled subscription name. The next part of the name consist the created resource group name. At the end, I do put the name of the role the group will grant.

resource "azuread_group" "cloudoing_group_owner" {
  display_name     = "AAD-${data.azurerm_subscription.current.display_name}-${azurerm_resource_group.cloudoing_resource_group.name}-Owner"
  owners           = [data.azuread_client_config.current.object_id]
  security_enabled = true
}

Groups cannot be created with no owners. We can leave this parameter optional or define an owner. In our example, we dynamically pull the object_id of the user who is executing the code.

To dynamically get info about the subscription name and object_id, two data resources are required.

Just a brief look at how a main file looks now.

data "azuread_client_config" "current" {}
data "azurerm_subscription" "current" {}

resource "azurerm_resource_group" "cloudoing_resource_group" {
  name     = var.resource_group_object.name
  location = var.resource_group_object.location
}

resource "azuread_group" "cloudoing_group_owner" {
  display_name     = "AAD-${data.azurerm_subscription.current.display_name}-${azurerm_resource_group.cloudoing_resource_group.name}-Owner"
  owners           = [data.azuread_client_config.current.object_id]
  security_enabled = true
}
main.tf

Resource group is ready, group creation is there. The last step is to assign permissions for a group on a resource group.

We start by setting up the scope. In our case, this would be a resource group. By using a reference to resource group definition azurerm_resource_group.cloudoing_resource_group.id, we can dynamically pass resource group id to scope. The next step is to define a role that will be assigned. Simply put there a name of a role (built in or custom). Here, the Owner role will be assigned. Principal_id defines on what object role will be assigned. In our case, we do the similar trick we did for resource group – reference previously created group id. The last thing is to set a meaningful description. Using a reference trick again, the resource group name gets passed automatically to the description.

resource "azurerm_role_assignment" "cloudoing_owner_role_assignment" {
  scope                = azurerm_resource_group.cloudoing_resource_group.id
  role_definition_name = "Owner"
  principal_id         = azuread_group.cloudoing_group_owner.id
  description          = "Group granting owner rights on ${azurerm_resource_group.cloudoing_resource_group.name}"
}
main.tf

Let's take a look at a code again.

data "azuread_client_config" "current" {}
data "azurerm_subscription" "current" {}

resource "azurerm_resource_group" "cloudoing_resource_group" {
  name     = var.resource_group_object.name
  location = var.resource_group_object.location
}

resource "azuread_group" "cloudoing_group_owner" {
  display_name     = "AAD-${data.azurerm_subscription.current.display_name}-${azurerm_resource_group.cloudoing_resource_group.name}-Owner"
  owners           = [data.azuread_client_config.current.object_id]
  security_enabled = true
}

resource "azurerm_role_assignment" "cloudoing_owner_role_assignment" {
  scope                = azurerm_resource_group.cloudoing_resource_group.id
  role_definition_name = "Owner"
  principal_id         = azuread_group.cloudoing_group_owner.id
  description          = "Group granting owner rights on ${azurerm_resource_group.cloudoing_resource_group.name}"
}
main.tf

Having just an owner is not enough, let's create a similar setup for Contributor and Reader roles. This can be easily done by copying and pasting azuread_group and azurerm_role_assignment resource blocks twice and changing role names.

The code creating resource group, three AAD groups and their roles' assignment is ready.

data "azuread_client_config" "current" {}
data "azurerm_subscription" "current" {}

resource "azurerm_resource_group" "cloudoing_resource_group" {
  name     = var.resource_group_object.name
  location = var.resource_group_object.location
}

resource "azuread_group" "cloudoing_group_owner" {
  display_name     = "AAD-${data.azurerm_subscription.current.display_name}-${azurerm_resource_group.cloudoing_resource_group.name}-Owner"
  owners           = [data.azuread_client_config.current.object_id]
  security_enabled = true
}

resource "azurerm_role_assignment" "cloudoing_owner_role_assignment" {
  scope                = azurerm_resource_group.cloudoing_resource_group.id
  role_definition_name = "Owner"
  principal_id         = azuread_group.cloudoing_group_owner.id
  description          = "Group granting owner rights on ${azurerm_resource_group.cloudoing_resource_group.name}"
}

resource "azuread_group" "cloudoing_group_reader" {
  display_name     = "AAD-${data.azurerm_subscription.current.display_name}-${azurerm_resource_group.cloudoing_resource_group.name}-Reader"
  owners           = [data.azuread_client_config.current.object_id]
  security_enabled = true
}

resource "azurerm_role_assignment" "cloudoing_reader_role_assignment" {
  scope                = azurerm_resource_group.cloudoing_resource_group.id
  role_definition_name = "Reader"
  principal_id         = azuread_group.cloudoing_group_reader.id
  description          = "Group granting reader rights on ${azurerm_resource_group.cloudoing_resource_group.name}"
}

resource "azuread_group" "cloudoing_group_contributor" {
  display_name     = "AAD-${data.azurerm_subscription.current.display_name}-${azurerm_resource_group.cloudoing_resource_group.name}-Contributor"
  owners           = [data.azuread_client_config.current.object_id]
  security_enabled = true
}

resource "azurerm_role_assignment" "cloudoing_contributor_role_assignment" {
  scope                = azurerm_resource_group.cloudoing_resource_group.id
  role_definition_name = "Contributor"
  principal_id         = azuread_group.cloudoing_group_contributor.id
  description          = "Group granting contributor rights on ${azurerm_resource_group.cloudoing_resource_group.name}"
}
main.tf
Files
You can download finished module here.

Implementing module

Be sure to call two providers - azurerm and azuread. Both are required to run the code. In my case, the providers' initialization of backend setup looks like that.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.50.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "2.36.0"
    }
  }
  backend "azurerm" {
    storage_account_name = "stcldng125414demo"
    container_name       = "tfstate"
    key                  = "rg-cloudoing-demo.tfstate"
    use_azuread_auth = true
  }
}

provider "azurerm" {
  features {}
}

provider "azuread" {
}

To call a module, add the following code to a main.tf or create a separate file.

module "rg" {
  source = "git::https://dev.azure.com/cloudoingdemo/terraform-azurerm-modules/_git/resource-group"

  resource_group_object      = var.rg-cloudoing-demo
}

Initialize variable rg-cloudoing-demo in variables.tf

variable "rg-cloudoing-demo" {}

Finally, create a terraform.tfvars file and set values.

rg-cloudoing-demo = {
  name       = "rg-cloudoing-demo"
  location   = "westeurope"
}
Files
You can pull the example code from my Azure DevOps repository.

Time to deploy

Let's start by initializing the Terraform

terraform init

After successful initialization, it's time to plan the deployment.

terraform plan

In total 7 resources will be created. Everything looks ok, let's apply.

terraform apply

Apply was completed successfully. Resource group, AAD groups and permission were applied.

IAM of created group

Summary

The power of terraform modules shine there. By combining multiple resource types, we standardized resource group deployments. Even such a trivial thing like a resource group can benefit from creating a module. Just imagine what could be achieved for more advanced services. I hope that post would inspire you how to build templates/modules that help you shape and maintain your environment.  You can download the module and example implementation from my Azure DevOps organization.

Thank you

You've successfully subscribed to Cloudoing
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.