Skip to main content

Command Palette

Search for a command to run...

Automating Azure Storage Accounts and Managed Identities with Terraform

Updated
4 min read
Automating Azure Storage Accounts and Managed Identities with Terraform

In this example, we use Terraform to automate the creation of Azure Storage Accounts and their containers for each environment (production, pre-production, and non-production). The goal is to implement a scalable, secure, and segmented architecture where each environment is isolated and has its own User Assigned Managed Identity. This identity enables precise access control, which is essential for meeting enterprise security and compliance requirements.

Final Objective: Target Structure for Storage Accounts and Managed Identities

The final objective of this configuration is to create a structure for each environment with storage accounts and dedicated storage containers, each associated with a specific managed identity. This setup is designed to meet the following needs:

  • Data Security and Isolation: Each environment (production, pre-production, non-production) has its own storage account, containers, and a dedicated managed identity to isolate data and control access.

  • Flexibility and Scalability: Using dynamic variables and loops (for_each), this configuration allows us to add new environments or containers easily without complex code modifications.

  • Compliance and Best Practices: Separating resources by environment with dedicated identities aligns with security standards for access management and data isolation.

Structure and Configuration: Overview

The target structure is defined in the terraform.tfvars file, which specifies the storage account names and containers for each environment, along with the names of the managed identities:

# Storage accounts and their containers, must be ordered by environment
storage_accounts = {
  "prd" = {
    sa_name    = "saprojectprd001"
    containers = ["container-xyz-prd"]
  }
  "ppd" = {
    sa_name    = "saprojectppd001"
    containers = ["container-xyz-ppd"]
  }
  "npd" = {
    sa_name    = "saprojectnpd001"
    containers = ["container-xyz-dev", "container-xyz-qas", "container-xyz-sbx", "container-xyz-poc"]
  }
}

# User Assigned Managed Identity
identity_names = {
  "prd" = "id-project-prd-chn-001"
  "ppd" = "id-project-ppd-chn-001"
  "npd" = "id-project-npd-chn-001"
}
  • Production (prd): A storage account (saprojectprd001) with a container for production data (container-xyz-prd) and a specific managed identity (id-project-prd-chn-001).

  • Pre-production (ppd): A storage account (saprojectppd001) with a container for pre-production data (container-xyz-ppd) and a specific managed identity (id-project-ppd-chn-001).

  • Non-Production (npd): A storage account (saprojectnpd001) with multiple containers (container-xyz-dev, container-xyz-qas, etc.) for development and testing environments and a dedicated managed identity (id-project-npd-chn-001).

This organization provides environment-specific isolation while centralizing resources for non-production environments.


Implementation in Terraform

1. Creating Storage Accounts and Containers with for_each

We use the for_each loop in Terraform to automatically deploy Storage Accounts and their containers for each environment defined in terraform.tfvars.

Deploying Storage Accounts

The following code shows the creation of storage accounts, where each environment has its own account:

resource "azurerm_storage_account" "sa_account" {
  for_each                        = var.storage_accounts
  name                            = each.value.sa_name
  location                        = azurerm_resource_group.this_001.location
  resource_group_name             = azurerm_resource_group.this_001.name
  account_tier                    = var.sa_account_tier
  account_replication_type        = var.sa_account_replication_type
  public_network_access_enabled   = var.sa_account_public_network_access_enabled
  allow_nested_items_to_be_public = var.st_nested_public
  min_tls_version                 = var.st_min_tls_version

  queue_properties {
    logging {
      delete                = var.st_logging_delete
      read                  = var.st_logging_read
      write                 = var.st_logging_write
      version               = var.st_logging_version
      retention_policy_days = var.st_logging_retention
    }
  }

  blob_properties {
    delete_retention_policy {
      days = var.st_soft_delete_retention
    }
  }

  tags = {
    Projet       = var.tags_project
    Facturation  = var.tags_facturation
    Souscription = var.tag_sub_re_chn_dig
  }
}
Creating Containers in Each Storage Account

For the containers, we use flatten to combine each storage account and its containers into a single, manageable list:

locals {
  storage_containers = flatten([
    for env, sa in var.storage_accounts : [
      for container in sa.containers : {
        sa_name   = sa.sa_name
        container = container
        env       = env
      }
    ]
  ])
}

resource "azurerm_storage_container" "sc_account" {
  for_each = { for idx, container in local.storage_containers : "${container.sa_name}-${container.container}" => container }

  name                  = each.value.container
  storage_account_name  = azurerm_storage_account.sa_account[each.value.env].name
  container_access_type = "private"

  depends_on = [
    azurerm_storage_account.sa_account,
    azurerm_private_endpoint.st_pep,
    azurerm_private_dns_a_record.st-record
  ]
}

The for_each loop automatically deploys each container for the corresponding storage account based on the environment.

2. Creating and Assigning Managed Identities per Environment

For each environment, a User Assigned Managed Identity is created and assigned to the corresponding resources.

Defining Managed Identities

Each managed identity is created according to its name specified in identity_names:

resource "azurerm_user_assigned_identity" "this" {
  for_each            = var.identity_names
  resource_group_name = azurerm_resource_group.this_001.name
  location            = azurerm_resource_group.this_001.location
  name                = each.value
}
Assigning Roles to Identities

Finally, we assign the necessary roles to each identity, enabling access to storage containers as required for each environment.

  • Storage Blob Data Contributor: This allows each identity to access storage blobs.
resource "azurerm_role_assignment" "storage_blob_data_contributor" {
  for_each = {
    for env, id_name in var.identity_names : env => flatten([
      for container in var.storage_accounts[env].containers : {
        sa_name   = var.storage_accounts[env].sa_name
        container = container
      }
    ])
  }

  principal_id         = azurerm_user_assigned_identity.this[each.key].principal_id
  scope                = azurerm_storage_account.sa_account[each.key].id
  role_definition_name = "Storage Blob Data Contributor"

  depends_on = [
    azurerm_storage_account.sa_account,
    azurerm_storage_container.sc_account
  ]
}

Conclusion

In summary, this Terraform configuration allows us to deploy Storage Accounts and Managed Identities in an automated and segmented way for each environment. It ensures data separation, access isolation, and provides maximum flexibility to add or modify environments effortlessly. This architecture offers a robust and scalable solution for managing Azure resources and access controls.

More from this blog

D

DINA DevOps Technical's Blog

59 posts