Managing Hub-and-Spoke architectures in Azure can be a challenge when dealing with multiple subscriptions. This guide, originally published on devopsstart.com, explains how to use Terraform provider aliases to streamline your deployments.
How to Manage Multiple Azure Subscriptions in Terraform
To deploy resources across multiple Azure subscriptions in a single Terraform configuration, you must use provider aliases. By default, the azurerm provider targets only one subscription based on your authentication context. To override this, you define multiple provider blocks, assigning an alias to each and specifying a unique subscription_id.
This pattern is essential for Hub-and-Spoke network architectures. In these environments, central shared services (like Azure Firewall or ExpressRoute) live in a Hub subscription, while application workloads reside in separate Spoke subscriptions. Without aliases, you would be forced to run separate Terraform states and pipelines for every single subscription, which makes cross-subscription networking a manual nightmare.
You can find the complete provider specification in the official Terraform Azure Provider documentation.
Implementing Provider Aliases
To start, you need to configure your providers.tf file. The provider without an alias becomes the default. Any provider with an alias must be explicitly called when defining a resource using the provider meta-argument.
# providers.tf
# Default provider (Spoke Subscription)
provider "azurerm" {
features {}
subscription_id = "00000000-0000-0000-0000-000000000000"
}
# Aliased provider (Hub Subscription)
provider "azurerm" {
alias = "hub"
features {}
subscription_id = "11111111-1111-1111-1111-111111111111"
}
When you create a resource, use the provider argument to tell Terraform which subscription to use. If you omit this, Terraform defaults to the primary provider.
# Deploy a VNet in the Hub subscription
resource "azurerm_virtual_network" "hub_vnet" {
provider = azurerm.hub
name = "hub-vnet"
address_space = ["10.0.0.0/16"]
location = "eastus"
resource_group_name = "hub-rg"
}
# Deploy a VNet in the Spoke subscription (default provider)
resource "azurerm_virtual_network" "spoke_vnet" {
name = "spoke-vnet"
address_space = ["10.1.0.0/16"]
location = "eastus"
resource_group_name = "spoke-rg"
}
Cross-Subscription Data Referencing
A common production scenario involves fetching an existing resource ID from a Hub subscription to use as a property in a Spoke resource, such as creating a VNet peering. In my experience, this is where most "Resource Not Found" errors occur because the data block defaults to the wrong subscription.
# Fetch Hub VNet ID from the Hub subscription
data "azurerm_virtual_network" "hub_vnet_data" {
provider = azurerm.hub
name = "hub-vnet"
resource_group_name = "hub-rg"
}
# Create peering in the Spoke subscription pointing to the Hub
resource "azurerm_virtual_network_peering" "spoke_to_hub" {
name = "spoke-to-hub"
resource_group_name = "spoke-rg"
virtual_network_name = azurerm_virtual_network.spoke_vnet.name
remote_virtual_network_id = data.azurerm_virtual_network.hub_vnet_data.id
}
By explicitly assigning provider = azurerm.hub to the data block, Terraform authenticates against the Hub subscription to retrieve the ID before attempting to create the peering in the Spoke subscription.
The Module Provider Gotcha
The biggest mistake engineers make with multi-subscription setups is assuming modules inherit aliases automatically. They do not. If you call a module and it contains azurerm resources, those resources will use the default provider regardless of where the module is called from.
To fix this, you must explicitly pass the aliased provider into the module using the providers map.
module "spoke_workload" {
source = "./modules/workload"
# Map the module's internal 'azurerm' provider to the 'hub' alias
providers = {
azurerm = azurerm.hub
}
vnet_id = data.azurerm_virtual_network.hub_vnet_data.id
}
Inside the module code, do not define a provider block. Just use the standard azurerm resource blocks; the mapping happens at the root level. This ensures your modules remain reusable across different environments. I have seen this fail in clusters with >50 nodes where a missed provider mapping caused a production workload to be deployed into a development subscription, leading to significant security audit failures. To maintain high reliability, consider testing your infrastructure as code.
Best Practices for Naming and Scale
Avoid generic names like azurerm.sub1 or azurerm.secondary. In a production environment with dozens of subscriptions, these names provide zero context and lead to configuration errors. Use functional names that describe the role of the subscription:
azurerm.hubazurerm.shared_servicesazurerm.prod_workloadazurerm.identity_mgmt
In environments with more than 50 subscriptions, managing these aliases in a single providers.tf file becomes brittle. At that scale, I recommend splitting your state files by subscription or using a wrapper tool. This reduces the blast radius of a single terraform apply and decreases the time spent in the "refreshing state" phase, which can otherwise take several minutes.
FAQ
Can I use the same Service Principal for multiple subscriptions?
Yes, as long as that Service Principal has the required RBAC roles (for example, Contributor) across all targeted subscriptions. Terraform handles the switching via the subscription_id field in the provider block.
Do I need to run az account set before running Terraform?
No. When you explicitly define subscription_id in the provider block, Terraform ignores the current active subscription in your Azure CLI session and targets the ID specified in the code.
Does using aliases increase the plan time?
Slightly. Terraform must establish separate API sessions for each provider instance. In very large environments, this can add 10 to 30 seconds to the refresh phase.
Conclusion
Using provider aliases is the only professional way to handle multi-subscription Azure deployments. By separating your Hub and Spoke configurations and explicitly passing providers to your modules, you eliminate the risk of deploying resources to the wrong environment.
Your next steps should be to:
- Audit your current
providers.tfand rename any generic aliases to functional names. - Check your module calls to ensure
providers = { ... }is being used for all non-default subscriptions. - Implement data blocks to automate the linkage between Hub and Spoke resources.
Top comments (0)