hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
42.42k stars 9.5k forks source link

Ability to set "type" on module output #30579

Open brandon-fryslie opened 2 years ago

brandon-fryslie commented 2 years ago

Current Terraform Version

Terraform v0.15.5
on darwin_amd64

Use-cases

It would be useful to define the expected type of an output (the same way we can for variable). That would allow Terraform to throw an error if the output value does not conform to the specified type.

For example, I have a child module (config_module) that provides some information about the environment that the current root module is being run in. I want to add an output called is_prod. This value should always be a boolean. However right now there is no way to enforce this within config_module itself, and we will only see an error when a consuming module attempts to use the value inappropriately.

Attempted Solutions

Currently you cannot set a type on an output. I'm sure I could hack together some sort of type checking module but that would defeat the purpose.

Proposal

I don't believe this would interfere with or impact any other existing Terraform features, beyond enabling more strict type checking at module boundaries. Output type checking could take place whenever is most convenient for the implementation, such as when running an apply or for a plan when all output values are known.

References

crw commented 2 years ago

Thanks for the enhancement request!

apparentlymart commented 2 years ago

Hi @brandon-fryslie! Thanks for sharing this.

It's an interesting coincidence that just yesterday I was thinking about this very possibility. Today we typically advise using the type conversion functions like toset to influence the return type of an output value, but although that broadly works it does have a number of drawbacks:

The reason I was thinking about that this week is that we're currently considering some extensions to the type constraints model and the custom validation features, part of which is intended to advance the existing optional attributes experiment. Some of our design sketches so far call for introducing a new argument alongside variable type so that we can add some new features that wouldn't fit cleanly into the existing constraint model, and so I think we ought to wait until that design effort is a little more baked before doing anything specific here -- if we do decide to introduce a new argument that supersedes type then I imagine we'd want to use that new design for output too, so that the two will remain symmetrical.

That design work is still early so I can't promise anything specific here yet, but I'm grateful for you raising this because it helps me to see that my thoughts about the limitations of the current approach are valid, since you've encountered at least one of these problems yourself.

brandon-fryslie commented 2 years ago

Thank you! I'm glad to know this fits in with Terraform's design ethos and goals. I'm a fan of strong typing and catching bugs statically (and the focus on that is one of the reasons I love Terraform over other tools).

serve as explicit documentation for users of the module as to what type they should expect to receive

no separate statement about the author's intention in order to validate the value against, which makes it easy to accidentally change the return type of an output value and not know about it until it breaks a consuming module

These are the two main factors that caused me to open the ticket. If someone else modifies a module to change the type field on an output, that will service as a strong signal they need to also consider that they are breaking downstream consumers. I also think the symmetry with variable will be nice.

I'm really excited for the work on optional and can't wait until that makes it out of experimental. I'm in no rush for this feature because there are workarounds, but it would generally be appreciated by users. Thanks again, have a good day.

korinne commented 2 years ago

Hi all, we recently released the Terraform v1.3 alpha, which includes the ability to mark object type attributes as optional and set default values.

I'm following up with issues that have provided feedback on the previous experiment, and wanted to invite you all to try the alpha and provide any feedback on this updated design. You can learn more/add comments here. Thank you so much in advance, and hope you like the feature!

makarov-roman commented 3 months ago

although optional is nice, it's not enough. There are still use cases where we want to control types. e.g. infamous conditionals.

│ Error: Error in function call
│ 
│   on .terraform/modules/namespace_centrals/loadbalancer.tf line 38, in module "network_loadbalancer":
│   38:   local_alb_arn                   = coalesce(var.alb_use_existing.lb_arn, module.application_loadbalancer[0].arn)
│     ├────────────────
│     │ while calling coalesce(vals...)
│     │ module.application_loadbalancer[0].arn is null
│     │ var.alb_use_existing.lb_arn is "arn:aws:elasticloadbalancing:eu-west-************************"
│ 
│ Call to function "coalesce" failed: all arguments must have the same type.

where module.application_loadbalancer[0].arn supposed to be optional(string), but terraform sees it as either string or null and union is not possible (at least I don't understand how).

code for the output:

output "arn" {
  value = length(aws_lb.network) != 0 ? aws_lb.network.*.arn : null
}

it was just aws_lb.network.*.arn before, but then tf saw either empty tuple or string, which is even more confusing

apparentlymart commented 3 months ago

Hi @makarov-roman,

In that example your output value would be inferred as having a tuple type because the one of the results is from a splat expression. Therefore this error seems correct because you are coalescing a string and a tuple.

If your intention was for the output value to be just a single string -- presumably then, your resource has a count that is either zero or one -- then I think this would be a good situation to use the one function:

output "arn" {
  value = one(aws_lb.network.*.arn)
}

This would then cause the output value to be of type string.

Of course, if Terraform had allowed you to set type = string on the output value then you could have an a more specific error message here, saying that the output value's result was a tuple instead of a string, so I think specifying the type of an output value is still valuable; I just shared the above in the hope of giving you something that works in the meantime.


We have designed the language for Terraform Stacks to use explicit types on output values as a way to trial this in a less risky context than the current Terraform language. If that design proves successful (Stacks is currently in a private preview phase) then I expect to bring a similar design and implementation to the traditional Terraform language too.