Deploying to Multiple Azure Subscriptions with Terraform Provider Aliases

Learn how to deploy resources across multiple Azure subscriptions in a single Terraform project using provider aliases. One codebase, one state file, multiple subscriptions.

Deploying to Multiple Azure Subscriptions with Terraform Provider Aliases
Deploying to Multiple Azure Subscriptions with Terraform Provider Aliases

If, like me, you've been using Terraform for a while, you've probably deployed resources into a single Azure subscription. But what happens when your organization has multiple subscriptions, say, one for development, one for staging, and one for production, and maybe you want to deploy to both development and staging at the same time within one single Terraform project. 

In this post, I'll walk you through exactly how to do that using provider aliases, one of Terraform's most powerful (and beginner-friendly) features.

The Problem

By default, Terraform's azurerm provider targets one Azure subscription. If you need resources in two or three different subscriptions, you might think you need separate Terraform projects for each. That means multiple state files, multiple terraform apply runs, and multiple sets of configuration to keep in sync.

There's a better way.

Terraform Provider Aliases

Terraform lets you declare multiple instances of the same provider, each with a different configuration. You distinguish them using the alias argument. Think of it like opening different browser tabs, each logged into a different Azure subscription, they all work independently and simultaneously.

Here's what that looks like in practice.

Terraform Project Structure

├── providers.tf       # Provider configuration (one per subscription)
├── variables.tf       # Input variables
├── main.tf            # Resources for each subscription
├── outputs.tf         # Output values
└── terraform.tfvars   # Your actual subscription IDs and overrides

Step 1: Define Your Variables

First, we need variables for each subscription ID and its region. We also use a 'prefix' variable for consistent naming via the Azure CAF naming provider.

variable "subscription_id_1" {
  description = "Azure Subscription ID for environment 1"
  type        = string
}

variable "subscription_id_2" {
  description = "Azure Subscription ID for environment 2"
  type        = string
}

variable "subscription_id_3" {
  description = "Azure Subscription ID for environment 3"
  type        = string
}

variable "location_1" {
  description = "Azure region for Subscription 1"
  type        = string
  default     = "switzerlandnorth"
}

variable "location_2" {
  description = "Azure region for Subscription 2"
  type        = string
  default     = "norwayeast"
}

variable "location_3" {
  description = "Azure region for Subscription 3"
  type        = string
  default     = "swedencentral"
}

variable "prefix" {
  description = "Project prefix fed into CAF naming (e.g. project or workload name)"
  type        = string
  default     = "demo"
}

Each subscription gets its own location variable, so you can deploy to different Azure regions if needed.

Step 2: Configure Multiple Providers

This is the core of the technique. In providers.tf, we declare the required providers and then create three aliased instances of azurerm:

terraform {
  required_version = ">= 1.3"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.71, < 5.0.0"
    }
  }
}

# ---------- Subscription 1 ----------
provider "azurerm" {
  alias                = "sub1"
  subscription_id      = var.subscription_id_1
  storage_use_azuread  = true
  features {}
}

# ---------- Subscription 2 ----------
provider "azurerm" {
  alias                = "sub2"
  subscription_id      = var.subscription_id_2
  storage_use_azuread  = true
  features {}
}

# ---------- Subscription 3 ----------
provider "azurerm" {
  alias                = "sub3"
  subscription_id      = var.subscription_id_3
  storage_use_azuread  = true
  features {}
}

What's happening here?

  • Each provider azurerm block has a unique alias — sub1, sub2, sub3.
  • Each one points to a different subscription via subscription_id.
  • 'storage_use_azuread = true' tells the provider to authenticate to storage data planes using Azure AD instead of shared access keys. This is required when your subscriptions enforce a policy that disables key-based authentication on storage accounts (which is a security best practice).

When you run terraform init, Terraform initializes all three provider instances in parallel. There's no sequential "switching", it's three simultaneous connections.

Step 3: Pin Resources to Providers

Now the fun part. In main.tf, each resource declares which provider instance it belongs to using the provider meta-argument:

# This ensures we have unique CAF compliant names for our resources.
module "naming_sub1" {
  source  = "Azure/naming/azurerm"
  version = "0.4.2"
  prefix  = [var.prefix]
  suffix  = ["sub1"]
}

module "naming_sub2" {
  source  = "Azure/naming/azurerm"
  version = "0.4.2"
  prefix  = [var.prefix]
  suffix  = ["sub2"]
}

module "naming_sub3" {
  source  = "Azure/naming/azurerm"
  version = "0.4.2"
  prefix  = [var.prefix]
  suffix  = ["sub3"]
}


# ============================================================
#  Subscription 1
# ============================================================

resource "azurerm_resource_group" "sub1" {
  provider = azurerm.sub1
  name     = module.naming_sub1.resource_group.name
  location = var.location_1
}

