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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variable interpolation doesn't work in tags of aws_subnet_ids #14516

Closed
dvishniakov opened this Issue May 15, 2017 · 9 comments

Comments

Projects
None yet
6 participants
@dvishniakov

dvishniakov commented May 15, 2017

Community Note

  • Please vote on this issue by adding a 馃憤 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request

Note: this block was copy-pasted from terraform-provider-aws

Terraform Version

0.9.5

Affected Resource(s)

  • aws_subnet_ids

Terraform Configuration Files

variable "myvar" {
  default = "myvalue"
}

variable "mymap" {
  type = "map"
  default = {
    "internal_subnet_tag.key" = "associate_public_ip_address"
    "internal_subnet_tag.value" = "no"
  }

}

data "aws_subnet_ids" "our_vpc" {
  vpc_id = "real-vpc-id"
  tags {
    "${var.myvar}" = "${var.mymap["internal_subnet_tag.value"]}"
    # "${var.mymap["internal_subnet_tag.value"]}" = "${var.mymap["internal_subnet_tag.value"]}"
    # "${var.mymap["internal_subnet_tag.key"]}" = "${var.mymap["internal_subnet_tag.value"]}"
    #  ----- Lines below work well
    # "associate_public_ip_address" = "${var.mymap["internal_subnet_tag.value"]}"
    # "associate_public_ip_address" = "no"
  }
}

Debug Output

--Cut of bad debug
data.aws_subnet_ids.our_vpc: Refreshing state...
2017/05/15 15:49:12 [DEBUG] plugin: terraform: aws-provider (internal) 2017/05/15 15:49:12 [DEBUG] Matching ^aws: with ${var.myvar}
2017/05/15 15:49:12 [DEBUG] plugin: terraform: aws-provider (internal) 2017/05/15 15:49:12 [DEBUG] DescribeSubnets {
2017/05/15 15:49:12 [DEBUG] plugin: terraform: Filters: [{
2017/05/15 15:49:12 [DEBUG] plugin: terraform: Name: "vpc-id",
2017/05/15 15:49:12 [DEBUG] plugin: terraform: Values: ["real-vpc-id"]
2017/05/15 15:49:12 [DEBUG] plugin: terraform: },{
2017/05/15 15:49:12 [DEBUG] plugin: terraform: Name: "tag:${var.myvar}",
2017/05/15 15:49:12 [DEBUG] plugin: terraform: Values: [""]
2017/05/15 15:49:12 [DEBUG] plugin: terraform: }]
2017/05/15 15:49:12 [DEBUG] plugin: terraform: }

--Cut of good debug
data.aws_subnet_ids.our_vpc: Refreshing state...
2017/05/15 15:48:26 [DEBUG] plugin: terraform: aws-provider (internal) 2017/05/15 15:48:26 [DEBUG] Matching ^aws: with associate_public_ip_address
2017/05/15 15:48:26 [DEBUG] plugin: terraform: aws-provider (internal) 2017/05/15 15:48:26 [DEBUG] DescribeSubnets {
2017/05/15 15:48:26 [DEBUG] plugin: terraform: Filters: [{
2017/05/15 15:48:26 [DEBUG] plugin: terraform: Name: "vpc-id",
2017/05/15 15:48:26 [DEBUG] plugin: terraform: Values: ["real-vpc-id"]
2017/05/15 15:48:26 [DEBUG] plugin: terraform: },{
2017/05/15 15:48:26 [DEBUG] plugin: terraform: Name: "tag:associate_public_ip_address",
2017/05/15 15:48:26 [DEBUG] plugin: terraform: Values: ["no"]
2017/05/15 15:48:26 [DEBUG] plugin: terraform: }]
2017/05/15 15:48:26 [DEBUG] plugin: terraform: }

