0

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?

Jeremy Schultz
  • 579
  • 1
  • 6
  • 26

3 Answers3

3

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.

Martin Atkins
  • 62,420
  • 8
  • 120
  • 138
  • Thanks for the thorough answer. I can tell there's no ideal way to avoid the problem, though `try()` and `one()` are easy and effective. I'm new to functions in Terraform so I'm wondering if the function calls will have an impact on performance. – Jeremy Schultz Apr 11 '23 at 14:17
  • I have implemented `one()` and it does resolve the problem. – Jeremy Schultz Apr 11 '23 at 18:15
  • 1
    Terraform's performance is typically constrained mostly by the time taken to make API calls and so the time taken to execute functions, while nonzero, tends to be negligible in comparison to network requests and other slow I/O that will happen while Terraform is doing its work. – Martin Atkins Apr 11 '23 at 18:53
1

Unfortunately there is no better way to define this as of now. You can see references to value by index everywhere in most of the popular modules (https://github.com/terraform-aws-modules/terraform-aws-vpc/blob/master/main.tf for example).

Even I've hoped that there was a simpler way to deal with this when we want resources to be created conditionally(feature flagged).

Technowise
  • 1,182
  • 6
  • 11
  • 1
    I looked at that main.tf and see there's a `try()` function designed to fall back to a value that doesn't produce an error. (I see Martin mentioned it below.) That is another possible solution, though I don't know if it's a great one with all the function calls. – Jeremy Schultz Apr 11 '23 at 13:51
0

Your single use of count will cascade to everything else. So you will also have to check the var.disable_resource == true condition in other places in your code. This includes locals, which you can write as follows:

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

This will successfully skip the resource_name_sid creation. But obviously, now you will have to keep using the condition in ever other place where resource_name_sid would be used.

Marcin
  • 215,873
  • 14
  • 235
  • 294