Using templatefile() with tags: getting error "map of string required"

I’ve recently started as a DevOps Engineer at ACME Corporation
where all Terraformed resources are tagged using variables
defined on the command-line. Self-contained example:

provider "aws" {
  region = "us-east-2"
}

variable "acme_owner" {
  type = string
}

variable "acme_project" {
  type = string
}

resource "aws_instance" "example" {
  ami           = "ami-07efac79022b86107" # ubuntu 20
  instance_type = "t2.micro"
  tags = {
    Owner    = var.acme_owner
    Project = var.acme_project
  }
}

There are actually over half a dozen tags for each AWS resource, and
they are almost all defined using input variables, except
Name which includes the resource name (e.g. acme-prod-webserver)
where “prod” is the environment name and “webserver” is the
resource name.

This “tags” block appears many times in the company code base, and it is identical nearly every time, except the Name varies.

I tried breaking out the tags into a template to reduce code duplication, like so:

tags.j2:

{
  Name    = ${name}
  Owner   = var.acme_owner
  Project = var.acme_project
}

main.tf:

provider "aws" {
  region = "us-east-2"
}

variable "acme_owner" {
  type = string
}

variable "acme_project" {
  type = string
}

resource "aws_instance" "webserver" {
  ami           = "ami-07efac79022b86107" # ubuntu 20
  instance_type = "t2.micro"
  tags = templatefile("${path.module}/tags.j2",
                      { name = "acme-prod-webserver" })
}

But when I ran it with terraform plan -var acme_owner=foo -var acme_project=bar I got an error:

Error: Incorrect attribute value type

  on main.tf line 17, in resource "aws_instance" "webserver":
  17:   tags = templatefile("${path.module}/tags.j2",
  18:                       { name = "acme-prod-webserver" })
    |----------------
    | path.module is "."

Inappropriate value for attribute "tags": map of string required.

Now, I’m confused. I know what a map is, and what a string is, but what is a map of string?

Hi @atsaloli,

I guess I will first start by defining “map of string”. In Terraform, all collections have a specified element type, so a “map of string” is a map whose elements are all strings. Similarly, a “map of number” would be a map whose elements are all numbers. A “list of string” is a list whose elements are all strings.

Terraform is returning this error because the template you’ve written results in the following string:

{
  Name    = acme-prod-webserver
  Owner   = var.acme_owner
  Project = var.acme_project
}

Note that although this string looks like it contains Terraform language syntax, it’s just a string that happens to have some braces and equals signs in it… Terraform doesn’t interpret the result in any way.

It would in principle be possible to write a template that produces a map of string as its result, but that would not be a typical way to meet the goal you stated here of having a common map of tags that you will assign to all AWS resources. You can do that without templates by defining a local value:

locals {
  common_tags = {
    Owner   = var.acme_owner
    Project = var.acme_project
  }
}

With that local value defined, you can use local.common_tags to refer to that mapping elsewhere in your module. In your case here that’s only a subset of the tags you need, so you can use that result with the merge function to merge in the resource-specific tags, like this:

  tags = merge(
    local.common_tags,
    { name = "acme-prod-webserver" },
  )

The templatefile function is typically used only for constructing complex multi-line strings that are in some externally-defined format. From Terraform’s perspective the result is always taken as a literal value – usually a string – and so the result will usually be passed into some argument of a resource that is expecting a multi-line string value, such as a Cloud-Init configuration in aws_instance's user_data.

Hi @apparentlymart

That works beautifully! Thank you for the assist! And for the explanation. Much appreciated!

Best,
Aleksey