Passing additional block to module

Hello

I created a bunch of modules and thus far everything is working as intended. I am stuck with something which seems like it should be simple but for the life of me I cannot wrap my head around.

So I have a module to create an Azure app service plan.
Another module to create an Azure webapp (using the above app service plan)
I re-use the webapp module several times to create mutliple webapps.

Question is: Is (and how?) it possible to pass the additional blocks to the webapp module as opposed to have them defined in the module itself? I am referring to specifically the “app settings” & “connection string” blocks. My module currently looks like this:

modules/webapp.tf

 resource "azurerm_app_service" "app" {
  name                = "${lower(var.environment)}-${lower(var.application)}${var.apptype != "" ? "-${var.apptype}-" : "-"}${lower(var.regionabrv)}-wa"
  location            = var.region
  resource_group_name = var.resourcegroup
  app_service_plan_id = var.appservice
  https_only = "true"

  site_config {
    dotnet_framework_version = "v4.0"
    scm_type                 = "None"
    always_on                 = "true"
    use_32_bit_worker_process = "true"
    remote_debugging_enabled = "false"

    default_documents = [
    "Default.html",
    ]
  }

From my main file I call the module like this:
…main.tf

  module "webapp" {
  source = "../../_modules/webapp"
  resourcegroup = module.resourcegroup.resourcegroup_info.resourcegroup_name
  appservice = module.web-serviceplan.appserviceplan_info.app_service_plan_id
  environment = var.environment
  application = var.application
  region = var.region
  regionabrv = var.regionabrv
  apptype = "web"
  appsettings = var.appsettings
}

The missing piece is adding something like the below block (which might contain generated values coming from other modules defined in main.tf, or something different depending on which webapp is being created. Same for connection string block) to the webapp in flight:

app-settings = {
    "key1" = "value1"
    "APPINSIGHTS_INSTRUMENTATIONKEY" = "${module.appinsights.api_key}"
    "key2" = "value2"
    "key3" = "value3"
    "key4" = "value4"
    "msmq:ConString" = "${module.servicebus.default_primary_connection_string}"

Apologies if post is lengthy, but any help would be much appreciated.

Hi @CyanMass45!

It looks like app_settings in azurerm_app_service is just an argument that takes a map value rather than a nested block, so you can pass that through a variable of your module without doing anything special relative to the other values:

variable "app_settings" {
  type    = map(string)
  default = {}
}

resource "azurerm_app_service" "app" {
  # ...

  app_settings = var.app_settings
}

The caller can then just set this to any map value. That can either be a literal map, or a map constructed dynamically from other data using a complex expression:

module "webapp" {
  source = "../../_modules/webapp"

  # ...
  app_settings = {
    APPINSIGHTS_INSTRUMENTATIONKEY = module.appinsights.api_key
  }
}

If you wanted to do something similar for one of the nested blocks of this resource type, like connection_string, a little more ceremony is required so Terraform can validate that all of the values are set properly, since a nested block is a fixed structure rather than an arbitrary map value:

variable "connection_strings" {
  # (notwithstanding that it's confusing for a
  # variable whose name ends in _strings to
  # have an object type...)
  type = list(object({
    name = string
    type = string
    value = string
  }))
  default = []
}

resource "azurerm_app_service" "app" {
  # ...

  dynamic "connection_string" {
    for_each = var.connection_strings
    content {
      name  = connection_string.value.name
      type  = connection_string.value.type
      value = connection_string.value.value
    }
  }
}

This uses dynamic blocks to produce one connection_string block for each element of var.connection_strings.

The caller of the module can then set it to any value that is a list of the expected object type, again either literally or constructed dynamically using a complex expression:

module "webapp" {
  source = "../../_modules/webapp"

  # ...
  connection_strings = [
    {
      name  = "example"
      type  = "example"
      value = "example"
    },
  ]
}

The specific type constraint on variable "connection_strings" allows Terraform to validate that the caller is passing a value with the correct structure, and produce an error referring to the argument inside the calling module block if so.

2 Likes

That worked perfectly, thanks for the help!