resource "azurerm_storage_account" "sub1" {
  provider                  = azurerm.sub1
  name                      = module.naming_sub1.storage_account.name_unique
  resource_group_name       = azurerm_resource_group.sub1.name
  location                  = azurerm_resource_group.sub1.location
  account_tier              = "Standard"
  account_replication_type  = "LRS"
  account_kind               = "StorageV2"
  https_traffic_only_enabled  = true
  min_tls_version             = "TLS1_2"
}


# ============================================================
#  Subscription 2
# ============================================================

resource "azurerm_resource_group" "sub2" {
  provider = azurerm.sub2
  name     = module.naming_sub2.resource_group.name
  location = var.location_2
}

resource "azurerm_storage_account" "sub2" {
  provider                  = azurerm.sub2
  name                      = module.naming_sub2.storage_account.name_unique
  resource_group_name       = azurerm_resource_group.sub2.name
  location                  = azurerm_resource_group.sub2.location
  account_tier              = "Standard"
  account_replication_type  = "LRS"
  account_kind               = "StorageV2"
  https_traffic_only_enabled  = true
  min_tls_version             = "TLS1_2"
}


# ============================================================
#  Subscription 3
# ============================================================

resource "azurerm_resource_group" "sub3" {
  provider = azurerm.sub3
  name     = module.naming_sub3.resource_group.name
  location = var.location_3
}

resource "azurerm_storage_account" "sub3" {
  provider                    = azurerm.sub3
  name                        = module.naming_sub3.storage_account.name_unique
  resource_group_name         = azurerm_resource_group.sub3.name
  location                    = azurerm_resource_group.sub3.location
  account_tier                = "Standard"
  account_replication_type    = "LRS"
  account_kind                = "StorageV2"
  https_traffic_only_enabled  = true
  min_tls_version             = "TLS1_2"
}

The line provider = azurerm.sub1 is the magic. It tells Terraform: "Create this resource using the provider instance aliased as sub1," which means it goes into Subscription 1. Repeat the same pattern for azurerm.sub2 and azurerm.sub3, and you've got resources spread across three subscriptions.

Step 4: Set Your Variables

Create a terraform.tfvars file with your actual subscription IDs:

subscription_id_1 = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
subscription_id_2 = "ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj"
subscription_id_3 = "kkkkkkkk-llll-mmmm-nnnn-oooooooooooo"


# Optional: override the default regions
# location_1 = "switzerlandnorth"
# location_2 = "norwaywest"
# location_3 = "swedencentral"

Remember to remove the hashtag on the location lines if you wish to override the default regions.

If you’re wondering what a terraform.tfvars file is and why we’re using it here, I’ve written a separate guide that walks through it in detail.

Read it here

Step 5: Deploy

terraform init       # Initializes all three provider instances
terraform plan -out=tfplan
terraform apply tfplan

That's it. One plan, one apply, three subscriptions.

Step 6: Check Your Outputs

The configuration exposes the created resource names as outputs:

# ---------- Subscription 1 ----------
output "sub1_resource_group_name" {
  description = "Resource group name in Subscription 1"
  value       = azurerm_resource_group.sub1.name
}

output "sub1_storage_account_name" {
  description = "Storage account name in Subscription 1"
  value       = azurerm_storage_account.sub1.name
}

# ---------- Subscription 2 ----------
output "sub2_resource_group_name" {
  description = "Resource group name in Subscription 2"
  value       = azurerm_resource_group.sub2.name
}

output "sub2_storage_account_name" {
  description = "Storage account name in Subscription 2"
  value       = azurerm_storage_account.sub2.name
}

# ---------- Subscription 3 ----------
output "sub3_resource_group_name" {
  description = "Resource group name in Subscription 3"
  value       = azurerm_resource_group.sub3.name
}

output "sub3_storage_account_name" {
  description = "Storage account name in Subscription 3"
  value       = azurerm_storage_account.sub3.name
}

After terraform apply, you'll see all six names printed in your terminal.

How Does Terraform "Switch" Between Subscriptions?

Short answer: it doesn't. There's no switching at all.

When you run `terraform init`, Terraform creates an authenticated session for each aliased provider. All three sessions exist simultaneously. When Terraform processes a resource, it looks at the `provider` argument and uses the corresponding session. It runs completely parallel, it doesn’t run sequentially. 

All of this is tracked in a single state file. You don't need multiple workspaces or separate runs.

When to Use This Pattern

This approach works well when:

  • You manage a small, fixed number of subscriptions.
  • You want a single state file and a single terraform apply for everything.
  • The resources in each subscription are similar but not identical.

If you have many subscriptions or highly dynamic environments, consider using Terraform modules with for_each and passing providers explicitly.

Wrapping Up

Deploying across multiple Azure subscriptions doesn't require multiple Terraform projects. With provider aliases, you get:

  • One codebase to maintain.
  • One state file to track everything.
  • One terraform apply to deploy across all subscriptions.

The key takeaway: alias creates named instances of a provider, and provider = azurerm.<alias> on a resource tells Terraform which instance (and therefore which subscription) to use. That's all there is to it.

The full source code for this project is available on GitHub. Happy deploying!