Search code examples
terraformdevopshcl

Terraform: Handling locals that are conditional due to feature flags


I'm building a Terraform module that uses some variables for feature flags along with locals for storing some computed values. I'm bumping into some errors while a flag is true.

The flags (booleans saved as variables) are on every resource and use this convention which seems to be standard in Terraform:

resource "provider_resource_id" "resource_name" {
    ...
    count = var.disable_resource ? 0 : 1
    ...
}

The provider outputs IDs when making these resources and because count forces me to put an index on them, I'm saving them as locals in a locals.tf file to be less verbose:

locals {
    resource_name_sid = provider_resource_id.resource_name[0].sid
}

I'm now running terraform apply when disable_resource = true and get this error: Invalid index: provider_resource_id.resource_name[0].sid (provider_resource_id.resource_name is empty tuple). I see that defining the local when the resource isn't created is a problem. So I commented out the local. Now I get other errors on all resources expecting the local: Reference to undeclared local value: (resource_name_sid has not been declared) These resources wouldn't actually be built due to the flag, but they still expect the local (which I can't define because the resource isn't being built).

I bet I can put a ternary on every local to say, for example:

locals {
    resource_name_sid = var.disable_resource ? "" : provider_resource_id.resource_name[0].sid
}

But that is getting verbose again. Maybe I can't externalize these locals and use feature flags at the same time. (I did try moving locals into the resources file but got the same result.) Maybe I need to abandon the use of locals for storing these and just put them inline in the resources. Or is there something I am missing?


Solution

  • There is no way to avoid explaining to Terraform what should happen in the case where the object doesn't exist, but there are some shorter ways to express the idea of using a fallback value as a placeholder when there are zero instances of the resource.


    One concise option is to use one, which is a function intended to deal with the common situation of turning a list of zero or one elements into a value that might be null:

    locals {
      resource_name_sid = one(provider_resource_id.resource_name[*].sid)
    }
    

    provider_resource_id.resource_name[*].sid produces a list of length matching the count of provider_resource_id.resource_name. In your configuration the count can only be either zero or one, which matches the expectations of one.

    Therefore local.resource_name_sid will either be a single sid value or it will be null.


    Another possibility is to use try to let the element lookup [0] fail and provider a fallback value to use if it does:

    locals {
      resource_name_sid = try(provider_resource_id.resource_name[0].sid, null)
    }
    

    This option lets you choose a different fallback value to use instead of null if you like, although null is the typical way to represent the absense of a value in Terraform so I would suggest using that unless you have some other working SID value to use as a fallback.

    Using null has the advantage that you can then assign local.resource_name_sid directly to an argument of another resource and then in the case where its null it will be completely indistinguishable to the provider from having omitted that argument entirely, because null also represents the absence of an argument.


    A final option is to directly test the length of provider_resource_id.resource_name to see if there is a zeroth index:

    locals {
      resource_name_sid = (
        length(provider_resource_id.resource_name) > 0 ?
        provider_resource_id.resource_name[0].sid :
        null
    }
    

    This is similar to the conditional you included in your question but it directly tests whether there's a provider_resource_id.resource_name[0] rather than repeating the reference to var.disable_resource.

    Testing the resource directly means that if you change the count definition in future then you won't need to update this expression too, as long as your new count expression still chooses between either zero or one elements.

    However, this is the most verbose option and requires repeating the long expression provider_resource_id.resource_name in two places, so I'd typically use the try option above if I needed to have a non-null fallback value, and the one option if null is a sufficient fallback value.

    The one function also has the advantage over the others that it will fail if there is ever more than one instance of provider_resource_id.resource_name, and so if you update this module to have multiple instances of that resource in future then you'll be reminded by the error to update your other expressions to deal with two or more SID values. The other expressions will just silently ignore the other SIDs.