-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Terraform design discussion: inline blocks, empty vs omitted parameters, and modules #14037
Comments
+1 on parameter omitting, and ill raise with another example: In the name of trying to make reusable components, what if we wanted to provide all of a resources parameters via an object, thereby allowing us to just loop over a list of objects:
We could then reuse the resource:
For this simple example, we dont have any major issues with omitting values, but if any of the resource's parameters expects an object with required values, then we cannot simply pass in an empty map and have it omit that value but still be able to specify it for other instances in a loop. What would be ideal is if we had something similar to Ansible's |
Thanks for this thoughtful discussion and analysis, @brikis98! We are certainly aware of these and other shortcomings in the configuration language right now. It is a long-term goal to add language features that make reusable modules more practical to create, and you've touched on two of the big things here. We've been concerned that iteratively adding features to the language over time would cause it to grow into a bit of a "Frankenstein's Monster", so we've instead been quietly collecting use-cases for a while now with the intent of informing a more holistic revamp of the language at some point in the future, where we can hopefully find the minimal set of features to serve the most important use-cases without excessively complicating the configuration language. The use-cases and potential solutions you've shared here are a great addition to that collection! I can't promise immediate action on this front but I appreciate you taking the time to document this and look forward to discussing a more concrete language change proposal with the community in the future. |
@apparentlymart Got it. Glad to hear this is on your radar. Keep us posted :) |
I'm currently stumped by this deficiency. I want to create a re-usable aws elastic beanstalk environments, with the list of settings defined by variable(s) passed into the module, eg:
where the number of inline settings blocks will be different for each environment. I'd really like to see a solution some time soon. |
@robinbowes although its not clearly documented as an example, settings that take a map (especially those that can be specified more than once for any given resource) are usually just implemented as lists internally. I havent tried it, but this may be possible with this resource:
If that does work you may just be able to pass in the list via tfvars? |
@ju2wheels Thanks - I'll give it a whirl tomorrow and report back. Presumably, if it works, I can add a list of "default" settings and join the user-supplied list to create the value passed to "settings" = [...] ? |
Im pretty sure I tried this and am confident it wont work. The way TF handles creating objects dynamically and trying to merge it will most likely not work out unfortunately. |
Related question: is there any way to define variables without making them settable outside the module? eg. (pseudo code):
|
Theres an open issue about local variables coming in a future version. |
@apparentlymart First off (and as somewhat of a disclaimer), thank you for making such an awesome, elegant piece of software. I'm fully aware that I've chosen to use a pre-1.0 version of an open source tool that solves a very intricate problem, and I don't want to sound entitled / unappreciative of the amount of energy you've invested nor dismissive of just how great the current iteration is. Your concerns are warranted and pragmatic, and I know you have your own upstream constraints to deal with. I'll do my best to keep my criticism constructive, and you can take what I'm about to say with a grain of salt. That said, I think the issues the OP is raising are very large deficiencies on the UX front. They both currently have me banging my head against a wall, and I've tried things like direct map interpolation, complex attribute injection via It seems worth noting that the lack of said features has each of us building our own private monsters by wrapping and templating terraform to achieve things that can't be accomplished natively today, which IMHO is an anti-pattern. Doing so obfuscates the declarative intent, adds unnecessary complexity, makes terraform upgrades scarier (or impossible, depending on how you've designed your Frankenstein) and may preclude the ability to safely use these features when they do land without a complete rewrite. I'm trying to unwind such a system today, which heavily templates a single, large AWS deployment, so that we can build analogous configurations for other cloud providers. "Frankenstein monster" is an apt description. Hopefully that was semi-constructive in and of itself, since you're keeping a running tally of pain points, but I'll stop belaboring points you've undoubtedly thought longer and harder about than I. More constructively, what can be done today? Would it make sense to transparently document the existing limitations, existing workarounds (however hacky, if they exist) and (where design has solidified and timelines aren't nebulous) what the roadmap looks like? I'm still not entirely sure what is / isn't possible, despite my now-intimate familiarity with docs. Perhaps we could document what patterns / practices make sense to you as designers in these scenarios, so that we don't deviate too far from the path forward? Are there stop-gap solutions? Where no workarounds exist, is any of this (temporarily or not) pluggable without totally corrupting the product? What can do to help (besides generating lengthy diatribes in github issues)? |
Hi all! I'm sorry I lost track of this issue when posting updates elsewhere. Configuration language concerns are now covered in a few different issues so I've been posting updates in e.g. #7034, etc but neglected to update here. Work is in progress right now on integrating the so-called "HCL2" (will later just be "HCL", once things stablize) that is the main outcome of the design process I alluded to above. Along with changing the configuration language itself, we also need to change many details about Terraform's internal model to support some of these new ideas, such as null values (differentiated from empty values). Because this is a big and invasive series of changes, with compatibility implications along the way, we're approaching this cautiously so we can gather feedback and then ensure there's a migration path through some of the small differences that the new language entails. Some of this work is already in As others have noticed, some coincidences in the current implementation (exploiting some quirks of how list and map support was retrofitted into a Terraform Core that originally dealt with strings) mean that you can, with some experimentation, trick Terraform into doing some dynamic behavior. These are not documented because they are pretty temperamental (things have to line up just right for it to work) and, indeed, will almost definitely be broken by the forthcoming changes that will make complex data types first class in Terraform core, since the current coincidences will no longer hold with the new, more-robust type system. While the new language version is intended to be broadly compatible with the current, we've focused on remaining compatible with idiomatic usage as shown in documentation, rather than with all of the implementation details. Focusing on the two concerns raised in this issue: The first round of changes does not yet include support for dynamically-generating nested blocks but there is a plan for that which the new language has the building blocks to support and it will follow in a subsequent update. You can see in the discussion of #7034 the sketches of what this might look like, though the details may evolve as we learn more during the initial implementation of HCL2. "HCL2" also has native support for null values, which are intended to be equivalent to omitting an attribute when used in that context. In the long run this should allow an approach similar to what @brikis98 suggested in the opening comment on this issue, though again we can't get there all in one step because Terraform's internal model of variables does not have any way to distinguish null from empty, and so we need to propagate this idea through all of Terraform's features before it will be generally useful. I want to make it clear that these various concerns are not being ignored. Before I joined the Terraform team I wrote my fair share of moderately-complex Terraform configurations in my previous role and so I totally understand the frustrations you are all feeling. The reason this isn't out yet is because it's a complex set of work with lots of inter-dependencies and risk, and so it would be irresponsible to try to do it all in one step, particularly with other Terraform Core development going on concurrently. The opt-in preview version should be released soon. As noted above, this first pass won't actually address the two specific issues identified here because they are "deeper" problems that we'll need to address in a later change, but it will introduce some expression-level features (as opposed to "structural" features, which these two are) that will make certain kinds of dynamic configuration easier to express, and give us the building blocks we need to permeate these concepts throughout Terraform Core. I'll try to remember to post updates here as work continues. Let's try to keep this particular issue focused on the two concerns @brikis98 originally raised, since there's a number of other issues for other configuration language changes and so it's better to keep these discussions focused so that people can subscribe to the specific ones they are interested in. |
@apparentlymart Semi-off-topic / no response necessary, but thank you so much for the transparent, informative response. I hope my comment was, in fact, constructive. As I dig through various issues and understand Terraform better, it's becoming increasingly obvious that the team is carefully weighing tradeoffs and taking calculated steps in the right direction (while context-switching to listen to and manage the expectations of nagging users, such as myself). There are a handful of current limitations (these two top that list) that I don't see as being solvable in userspace today without templating Terraform, but I'll start working at reusable wrappers for native workarounds, where possible, and keep an eye on HCL2. |
Add me to the list of people creating frankenstein workarounds throughout their terraform config in order to workaround this issue. In my case, I'm using the OP's library of modules, so at least my frankenstein workaround tends to be identical to his, and fixes to one will likely fix the other. But I've been building out an AWS infrastructure for a month or more now, via terraform/terragrunt and it is kind of shocking just how much of that rather lengthy development time has been devoted to working around this issue in various places - either unwinding bugs in an existing frankenstein solution, creating pull requests for the OP to integrate my fixes, or discovering new places where a frankenstein solution is needed. This was not, at all, aided by the addition of a backwards-incompatible change in Terraform 0.11 which breaks the most common frankenstein solution for this issue - conditionally defining multiple resources but setting count = 0 on all resources that shouldn't be generated because they have whole blocks that shouldn't be defined. If I had to estimate, I would guess that the inability to conditionally include a config block has been maybe 30% of my total development time, and 20% more has been spent working around the inability to do useful things with compound data structures in HCL (list of maps, etc). These issues tend to result in really verbose workarounds, too, which simply adds to the quantity of code/config that must be written/tested/maintained. In many cases, every single resource in a template/module must be defined twice. And god forbid you want to combine 2 or more features that each requires its own config block, you suddenly have to define 2^n resources for each resource you want to actually create, and ensure you get all of them correctly defined without unintended cut and paste errors. The test/debug cycle suddenly gets extraordinarily time consuming, even once you've spent a couple of hours cutting and pasting things into a frankenstein workaround. So far, I have ended up forking modules and editing them for my own uses when needing to deal with 2 different conditional blocks in the same resource, rather than trying to create 2^2 or 2^3 resources everywhere in the module. The complexity is just too much, so I'd rather have to worry about manually pulling updates into my fork than to try to define a generally useful module that could be re-integrated to the repo it was forked from. I may regret that decision when it comes time to pull in all of the changes required to accommodate accessing resource attributes when count is explicit but equal to 1. I wasn't anticipating the imminent arrival of a significant rewrite of every module I use in order to accommodate that change when I first decided to permanently fork a few modules that seemed mostly mature and stable. |
I'm not sure how I missed @ju2wheels's suggestion, but I just spotted it somewhat at random today, and it actually works great as a workaround for dynamic inline blocks! For example, the aws_elb only allows you to define listeners as inline blocks: resource "aws_elb" "bar" {
name = "foobar-terraform-elb"
availability_zones = ["${data.aws_availability_zones.available.names}"]
listener {
instance_port = 8000
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
listener {
instance_port = 8000
instance_protocol = "https"
lb_port = 443
lb_protocol = "https"
}
} But it turns out you can also define listeners using an input variable which contains a list of maps! resource "aws_elb" "bar" {
name = "foobar-terraform-elb"
availability_zones = ["${data.aws_availability_zones.available.names}"]
listener = "${var.listeners}"
}
variable "listeners" {
type = "list"
default = [
{
instance_port = 8000
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
},
{
instance_port = 8000
instance_protocol = "https"
lb_port = 443
lb_protocol = "https"
}
]
} @apparentlymart This seems like a reasonable workaround to me. Should it be documented somewhere? |
@brikis98 it was undocumented when I last used Terraform around 6 months ago and was something I used successfully quite frequently. Although, I did find a few resources where I think the internal representation or type checking was wrong for which I meant to open issues but never got around to. For example. I have some notes from old attempts:
I think these resource properties should have allowed a map variable to be used but instead wanted a list of a single map which made it confusing, but the concept still worked. The only time I had issues with this approach was trying to construct the input value on the fly by merging an inline static map with a variable map IIRC or something like that. I think I had a discussion with @apparentlymart about it on gitter so the history might be there depending on how far back it goes. |
Treating a nested block like an attribute and assigning a list of maps to it is in the class of things I was describing above as "coincidences in the current implementation": it happens that Terraform uses a similar representation in memory for lists of maps as it does for nested blocks, and so if you're very careful you can exploit some holes in Terraform's validation and get this to work. However, there are lots of quirks and inconsistencies here because this is not something that is intentionally supported. The issue @ju2wheels describes is a big one: Terraform expects to be able to do structural validation of nested blocks, but this fails when the internal representation doesn't align with our expectations for how nested blocks behave. In this case, the list of maps is This "trick" will be broken by the new language parser, because its internal representation of blocks and attributes is now physically distinct and the validator no longer has the hole that makes the workaround seem to work in some cases. This is why the pattern you saw there is intentionally not documented. You can see in #7034 the latest sketches of what this might look like when implemented "for real". It's built around a nested block because that ensures that the structure will behave in a "block-like" way, allowing Terraform to validate the contents statically, etc. I'm going to keep posting updates in that issue as things develop; we've done all the prototyping we can do until we get the new language parser properly integrated into Terraform Core, so we're going to keep our focus on that for the moment and then finalize the |
@apparentlymart Thanks for the update. Will the new |
I know that there are several users out there exploiting this implementation detail, and indeed some of them are in verified modules on the Terraform Registry, so we're planning to release We're at the tail end of some design/planning discussion so implementation should pick up steam again in the very near future. Once we have an initially-workable implementation we'll firm up the release plan. |
Is there a bounty program that I can donate for this feature? I currently have a love/hate relationship with terraform, and fixing this issue would make this a true love story. |
@jessecollier well.. you don't have to donate.. https://www.hashicorp.com/blog/terraform-0-1-2-preview |
Hi again! Sorry for the long silence here. Since this issue actually covers two different things, I'm first going to note that the first issue was the same as #7034 and that is now closed after verification of the change with v0.12.0-alpha1 🎉 . With that said, I'm going to focus this reply on the second request about conditionally-unset values. v0.12.0-alpha1 also includes support for the special value variable "storage_encrypted" {
description = "Specifies whether the DB instance is encrypted."
default = false
}
variable "kms_key_id" {
description = "The ARN for the KMS encryption key. Only used if storage_encrypted is true."
default = null
}
# It would be nice if this worked
resource "aws_db_instance" "default" {
storage_encrypted = var.storage_encrypted
kms_key_id = var.kms_key_id
} Another way to use # assuming here that ams_kms.key.main.id has:
# count = var.use_kms ? 1 : 0
kms_key_id = var.use_kms ? aws_kms_key.main[0].id : null From a provider's perspective, an omitted value and a With that said, I think we've now covered both of the ideas mentioned in this issue, and so I'm going to close it. Thanks for suggesting these, and thanks for the patience while we did the groundwork to make them possible. |
Hello @apparentlymart , can the fix for |
I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. |
I hope it's OK to file an issue to discuss Terraform's design. If this sort of discussion belongs somewhere else, let me know!
Terraform's support for modules is awesome. However, there are two design choices in Terraform itself that make it very hard to build reusable, configurable modules: inline blocks and empty vs omitted parameters. I discuss each below, what issues they cause for modules, and possible ways to fix them.
1. Inline blocks
Problem
Some resources are configured with a large number of inline blocks, such as aws_elb, which uses an inline block to define each
listener
:If you create such resources in a module, as far as I know, there is no way to specify a dynamic number of inline blocks based on input variables. For example, if I had a module that took in a
var.elb_ports
parameter that contained a list of port numbers, I'm not aware of any way to loop over the ports invar.elb_ports
and dynamically create a listener for each port.As a result, for resources that rely on inline blocks—and there are many, including
aws_elb
,aws_cloudfront_distribution
,aws_s3_bucket
,aws_ecs_service
,aws_ecs_task_definition
, and thetags
andebs_xxx
blocks on many other resources such asaws_instance
andaws_autoscaling_group
—there is no way to create a flexible, general-purpose module.Possible solutions
One option is to create "companion resources" for all inline blocks. For example, you can add security group rules to an aws_security_group using the aws_security_group_rule "companion resource." Since the latter is a separate resource, you can create them dynamically using the
count
parameter.Another option is to support the
count
parameter in inline blocks:2. Empty vs omitted parameters
Problem
Terraform is inconsistent with the difference between setting a parameter to an empty value vs omitting the parameter entirely. For example, with aws_instance, you're allowed to set the
key_name
parameter to an empty string to not associate an EC2 Key Pair with the EC2 Instance. This allows you to create a module that exposes akey_name
parameter as a variable with the default value set to an empty string:Now compare that to the aws_db_instance resource. If you omit the
kms_key_id
parameter, everything works fine:However, if you try to make the
kms_key_id
a configurable parameter in your module, and set its default to an empty string, you get the error "To enable encryption at rest StorageEncrypted must be set to 'true'":There are many other examples where a resource behaves differently for an empty parameter versus one that is omitted entirely. This makes it very hard to build reusable, configurable modules with those resources.
Possible solutions
One option is to try to update all resources to treat an empty string the same as omitting the parameter entirely. This may be tricky to do because, in many cases, it's not Terraform, but the underlying provider (e.g. AWS) that treats them differently.
Another option is to support the idea of a
null
orNone
value in Terraform. When a parameter is set to this value, it is exactly the same as omitting the parameter entirely.The text was updated successfully, but these errors were encountered: