Data block working inconsistently when 'count' and 'provider' arguments are used together

Problem description -

We have 2 provider configuration defined in the same script and want to execute a data block with particular provider based on the ‘count’ conditional expression. While the data block behaves normally when only the ‘count’ argument is used, when an additional argument of provider is added, it tries to read the provider configuration even if the condition turns out to be false. Question here is, if the condition is false, why the data block needs to execute the provider block?

Terraform config files -

terraform {
required_providers {
azurerm = {
source = “hashicorp/azurerm”
version = “>=3.0.0”
}
}
}

provider “azurerm” {
subscription_id = xxxxx
tenant_id = xxxxx
client_id = xxxxx
client_secret = xxxxx
features {}
}

provider “azurerm” {
alias = “china”
subscription_id = xxxxx
tenant_id = xxxxx
client_id = xxxxx
client_secret = xxxxx
environment = “china”
features {}
}

/* Using Data Source for fetching resource group details */
data “azurerm_resource_group” “rg” {
count = local.tenant == “SHS” ? 1 : 0
name = split("/", var.resource_id)[4]
}

data “azurerm_resource_group” “rg_china” {
count = local.tenant == “china” ? 1 : 0
provider = azurerm.china
name = split("/", var.resource_id)[4]
}

Note: local.tenant variable gets resolved to “SHS” here.

Debug Output

terraform apply --auto-approve

│ Error: building account: getting authenticated object ID: listing Service Principals: ServicePrincipalsClient.BaseClient.Get(): clientCredentialsToken: received HTTP status 400 with response: {“error”:“invalid_request”,“error_description”:“AADSTS90002: Tenant ‘xxxxx-xxxxx-xxxxx-xxxxx-xxxxx’ not found. Check to make sure you have the correct tenant ID and are signing into the correct cloud. Check with your subscription administrator, this may happen if there are no active subscriptions for the tenant.\r\nTrace ID: e36ac19c-09d3-417e-a5e0-aa09e99a3801\r\nCorrelation ID: 07b73f82-8cef-484e-bdf3-fccc382d68e5\r\nTimestamp: 2022-06-02 13:01:53Z”,“error_codes”:[90002],“timestamp”:“2022-06-02 13:01:53Z”,“trace_id”:“e36ac19c-09d3-417e-a5e0-aa09e99a3801”,“correlation_id”:“07b73f82-8cef-484e-bdf3-fccc382d68e5”,“error_uri”:"https://login.chinacloudapi.cn/error?code=90002"}

│ with provider[“Terraform Registry”].china,
│ on main.tf line 34, in provider “azurerm”:
│ 34: provider “azurerm” {

Expected Behavior

terraform should have skipped executing the data block with the provider argument.

Actual Behavior

data block is getting executed and provider block is getting called.

Steps to Reproduce

  1. Terraform script with 2 provider blocks and a data block with provider and count arguments. (count expression should return false)
  2. terraform init
  3. terraform plan (or) terraform apply

Terraform is acting consistently, but in a way which is not helpful to your use-case.

Terraform processes its configuration in various phases, and discovering which blocks exist, and the references between blocks, including data blocks and provider blocks, happens fairly early on - before count and for_each processing.

Unfortunately, this means it doesn’t support conditionally configuring providers based on whether data/resources turn out to not have any instances, after count/for_each.

hi @maxb thanks for your response. Looks like the default terraform behavior prevents it from executing the count argument before the provider argument.

Can you please suggest me a way to make this script work for 2 different provider configurations with data block should refer the correct provider configuration based on the value passed by the application/user?

Hi @kanagaraj-pandian-p,

From what you have shared it seems like your configuration always uses only one of the two provider configurations, and so you want to not activate the other one at all.

If that is true, I think you can get this effect by using only a single provider block with the provider configuration itself being dynamic based on the local values.

For example, you could add another local value which defines the particular provider configuration settings based on the tenant key, as a map of objects:

locals {
  tenant_azure_config = {
   SHS = {
     environment = null
   }
   china = {
     environment = "china"
   }
  }
}

Then in the single provider block, look up this value based on the tenant key:

provider "azurerm" {
  environment = local.tenant_azure_config[local.tenant]. environment
  # ...
}

I’ve only shown the “environment” argument in this example, but you can do the same for any other that needs to differ based on tenant.

This then means you only need one default provider configuration, and therefore only one resource block. The conditional count for the resource is unnecessary with this design.

1 Like

hi @apparentlymart Thank you very much for your response. We thought of a similar approach to choose the provider based on the ‘tenant’ value. But there is a problem with it as one of the provider uses ‘service principal’ for authentication and the other provider uses ‘managed service identity (msi)’. Hence we need to define and use 2 different provider configuration blocks.

I think one of the approaches might be do it using modules. I am going to try it as the next step. Can you please tell me whether there is any other approach available?

Untested, because I don’t have an Azure subscription, but I think @apparentlymart 's suggestion can extend to accomodate this:

locals {
  tenant_azure_config = {
    SHS = {
      use_msi = true
    }
    china = {
      environment     = "china"
      subscription_id = "00000000-0000-0000-0000-000000000000"
      client_id       = "00000000-0000-0000-0000-000000000000"
      client_secret   = "00000000-0000-0000-0000-000000000000"
      tenant_id       = "00000000-0000-0000-0000-000000000000"
    }
  }
}

provider "azurerm" {
  environment     = try(local.tenant_azure_config[local.tenant].environment, null)
  use_msi         = try(local.tenant_azure_config[local.tenant].use_msi, null)
  subscription_id = try(local.tenant_azure_config[local.tenant].subscription_id, null)
  client_id       = try(local.tenant_azure_config[local.tenant].client_id, null)
  client_secret   = try(local.tenant_azure_config[local.tenant].client_secret, null)
  tenant_id       = try(local.tenant_azure_config[local.tenant].tenant_id, null)
}

Or alternatively you could abandon configuring the provider within Terraform code and set it all via the environment variables that terraform-provider-azurerm looks for (see the provider documentation) - whichever would work best for you.

Thanks for your suggestions @maxb. For us environment variables solution won’t work. I am trying to make it work by separating the public and china providers as modules.