kaiser
kaiser

Reputation: 22333

Terraform 0.12 AWS resource containing JSON built from variable

To provision tag policies in an AWS organization, I need to build the JSON content from variables. Management of tag policies, scp, etc. shall be centralized, so changes can be applied everywhere: Renaming, adding, removing tags, etc.

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}
provider "aws" {
  profile = "default"
  region  = "us-west-1"
}

The problem at hand I am facing is: How would I build the JSON object?

Example variable/ tag map:

# tag_policies.tf
variable "resource_tags" {
  description = "Central resource tags"
  type = list( object( {
    name = string
    tags = map(string)
  } ) )
  default = [
    {
      name = "Environment"
      tags = {
        prod = "crn::env:prod"
        lab = "crn::env:lab"
        dev = "crn::env:dev"
      }
    }
  ]
}

What I have tried so far is to use HCL template tags, but I end up with one , comma too much when iterating through the map of tag names. This works fine for the join() with the sub-map of tag names, but does not workout if I try to wrap the template markup. Why did I try this? Because I ran out of ideas.

# vars.tf
resource "aws_organizations_policy" "root-tag-policy" {
  name = "RootTagPolicy"
  type = "TAG_POLICY"

  content = <<CONTENT
{
  "tags": {
    %{ for tag in var.resource_tags_env ~}
      "${tag.name}": {
        "tag_key": {
          "@@assign": "${tag.name}",
          "@@operators_allowed_for_child_policies": [ "@@none" ]
        },
        "tag_value": { "@@assign": [ "${join( ", ", values( tag.tags ) )}" ] }
      },
    %{ endfor ~}
  }
}
CONTENT
}

Upvotes: 0

Views: 847

Answers (3)

kaiser
kaiser

Reputation: 22333

After reading @martin-atkins answer, I finally understood how the for works for objects and maps. The var before the => arrow actually is part of the resulting object. (This highly confused me as I compared it to other languages arrow functions and arguments.)

The first part of the process is to build a map of maps. The main reason is that I don't want to have a convention of a name key in a map of variables. This might lead to handling of conventions later on, what should be avoided at all costs as it is a possible trap if one does not pay close attention or is aware of it. So the key actually is the name now.

Data Structure

variable "resource_tags" {
  description = "Central resource tags"
  type = map(
    map(string)
  )
  default = {
    Environment = {
      common = "grn::env:common"
      prod = "grn::env:prod"
      stage = "grn::env:stage"
      dev = "grn::env:dev"
      demo = "grn::env:demo"
      lab = "grn::env:lab"
    },
    Foo = {
      bar = "baz"
    }
  }
}

The content as JSON

After understanding that the key in { "tags": { … } } is just the part before the =>, I could reduce the final resource to the following block.

resource "aws_organizations_policy" "root-tag-policy" {
  name = "RootTagPolicy"
  description = "Tag policies, assigned to the root org."
  type = "TAG_POLICY"

  content = jsonencode({
    tags = {
      for key, tags in var.resource_tags : key => {
        tag_key = {
          "@@assign" = key
          "@@operators_allowed_for_child_policies" = ["@@none"]
        }
        tag_value = {
          "@@assign" = values( tags )
        }
      }
    }
  })
}

Quick test:

Add the following output statement after the resource block:

output "debug" {
  value = aws_organizations_policy.tp_root-tag-policy.content
}

Now apply (or plan or refresh) just this resource. It's faster this way. Then output the built debug from the apply or refresh run.

$ terraform apply -target=aws_organizations_policy.root-tag-policy
…things happening…
$ terraform output debug | json_pp

ProTips:

  1. Pipe the output of the output directly into json_pp or jq so you can read it.
  2. Use jq . if you want validation on top. If you see the output, it means it's valid. Else you should receive 0 as response.

Upvotes: 0

Martin Atkins
Martin Atkins

Reputation: 74054

kaiser's answer shows a good general approach: build a suitable data structure and then pass it to jsonencode to get a valid JSON string from it.

Here's an example that I think matches what the string template in the original question would've produced:

  content = jsonencode({
    tags = {
      for tag in var.resource_tags_env : tag.name => {
        tag_key = {
          "@@assign" = tag.name
          "@@operators_allowed_for_child_policies" = ["@@none"]
        }
        tag_value = {
          "@@assign" = values(tag.tags)
        }
      }
    }
  })

I'm not familiar with the aws_organizations_policy resource type so I'm sorry if I got some details wrong here, but hopefully you can adapt the above example to generate the JSON data structure you need.

Upvotes: 1

kaiser
kaiser

Reputation: 22333

The solution actually was quite simple: Iterate of the tags using a for expression and enclose it with curly braces { … } to return an object (=> returns tuples).

Finally jsonencode() cares about converting the HCL key = value syntax to proper JSON.

resource "aws_organizations_policy" "root-tag-policy" {
  name = "RootTagPolicy"
  type = "TAG_POLICY"

  content = jsonencode( [ for key, tag in var.resource_tags: {
    "${tag.name}" = {
      "tag_key" = {
        "@@assign" = tag.name,
        "@@operators_allowed_for_child_policies" = [ "@@none" ]
      },
      "tag_value" = { "@@assign" = [ join( ", ", values( tag.tags ) ) ] }
    }
  } ] )
}

EDIT This still does not work, as I forgot that the whole JSON object needs to get wrapped inside a tags: {}.

Upvotes: 1

Related Questions