Terraform: Cannot use a variable when a map is required

Created on 21 Feb 2018  路  5Comments  路  Source: hashicorp/terraform

Terraform Version

Terraform v0.11.4-dev

Terraform Configuration Files

resource "aws_instance" "instance" {
  connection = "${local.connection}"
}

Expected Behavior


Terraform is happy with this

Actual Behavior


root: not an object type for map (*ast.LiteralType)

Steps to Reproduce


terraform apply

Using variables should be straightforward.
connection apparently uses some magic, and config blocks like provisioner cannot be hold by a variable. Lists sometimes become strings and the variable system is a mess.

config enhancement

Most helpful comment

Hi @vmiszczak-teads,

Many existing issues with the interpretation of values in configuration are going to be addressed by some current work to revamp the configuration language parser, with details being discussed in many other GitHub issues.

However, as you noted certain constructs, such as all of the top-level blocks, and special blocks like connection and provisioner cannot be _entirely_ replaced by a variable. This is because they need to be resolved very early in Terraform's work in order to validate configuration and build the graph.

We have no plan to make these "structural" elements totally dynamic as you describe here, for fundamental architectural reasons. While I can definitely see the attraction of making the whole configuration fully dynamic, it is in practice a complex problem that would make Terraform's behavior far less predictable, preventing full validation of configuration, etc. As an analogy, consider the additional guarantees a statically-typed programming language is able to make compared to a dynamically-typed one; similar guarantees are considered a requirement for Terraform due to its scope to cause catastrophic failure when given invalid input. Terraform configuration blocks are more like a struct or class than they are like a map.

It is possible that we could address your specific use-case here with a more specialized feature to enable re-using connection configurations without duplicating them. Much like we've done with providers in 0.11, we can't make them participate as fully-general dynamic values in the configuration language but we _could_ introduce features addressing specific use-cases that tend to cause maintainability headaches today. For example:

# NOT YET IMPLEMENTED
resource "aws_instance" "example" {
  # ...

  connection {
    host       = "${self.private_ip}"
    # ...
  }
}

resource "null_resource" "example" {
  connection {
    # Populate any unset connection arguments using the fully-resolved
    # results from the connection block on the aws_instance above, avoiding
    # the duplication while still allowing for static validation.
    # Note that this is a static reference to the other resource, rather than
    # an expression with a value.
    inherit = aws_instance.example
  }
}

All 5 comments

Hi @vmiszczak-teads,

Many existing issues with the interpretation of values in configuration are going to be addressed by some current work to revamp the configuration language parser, with details being discussed in many other GitHub issues.

However, as you noted certain constructs, such as all of the top-level blocks, and special blocks like connection and provisioner cannot be _entirely_ replaced by a variable. This is because they need to be resolved very early in Terraform's work in order to validate configuration and build the graph.

We have no plan to make these "structural" elements totally dynamic as you describe here, for fundamental architectural reasons. While I can definitely see the attraction of making the whole configuration fully dynamic, it is in practice a complex problem that would make Terraform's behavior far less predictable, preventing full validation of configuration, etc. As an analogy, consider the additional guarantees a statically-typed programming language is able to make compared to a dynamically-typed one; similar guarantees are considered a requirement for Terraform due to its scope to cause catastrophic failure when given invalid input. Terraform configuration blocks are more like a struct or class than they are like a map.

It is possible that we could address your specific use-case here with a more specialized feature to enable re-using connection configurations without duplicating them. Much like we've done with providers in 0.11, we can't make them participate as fully-general dynamic values in the configuration language but we _could_ introduce features addressing specific use-cases that tend to cause maintainability headaches today. For example:

# NOT YET IMPLEMENTED
resource "aws_instance" "example" {
  # ...

  connection {
    host       = "${self.private_ip}"
    # ...
  }
}

resource "null_resource" "example" {
  connection {
    # Populate any unset connection arguments using the fully-resolved
    # results from the connection block on the aws_instance above, avoiding
    # the duplication while still allowing for static validation.
    # Note that this is a static reference to the other resource, rather than
    # an expression with a value.
    inherit = aws_instance.example
  }
}

If you are trying to reduce bloat of tf.config file, you can remove the new lines, and copy and paste a single config across multiple resources.

This can lead to mistakes if one forgets to copy and paste connection configs across multiple resources.

I'm working on very redundant configurations with terraform, I would certainly appreciate to have a way to avoid doing copy/paste for my connections and provisioners on each of my ressources. I think that these feature is related to other issues #1478 #8616 #232

On my part, I am dealing with different environments where some have bastion hosts and other don't and I would like to manage both using the same terraform code. We initially attempted something like listed in #8616 and then, instinctively tried what's described in here both to no avail.

We are not aiming to have something very dynamic but just to be able to optionally use a bastion host if one is required for the environment being _terraformed_. Our solution was to set a connection map that would add the bastion parameters if the bastion hosts was set.

Are there any good solutions for this?

Currently passing a large map of providers (15 regions) - to a module, would be nice to use a local map rather than actually paste this under each module invocation:

  providers = {
    "aws.ap-northeast-1" = "aws.ap-northeast-1"
    "aws.ap-northeast-2" = "aws.ap-northeast-2"
    "aws.ap-southeast-1" = "aws.ap-southeast-1"
    "aws.ap-southeast-2" = "aws.ap-southeast-2"
    "aws.ap-south-1"     = "aws.ap-south-1"
    "aws.ca-central-1"   = "aws.ca-central-1"
    "aws.eu-central-1"   = "aws.eu-central-1"
    "aws.eu-west-1"      = "aws.eu-west-1"
    "aws.eu-west-2"      = "aws.eu-west-2"
    "aws.eu-west-3"      = "aws.eu-west-3"
    "aws.sa-east-1"      = "aws.sa-east-1"
    "aws.us-east-1"      = "aws.us-east-1"
    "aws.us-east-2"      = "aws.us-east-2"
    "aws.us-west-1"      = "aws.us-west-1"
    "aws.us-west-2"      = "aws.us-west-2"
  }
Was this page helpful?
0 / 5 - 0 ratings