Expected Behavior

  • Variables in the tag name should be interpolated
  • Value of the tag should be included (see bad output, not sure why it's missing but it exists in the good output)

Actual Behavior

Tag name is not interpolated

Steps to Reproduce

  1. terraform plan

Important Factoids

Your AWS account should have vpc and subnet(s). At least one subnet with the specified tag key and value for the successful refresh

Additional question

  • Since filtering subnets using tags is a relatively new feature (0.9.4) - how come it doesn't support variable interpolation from the beginning?
@apparentlymart

This comment has been minimized.

Contributor

apparentlymart commented May 15, 2017

Hi @dvishniakov! Sorry things didn't work as expected here.

It is a limitation of the configuration language that attribute names must be constants. This is true for all attribute names, not specifically the ones in this tags map.

However, it should be possible to work around this by dynamically producing a map using the map function, thus avoiding this limitation of the configuration language:

  tags = "${map(var.myvar, var.mymap["internal_subnet_tag.value"])}"

In future we may extend the configuration language to have dynamic keys for maps. At present this isn't possible because it doesn't have enough information during parsing to recognize the difference between a map (where keys are arbitrarily chosen by you) and other objects (where the keys are fixed, defined by the resource schema). Let's consider this issue to be a feature request for this working with the more intuitive syntax.

@dvishniakov

This comment has been minimized.

dvishniakov commented May 16, 2017

@apparentlymart thanks! This workaround helps at this point, however such feature still could be useful so please keep this enhancement open.

@joelittlejohn

This comment has been minimized.

joelittlejohn commented Feb 12, 2018

This is currently painful for Kubernetes clusters, since K8s typically uses tags like:

"kubernetes.io/cluster/myclustername" = "shared"

So what I really need is a set of tags like this:

resource "aws_subnet" "sn-backend" {
  ...
  count = "${length(var.zones)}"
  tags {
    "Name" = "sn-backend-${element(var.zones, count.index)}"
    "Contact" = "${var.contact}"
    "Environment"  = "${var.environment}-${var.region}"
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
    "kubernetes.io/role/internal-elb" = ""
  }
}

This isn't possible of course. Producing these tags as a map is hard, because I have interpolation required for keys and values. This requires a lot of variables to be defined because if I use the syntax ${map(...)} then I can't do any interpolation.

The best solution I've found is to define all tags that have interpolation in their value as a map in one var (subnettags), then define each key that has interpolation as another var, then finally define tags as a merge like:

resource "aws_subnet" "sn-backend" {
  ...
  tags = "${merge(var.subnettags, map(var.clustertag, "shared"))}"
}

but wow is this verbose. I feel like this issue should be reopened and fixed as it is really awkward to work around.

@joelittlejohn

This comment has been minimized.

joelittlejohn commented Feb 12, 2018

It seems like a simple workaround here would be to let people specify tags as lists of keys and values like:

resource "aws_subnet" "sn-backend" {
  tags = [
    {
      key = "Name"
      value = "sn-backend-${element(var.zones, count.index)}"
    },
    {
      key = "kubernetes.io/cluster/${var.cluster_name}"
      value = "shared"
    }
  ]
}

It would avoid having to change the fundamentally static nature of keys in the configuration language.

Is that viable? Does terraform support lists of maps in its syntax?

@apparentlymart apparentlymart added config and removed core labels Feb 16, 2018

@apparentlymart

This comment has been minimized.

Contributor

apparentlymart commented Feb 16, 2018

Hi all,

The new improved parser/interpreter we are currently integrating has support for interpolation into map keys, so once that is released the configurations discussed in this issue should work as expected.

Integrating this is a big project that addresses a number of different configuration limitations, so I can't yet say exactly when it will be done but it is the current focus for the Terraform team at HashiCorp.

@dwmkerr

This comment has been minimized.

dwmkerr commented Mar 15, 2018

For anyone who is still struggling with the best way to do this, and waiting for the map interpolation to be supported, here's the way I'm doing it now. This is about as clean as I've been able to get it, and is specifically for dealing with the kubernetes.io/cluster/<clustername> challenge. Will also share on the related issues:

  1. Define the 'common tags':
locals {
  common_tags = "${map(
    "Project", "openshift",
    "KubernetesCluster", "${var.cluster_id}",
    "kubernetes.io/cluster/${var.cluster_name}", "${var.cluster_id}"
  )}"
}
  1. Apply the common tags, adding any specific ones for your resource.
