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

Feature Request: Pass complex blocks as variables #25668

Open AndresFPineros opened 4 years ago

AndresFPineros commented 4 years ago

Current Terraform Version

Version: 0.12.29

Use-cases

I want to be able to pass complex nested blocks as variables to avoid variable defintion overhead. Let me explain with the following code extracted from https://www.terraform.io/docs/providers/kubernetes/r/pod.html:

resource "kubernetes_pod" "with_node_affinity" {
  metadata {
    name = "with-node-affinity"
  }

  spec {
    affinity {
      node_affinity {
        required_during_scheduling_ignored_during_execution {
          node_selector_term {
            match_expressions {
              key      = "kubernetes.io/e2e-az-name"
              operator = "In"
              values   = ["e2e-az1", "e2e-az2"]
            }
          }
        }

        preferred_during_scheduling_ignored_during_execution {
          weight = 1

          preference {
            match_expressions {
              key      = "another-node-label-key"
              operator = "In"
              values   = ["another-node-label-value"]
            }
          }
        }
      }
    }

    container {
      name  = "with-node-affinity"
      image = "k8s.gcr.io/pause:2.0"
    }
  }
}

Here, we can see there are many nested blocks which are optional. In my use case, I have no idea which of these blocks will be used in a specific instance of the deployment. This means someone could use node_affinity or not, and inside that block they could use required_during_scheduling_ignored_during_execution or preferred_during_scheduling_ignored_during_execution or both, and inside those blocks other variations.

It would be a "dynamic" rabbit-hole to handle all the possible scenarios in which a person can configure these nested blocks. (Really, I can't imagine how to do it without over-complicating things with the dynamic block)

It would be nice if we could (not possible right now) pass complex blocks as variables, like:

variable "node_affinity" {
  default = {
    required_during_scheduling_ignored_during_execution {
      node_selector_term {
        match_expressions {
          key      = "kubernetes.io/e2e-az-name"
          operator = "In"
          values   = ["e2e-az1", "e2e-az2"]
        }
      }
    }
  }
}

And inside the deployment use them like:

resource "kubernetes_pod" "with_node_affinity" {
  metadata {
    name = "with-node-affinity"
  }
  spec {
    affinity {
      node_affinity = var.node_affinity
    }
    container {
      name  = "with-node-affinity"
      image = "k8s.gcr.io/pause:2.0"
    }
  }
}

Another solution -> allow setting the content map of a dynamic block:

dynamic "node_affinity" {
  for_each = var.node_affinity == null : [] : [{}]
  content   = var.node_affinity
}

This way someone would configure the "node_affinity" block with any configuration they'd like without having to overthink the variable definition and type checking. I'm not sure if this contradicts the Terraform way of doing things, but it would be a nice to have. Allowing users to treat blocks like maps (or list of maps if multiple blocks are allowed) would make Terraform more dynamic and friendly.

lllamnyp commented 2 years ago

Yes please! The lack of this feature forces module developers to replicate the entire API of a provider within their code or greatly constrain users to a subset of allowed parameters.

lucasalves-hotmart commented 2 years ago

Agreed. It would be great for code reuse, specially when dealing a large number o similar typed resources.

davidlbyrne commented 1 year ago

I can't believe you cannot pass a complex object as a var and just assign it as a single value to a block. Please tell me I'm the idiot and there is a better way to do this I just don't see it?

I'm implementing a module for a waf and there may be 1 rule or maybe 20 rules. The way the statement logic for the waf rules are constructed uses a potentially deeply nested objects/map. If I want the consumer of my module to be able to define their own rules and pass them as a map of objects map(any) then I will have to implement every possible key, and child, and child... as dynamic block. For reference : https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl.html#statement-block.

example:


variable "rules" {
  type  =  map(any)
  default = {
    block-non-usa-traffic = {
      name     = "block-non-usa-traffic"
      priority = 0
      action = {
        block = {
          custom_response = {
            response_code = 302
            response_header = {
              name  = "Location"
              value = "http://blocked.fartsmeller.com"
            }
          }
      } }
      statement = {
        not_statement = {
          statement = {
            geo_match_statement = {
              country_codes = [
                "US",
              ]
            }
          }
        }
      }
      visibility_config = {
        cloudwatch_metrics_enabled = true
        metric_name                = "block-non-usa-traffic"
        sampled_requests_enabled   = true
      }
    },  

The implementation is disgusting:

resource "aws_wafv2_web_acl" "common" {
  name  = local.name_prefix
  scope = "REGIONAL"

  default_action {
    allow {}
  }
  dynamic "rule" {
    for_each = var.rules
    content {
      name     = "${local.name_prefix}-${rule.value.name}"
      priority = rule.value.priority
     # statement = rule.value["statement"]  ## <--- this will not work :( so you have to do something like this: ... 
     dynamic "statement" { 
       for_each = rule.value.statement
       content { 
         dynamic "not_statement" {
           for_each = contains(keys(rule.value.statement), "not_statement' )) == true ? rule.value.statement.not_statement :[]
             dynamic "geo_match_statement" {
               for_each = ...
               ...
              }
             ... 
             ...
            }
         }
       }
      action {
        dynamic "block" { 
        foreach = contains(keys(rule.value.action), "block") == true ? [1] : []
          custom_response {
            response_code = action.block.response_code
            response_header {
              name  = action.block.response_header.name
              value = action.block.response_header.value
            }
          }
        }
      }

When on the other hand you could just do this:


resource "aws_wafv2_web_acl" "common" {
  name  = local.name_prefix
  scope = "REGIONAL"

  default_action {
    allow {}
  }
  dynamic "rule" {
    for_each = var.rules
    content {
      name     = "${local.name_prefix}-${rule.value.name}"
      priority = rule.value.priority
      statement = rule.value["statement"] 
   }
}

Please tell me I'm the idiot and there is a better way to do this I just don't see it?