Inter module dependency using output variables

I am seeing an odd issue where my parent module is not respecting the implicit dependency on the child module output variable.

In the below example, I have added an implicit dependency in the parent module “app_zzz” to an output variable of module “asp_zzz” but this config fails and does not wait for the asp_zzz module to complete.

Error Message

Error: Error: App Service Plan "asp-zzz" (Resource Group "rgp-zzz") was not found

  on ..\modules\appservice\app_service\main.tf line 5, in data "azurerm_app_service_plan" "asp":
   5: data "azurerm_app_service_plan" "asp" {

Terraform Config

module "asp_zzz" {
  source              = "../modules/appservice/app_service_plan"
  name                = "asp-zzz"
  resource_group_name = azurerm_resource_group.rgp.name
  kind                = "Windows"
  sku_tier            = "Premium"
  sku_size            = "V2"
}

module "app_zzz" {
  source                          = "../modules/appservice/app_service"
  name                            = "app-zzz"
  resource_group_name             = azurerm_resource_group.rgp.name
  app_service_plan_name           = module.asp_zzz.name_from_output
  https_only                      = true
  always_on                       = true
  dotnet_framework_version        = "v4.0"
}

Hi @dj-singh,

I think in this case the problem comes from the fact that Terraform v0.13 and earlier treat a data source as ready to read as soon as all of its configuration arguments are known, which happens immediately in your case because the resource group name is defined statically in the configuration.

I’ve typically recommended against having a configuration both manage and object and read that same object, because it creates strange situations like these where we’re trying to read an object that’s also due for creation. Instead of having the app_zzz module redundantly read back information that Terraform already determined while working on the other module, I’d suggest passing the object the first module generated.

In your asp_zzz module you can output all of the relevant information that another module might need in order to interact with that service plan. For example:

output "name" {
  value = azurerm_app_service_plan.example.name
}

output "id" {
  value = azurerm_app_service_plan.example.id
}

output "app_service_environment_id" {
  value = azurerm_app_service_plan.example.app_service_environment_id
}

# ...and anything else you'd consider to be
# part of the "public interface" of your
# "app service plan" module.

The value representing a module is an object whose attributes match the output values, so the calling module can use module.asp_zzz to refer to this entire object. You can pass it as a whole into the app_zzz module, like this:

module "app_zzz" {
  source = "../modules/appservice/app_service"

  app_service_plan = module.asp_zzz
  # ...
}

Inside the ../modules/appservice/app_service module, you can declare this app_service_plan variable with an object type constraint that covers the subset of the “asp” module’s outputs that the “app” module needs to do its work. For example:

variable "app_service_plan" {
  type = object({
    name = string
    id   = string
    # I omitted the app_service_environment_id
    # attribute to illustrate that you can omit
    # any output values that this module won't
    # actually use.
  })
}

In the ../modules/appservice/app_service module you can then delete the data "azurerm_app_service_plan" "asp" block and just refer to attributes from var.app_service_plan directly instead. This causes the data to flow between modules within Terraform itself, rather than indirectly via redundantly re-reading the data.


With all of that said, the forthcoming Terraform v0.14 will include a change to Terraform’s handling of data resources where they’ll be handled as part of the planning phase rather than as part of the refresh phase, and so something like what you tried before will probably work.

This practice of reading an object you’re also managing will still be redundant – it’s unnecessary to re-fetch data Terraform already has in memory – but Terraform will at least be able to understand better your intent to read the object back only after it’s been created.

For that reason, if you’d prefer not to do the refactoring I described above then you could potentially wait for Terraform 0.14 (due later this month), and approximate the forthcoming Terraform 0.14 behavior for now by forcing Terraform to ignore the data resource on the first run, using the -target option:

terraform apply -target=module.asp_zzz.azurerm_app_service_plan.example

(where .example above is a placeholder for the name of your azurerm_app_service_plan resource.)

If you’re not already using this configuration in production yet, you could also try it with the v0.14.0 beta releases. If you’re able to try it, I’d love to hear about what happens. (Please don’t use v0.14.0 betas if this configuration is already deployed in production, though.)

Hi @apparentlymart

Thanks for your inputs and suggestions. I have never had this issue before where this implicit dependency between modules is not respected. For what it’s worth, if I use the “id” output variable from my “asp” module, the dependency works. I think the only reason it is behaving differently for “name” is because it is statically defined in the “asp” module.

The below code works fine as now the “app service” module must wait for the “id” which will be available after the “app service plan” is created.

module "asp_zzz" {
  source              = "../modules/appservice/app_service_plan"
  name                = "asp-zzz"
  resource_group_name = azurerm_resource_group.rgp.name
  kind                = "Windows"
  sku_tier            = "Premium"
  sku_size            = "V2"
}

module "app_zzz" {
  source                          = "../modules/appservice/app_service"
  name                            = "app-zzz"
  resource_group_name             = azurerm_resource_group.rgp.name
  app_service_plan_name           = module.asp_zzz.id
  https_only                      = true
  always_on                       = true
  dotnet_framework_version        = "v4.0"
}
``