Accessing multiple Azure subscriptions in a single Terraform run
A tale of two cities Azure subscriptions #
Photo by Adrian Schwarz on Unsplash
When infrastructure is declared as Terraform code, resources are usually only created in a single Azure subscription. It normally is best practice to keep multiple subscriptions separated in code, to prevent ending up with a large codebase which can be difficult to maintain and understand. However, there are times when it is necessary, or most logical, to create or query resources from different subscriptions. Luckily, Terraform allows us to work with two (or more) subscriptions in a single run if needed, by means of configuration aliases.
As an example, say we have two subscriptions, one called main
and one secondary
. Each of those subscriptions has its own Terraform repository and resources, including their own Azure Key Vaults. Now, imagine we need to put a newly created password into both of them, since this secret will be used by applications on both subscriptions.
Credentials, a.k.a. Service Principals #
The first thing needed will be credentials for each subscription. We could create a single Service Principal and give it access to both subscriptions, but I recommend dividing up access to reduce the attack surface if any Service Principal was compromised.
If you are already using Terraform, chances are you already have Service Principals which you use for your Terraform runs. If not, we can create two new ones, one for each subscription. I will not get into the specifics of how to do that here, and instead recommend following the official Terraform documentation.
Configuring Terraform to use multiple Azure providers #
With our newly minted Service Principals (SPs) on hand, we can now configure Terraform to use them both.
First, we add some variables to hold the data for both subscriptions and SPs. That way, this sensitive information can be injected at runtime, for example by means of environment variables.
variables.tf #
# The Active Directory tenant ID.
# This one should be the same for both SPs
# and subscriptions
variable TENANT_ID { type=string }
# Data for the "main" subscription and SP
variable SUBSCRIPTION_ID_MAIN { type=string }
variable SERVICE_PRINCIPAL_ID_MAIN { type=string }
variable SERVICE_PRINCIPAL_SECRET_MAIN { type=string }
# Data for the "secondary" subscription and SP
variable SUBSCRIPTION_ID_SECONDARY { type=string }
variable SERVICE_PRINCIPAL_ID_SECONDARY { type=string }
variable SERVICE_PRINCIPAL_SECRET_SECONDARY { type=string }
With the variables in place, we can tell Terraform to use the azurerm provider with two separate configurations. Notice the alias
field in the config for the secondary subscription, which will allow us to specify when we want to use this one. Otherwise, Terraform will default to using the block with no alias
declared.
We also add the random provider, which we will use for creating the password we are saving to both Key Vaults:
config.tf #
terraform {
required_providers {
azurerm = {
version = "~>2.90"
}
random = {
version = "~>3.1"
}
}
}
# Configuration for our "main" subscription
provider "azurerm" {
tenant_id = var.TENANT_ID
subscription_id = var.SUBSCRIPTION_ID_MAIN
client_id = var.SERVICE_PRINCIPAL_ID_MAIN
client_secret = var.SERVICE_PRINCIPAL_SECRET_MAIN
features {}
}
# Configuration for the "secondary" subscription
provider "azurerm" {
alias = "secondary"
tenant_id = var.TENANT_ID
subscription_id = var.SUBSCRIPTION_ID_SECONDARY
client_id = var.SERVICE_PRINCIPAL_ID_SECONDARY
client_secret = var.SERVICE_PRINCIPAL_SECRET_SECONDARY
features {}
}
Terraform is now ready to work with both subscriptions.
Accessing and modifying resources #
With that out of the way, we can create the actual resources we need. We start by finding out data about both Key Vaults. Once again, notice the use of provider
to query the non-default subscription:
data.tf #
# Query data from the default subscription
data "azurerm_key_vault" "main_key_vault" {
name = "kv-main"
resource_group_name = "rg-main"
}
# Query the secondary subscription
data "azurerm_key_vault" "secondary_key_vault" {
provider = azurerm.secondary
name = "kv-secondary"
resource_group_name = "rg-secondary"
}
Now that we have what we need, we can finally create the secret, and store it into both Key Vaults, using the provider
block when appropriate:
shared_secret.tf #
resource "random_password" "shared_password" {
length = 64
}
# Saving the password in the main key vault
resource "azurerm_key_vault_secret" "shared_password_main" {
name = "super-secret-password"
value = random_password.shared_password.result
key_vault_id = data.azurerm_key_vault.main_key_vault.id
}
# And in the secondary, using the "provider" field again
resource "azurerm_key_vault_secret" "shared_password_secondary" {
provider = azurerm.secondary
name = "super-secret-password"
value = random_password.shared_password.result
key_vault_id = data.azurerm_key_vault.secondary_key_vault.id
}
And just like that, we have created our secret and saved it to both Key Vaults. If we ever need to rotate the secret, we can just taint
the single random_password
resource, and Terraform will update all the Key Vaults automatically. Neat!
Final notes #
Something to keep in mind is that all of these resources will be stored in the same terraform-state
file. Also, make sure you don’t end up creating circular dependencies, where each repository needs data from resources created in the other repository. These are a few of the reasons why it is good to be mindful about only using this little trick when appropriate.