How to extend terraform module input variable schema without breaking existing clients?

Viewed 318

I have a module with the following input variable:

variable "apsvc_map" {
  description = "The App Services sharing the same App Service Plan. Maps an App Service name to its properties."
  type        = map(object({ 
    identity_ids = list(string),
    disabled = bool
  }))
}

Now I would like to add a new property to the schema - no_custom_hostname_binding. The new version would be:

variable "apsvc_map" {
  description = "The App Services sharing the same App Service Plan. Maps an App Service name to its properties."
  type        = map(object({ 
    identity_ids = list(string),
    disabled = bool
    no_custom_hostname_binding = bool
  }))
}

And this change can be made backwards compatible in the module code with the help of the try function, because omitting the new property is equivalent to providing it with the false value.

However, terraform treats this schema strictly and would not allow passing an input without the new field:

2020-05-30T15:34:20.8061749Z Error: Invalid value for module argument
2020-05-30T15:34:20.8062005Z 
2020-05-30T15:34:20.8062205Z   on ..\..\modules\web\main.tf line 47, in module "web":
2020-05-30T15:34:20.8062336Z   47:   apsvc_map = {
2020-05-30T15:34:20.8062484Z   48:     dfhub = {
2020-05-30T15:34:20.8062727Z   49:       disabled     = false
2020-05-30T15:34:20.8065156Z   50:       identity_ids = [local.identity_id]
2020-05-30T15:34:20.8065370Z   51:     }
2020-05-30T15:34:20.8065459Z   52:   }
2020-05-30T15:34:20.8065538Z 

I understand from the error that terraform complains because I did not specify the value for the new property in the input.

So, there are three solutions:

  1. Update all the existing code to add the new property - out of the question.
  2. Tag the new version of the module differently and let the new code reference the new tag, while the old code continues to reference the old tag - in the long run would lead to proliferation of tags, creating all kinds of bizarre Cartesian multiplications of features in the tag names. Ultimately - out of the question.
  3. Relax the input variable schema by commenting out the optional properties and use try in the code.

The last option is not ideal, because the documentation for the module would not list the optional properties. But from the code management perspective - it is the best.

So the question is - can input object properties be defined as optional? Ideally, it should include the default value, but I am OK with the try approach for now.

EDIT 1

I actually thought I could pass unknown properties in the object, but no. Once the schema is given it is nothing less nothing more. So, the only backwards compatible solution is to use map(any) in my case.

2 Answers

Optional arguments in object variable have been suggested for Terraform:
https://github.com/hashicorp/terraform/issues/19898

Unfortunately as of May 30 2020, there has not been any progress on this.
That is the most upvoted issue on their repo, all we can do is keep upvoting and hopefully, that will be implemented soon.

And you are right the alternatives are just out of the question or plain hackish

Given your options, your preferences, and the fact that Terraform 0.12 doesn't support and Terraform 0.13 likely won't support optional or default values on objects, I think you have a fourth option:

variable "apsvc_map" {
  description = "The App Services sharing the same App Service Plan. Maps an App Service name to its properties."
  default = {}
  type        = map(object({ 
    identity_ids = list(string),
    disabled = bool
  }))
}

variable "no_custom_hostname_binding" {
  description = "Whether or not an App Service should disable hostname binding. Maps an App Service name to an override of the no_custom_hostname_binding property."
  type        = map(bool)
}

From there, you can use it like this:

lookup(var.no_custom_hostname_binding[local.awsvpc_map_key], null)

And declare overrides like this:

no_custom_hostname_binding = {
  "vpc_key" = true
}

in expressions where you need to know that parameter. This is not super-elegant, but without optional parameters, you don't have many good alternatives.

You can follow this pattern to add as many optional overrides as you need and add more later also without breaking clients.

Related