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.65k stars 9.55k forks source link

Allow dynamically-recursive child module references with count and for_each #27248

Open willscripted opened 3 years ago

willscripted commented 3 years ago

Hi there tf team,

tl;dr, recursive modules generate an error on terraform init. ~Not sure if bug or feature request.~ Feature request.

module "xyz" {
  source = "./"
}

Terraform Version

Terraform v0.14.2

Terraform Configuration Files

Please see demo. Since it is an issue with modules, there are multiple files to show. The module definition at dir/main.tf#L4-10 is the section generating the error.

module "dir" {
  source = "./"
  for_each = { for file in local.root_filesystem : file.name => file if file.type == "dir" }

  name = each.key
  children = each.value.children
}

Debug Output

Crash Output

No crash.log found.

Expected Behavior

Successful terraform init.

Actual Behavior

Init failed, appears to be when stack depth exceeds some thresholds. (Props to whoever added that failsafe; it's much easier to track down when the process terminates.)

Steps to Reproduce

git clone git@github.com:willscripted/terraform-recursion-error.git
cd terraform-recursion-error
terraform init

Additional Context

Nothing special. Out-of-the-box hashicorp/terraform container.

References

VladRassokhin touches on it right at the end of #15276. His #22648 issue is closely related to this. The rest of an is:issue recursion search does not look relevant.

apparentlymart commented 3 years ago

Hi @willscripted! Thanks for opening this issue.

Self-referential modules are intentionally not supported, so framing this as a bug would mean we'd likely focus on making it fail in a more helpful way with a specific error message, rather than merely hitting a recursion limit.

With that said, I expect your intent here was to request the ability to have arbitrarily recursive modules where an empty count or for_each represents the termination condition. That is not something we have intended to support so far because the module tree is always static today: all modules are always present, and the only thing that varies is how many module instances they have associated.

I'm going to relabel this as an enhancement to represent the request for a change in behavior, but I'll also warn that there are some significant technical barriers to implementing this, because it would require a big change in Terraform's architecture so that either terraform init is able to evaluate for_each expressions (it currently evaluates no expressions at all) or Terraform uses a very different model for module installation. That means that there probably won't be any movement on it in the near future, because our focus is currently elsewhere, and that the next step for it would be to do some research and produce a technical proposal for how Terraform's architecture would change to accommodate this new requirement.

In the meantime though, I can see that there's a use-case implied by your report here: describing a filesystem tree of arbitrary depth, presumably with the goal of creating such a directory structure in a remote system. It'd help if you could add a follow-up comment with some more details on what you are doing there, both because that helps inform the design process and because we might be able to offer some other approaches you could use with Terraform today.

Likewise, if anyone else finds this enhancement request and has another use-case to share, please add a comment describing it and showing what you've tried so far and what didn't work. For a design change of this size we typically prefer to have several different use-cases to consider so we can consider multiple possible designs and see how well each one fits the various use-cases.

Thanks again!

willscripted commented 3 years ago

Really appreciate the quick (and thorough!) response, @apparentlymart

Self-referential modules are intentionally not supported, so framing this as a bug would mean we'd likely focus on making it fail in a more helpful way with a specific error message, rather than merely hitting a recursion limit.

For what it's worth, I think it's failing well. I'm just happy that it terminates at all -- and with an error to boot.

With that said, I expect your intent here was to request the ability to have arbitrarily recursive modules where an empty count or for_each represents the termination condition.

Yes, this exactly.

I was hoping for a magic flag or missing base condition; no such luck. I do hope it's possible someday! Any solution in terraform-proper feels cleaner than layering some tech over top.

A file system is the first analog I could think of that wouldn't use terraform resources. My actual use-case is a tree of aws_api_gateway_resource resources. If I want to route a GET request with path /x/y/z, I need to create an aws_api_gateway_resource for each of x, y, and z -- with each element as the child of the former. Then in that final z leaf, connect an aws_api_gateway_method resource of type GET.

I think my options now are a) write out terraform configuration to a fixed depth, or b) use a script to generate terraform configuration files out to arbitrary depth.

Anyways, thanks for the response and consideration!

apparentlymart commented 3 years ago

Thanks for the extra use-case detail!

For API gateway in particular, I've seen folks have success using Terraform to generate an OpenAPI schema to pass all together into the REST API resource, and thus avoid dealing with all of the other resource types. I'm not sure if that will help in your case, but mention it only in case it helps you get something going with today's Terraform.

willscripted commented 3 years ago

Oh, definitely. Thanks, i'll check it out!

gtmtech commented 3 years ago

@apparentlymart Just ran into this issue today.

Recursion would make my code so much nicer. I have to map out organisational trees in google folder structure and amazon organizations organizational_unit / organizational_account structure for companies with different designs on how they do it.

I want to describe the org in a simple yaml file e.g.

---
root:
  development:
    dev:
      team1:
        project1_1: name1
      team2:
        project2_1: name2
#---

etc. It would be so helpful to be able to instantiate primary folder nodes with the subfolder structure as an argument and let it recurse, rather than to write a set of iteration functions for each level at the terraform root and then instantiate each one and having to interpolate parents and childs along the way. The code for this looks so ugly in comparison.

So yeah, cloud account/project/subscription governance with arbitrary layouts is my usecase πŸ‘

gtmtech commented 3 years ago

@apparentlymart One workaround I tried (which unfortunately didnt work) was to try this:

modules/
    folder
    folder2@ --> folder (symlink)
    folder3@ --> folder (symlink)
    folder4@ --> folder (symlink)

Then in the folder module:

variable "level" {
  type = string
  default = 1
}

Then in folder/subfolder.tf

module "subfolders" {
    for_each = toset(keys(var.subfolder_structure))
    source = "../folder${var.level + 1}"
}
β”‚ Error: Variables not allowed
β”‚ 
β”‚ On ../../modules/folder/subfolders.tf line 3: Variables may not be used
β”‚ here.

Shame, as this would have worked around the issue. Trying to think if there any other tricks on how it could be done without lots of copy+pasting of code

ganniterix commented 3 years ago

Recursion would make my code so much nicer. I have to map out organisational trees in google folder structure and amazon organizations organizational_unit / organizational_account structure for companies with different designs on how they do it.

I want to describe the org in a simple yaml file e.g.

I am trying to create OU's as well using some form of recursion, basically any kind of tree structure would need some kind of recursion. This would be extremely helpful.

hartzell commented 2 years ago

I’d like to assemble a set of GitLab groups/subgroups using the GitLab provider’s gitlab_group resource.

Subgroups are linked to their parent via the parent’s id, parent_id.

It’s straightforward to bang these out with an explicit set of resources, simply referring to the parent group and grabbing its ID in the subgroup.

I’m using a module to create the groups with appropriate defaults, it’s a riff and extension of ideas from Lotte-Sara Laan’s Module Parameter Defaults with the Terraform Object Type post. I can just add a separate invocation of my module for each resource, but I end up needing to call terraform init every time I add a new one, which seems unintuitive.

I’d rather just define the set of groups in a map variable and invoke the module once using a for_each loop.

The module is invoked something like this:

module "groups" {
  source         = "./modules/defaulted_gitlab_group"
  for_each       = local.our_groups
  name           = each.value["name"]
  path           = each.value["path"]
  description    = each.value["description"]
  parent_id      = each.value["parent_id"]
  group_settings = each.value["group_settings"]
}

Ideally our_groups would be something like this:

locals {
  # various things elided...
  our_groups = {
    "foo" : {
      name           = "Foo"
      path           = "foo"
      description    = "Foo"
      group_settings = local.group_defaults
      members = {
        "george-hartzell" = "owner",
      }
    }
    "foo-bar" : {
      name           = "Foo Bar"
      path           = "bar"
      description    = "Foo Bar"
      group_settings = local.group_defaults
      parent_id      = module.groups["foo"].gitlab_group.id
    }
  }
}

BUT, I end up with an error message like this (edited from a real example to match the sanitized input above):

β•·
β”‚ Error: Unsupported attribute
β”‚
β”‚   on foo_groups.tf line 35, in locals:
β”‚   35:       parent_id      = module.groups["foo"].gitlab_group.id
β”‚     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     β”‚ module.groups["foo"] is a object, known only after apply
β”‚
β”‚ This object does not have an attribute named "gitlab_group".
β•΅

I’ve also tried a second approach, where the local map contains the name of the parent group and I look it up in the module call/definition:

module "groups" {
  source         = "./modules/defaulted_gitlab_group"
  for_each       = local.our_groups
  name           = each.value["name"]
  path           = each.value["path"]
  description    = each.value["description"]
  group_settings = each.value["group_settings"]
  parent_id      = module.groups[each.value["parent_group_name"]].group.id
}

Somewhat predictably, this leads to a cycle:

β•·
β”‚ Error: Cycle: module.groups (close), module.groups.var.parent_id (expand), module.groups.gitlab_group.group, module.groups.output.group (expand)
β”‚

The only other resource I found that supported a hierarchy is a Google folder but I was unable to find any examples of people generating them from a map like I’m trying to do.

I’d love suggestions for getting out of the corner I’ve painted myself into.

AJCandfield commented 2 years ago

@hartzell Same thing here. I would like to be able to manage Gitlab resources entirely with Terraform/Terragrunt but it's proving to be fairly difficult if I want to keep it DRY.

wschult23 commented 2 years ago

I've encountered a similar problem. If you have nested maps, than you can't lookup the child attributes if you don't know how deep nested the attribute is. E.g. if you have a aws_instance object, then you can do the following:

lookup(aws_instance.my_instance,var.key)

if key is a top level attribute like "id", then it will return a proper value. But if the key is something like "tags.name" or "root_block_device.0.volume_size", it won't work. My first intention was, to use the split function and redo the lookup for each resulting value. But due to the lack of recursion capabilities, this won't work.

kyparisisg commented 2 years ago

@willscripted you can try and bounce between two or more modules which can give you the ability to emulate recursion assuming you call the modules within another module.

Just a wild thought...

KyleKotowick commented 2 years ago

@kyparisisg I've tried alternating between two modules, it has the same issue though (errors out at a certain depth).

apparentlymart commented 2 years ago

So far it seems like there are three ways people are trying to handle this sort of recursion:

The last of these is the only one that actually works today, and is what I would suggest for anyone who currently has use-cases of this sort. However, I think all three are worth exploring as possible ways to achieve a "built-in" answer; all three have some non-trivial design questions about how they would fit in to Terraform as it currently exists, without changing the meaning of any Terraform modules that were already written.

kyparisisg commented 2 years ago

@KyleKotowick I've solved this problem using CDKTF, and another way would be an engine which based on a payload it generates your terraform HCL (call to your modules with depends on). From working on both of the solutions, I'd definitely recommend the CDK for Terraform approach.