resource "aws_instance" "master" {
  //  Use our common tags and add a specific name.
  tags = "${merge(
    local.common_tags,
    map(
      "Name", "OpenShift Master"
    )
  )}"
}

Reference:

  1. Common tags: https://github.com/dwmkerr/terraform-aws-openshift/blob/feature/openshift-3.7/modules/openshift/01-tags.tf
  2. Applying tags: https://github.com/dwmkerr/terraform-aws-openshift/blob/feature/openshift-3.7/modules/openshift/06-nodes.tf
@vail130

This comment has been minimized.

vail130 commented Aug 20, 2018

Another option here is to use lifecycle ignore_changes to avoid unwanted plan diffs:

resource "aws_vpc" "vpc" {
  ...

  # Ignore k8s tag and tag count
  lifecycle {
    ignore_changes = ["tags.kubernetes", "tags.%"]
  }

  tags {
    # Other tags
  }
}
@apparentlymart

This comment has been minimized.

Contributor

apparentlymart commented Oct 27, 2018

Hi all! Sorry for the long silence here.

I took the example in the initial comment on this issue and adapted it a little to use a null_resource just because I don't have any VPCs in my test AWS account right now:

variable "mymap" {
  type = "map"
  default = {
    "internal_subnet_tag.key"   = "associate_public_ip_address"
    "internal_subnet_tag.value" = "no"
  }
}

resource "null_resource" "example" {
  triggers = {
    (var.mymap["internal_subnet_tag.key"]) = var.mymap["internal_subnet_tag.value"]
  }
}

This example uses the new ability for a map constructor to have arbitrary expressions for keys. Because we allow "naked" identifiers to be used as literal keys, a more complex expression like this one must be placed in parentheses so the parser can understand our intent. (It would also work to use a quoted string with an interpolation sequence as in the original example, but I used simple parentheses because they are less visually "noisy".)

I can see this working as expected in Terraform v0.12.0-alpha1:

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # null_resource.example will be created
  + resource "null_resource" "example" {
      + id       = (known after apply)
      + triggers = {
          + "associate_public_ip_address" = "no"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

In order to make this work, the new language parser makes a stronger distinction between attributes that happen to have map/object values (where dynamic keys are allowed) and nested blocks (where only literal keys are allowed, so that they can be validated early). The practical implication of this is that it is now required to use the equals sign to define a value for a map attribute, where before omitting this was just non-idiomatic.

As an example of what I mean, I've also included some of the examples shown in this issue updated to use the attribute definition syntax where previously they used nested block syntax:

data "aws_subnet_ids" "our_vpc" {
  vpc_id = "real-vpc-id"
  tags = {
    "${var.myvar}" = "${var.mymap["internal_subnet_tag.value"]}"
  }
}
resource "aws_subnet" "sn-backend" {
  ...
  count = "${length(var.zones)}"
  tags = {
    "Name" = "sn-backend-${element(var.zones, count.index)}"
    "Contact" = "${var.contact}"
    "Environment"  = "${var.environment}-${var.region}"
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
    "kubernetes.io/role/internal-elb" = ""
  }
}

The configuration upgrade tool that will be included with the v0.12.0 final release will be able to fix this usage automatically across your whole configuration, if you wish.

The technique of merging a set of common tags with some override tags will still work, and can now be expressed more clearly due to first-class expression syntax:

  tags = merge(
    local.common_tags,
    {
      Name = "OpenShift Master"
    }
  )

With all of that said, it looks like the use-cases covered by this issue have been addressed in the master branch and will be included in the subsequent v0.12.0 release, and so I'm going to close this. Thanks everyone for sharing your use-cases, and for your patience while we laid the groundwork to make this possible.

@dwmkerr

This comment has been minimized.

dwmkerr commented Oct 29, 2018

@apparentlymart - this is awesome, thank you! And thanks for the detailed update and usage examples. Really cool 馃槃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment