udondan
udondan

Reputation: 59989

Get type of a variable in Terraform

Is there a way to detect the type of a variable in Terraform? Say, I have a module input variable of type any, can I do some kind of switch, depending on the type?

variable "details" {
  type = any
}

local {
  name = var.details.type == map ? var.details["name"] : var.details
}

What I want to archive is, to be able to pass either a string as shorthand or a complex object with additional keys.

module "foo" {
  details = "my-name"
}

or

module "foo" {
  details = {
    name = "my-name"
    age = "40"
  }
}

I know this example doesn't make much sense and you would like to suggest to instead use two input vars with defaults. This example is just reduced to the minimal (non)working example. The end goal is to have a list of IAM policy statements, so it is going to be a list of lists of objects.

Upvotes: 8

Views: 17851

Answers (3)

evan lueck
evan lueck

Reputation: 21

[EDITED]

I had a use-case where type() would have been pretty helpful to use in my code for a Terraform module I was creating. But, as mentioned, the type() function is only usable currently (Terraform v1.5.6) in the Terraform console, which makes it bit more difficult to do type inference, but not impossible!

So here we go (also, I appreciate the feedback! I'm new to stackoverflow):

My module was using Terraform to render YAML for SSM document parameters, placing them onto SSM document runbooks, and inject them into a cloudformation template. I wanted to dynamically infer the types of my parameters but render the type verbiage that AWS syntax expects. But type inference is tricky in terraform.

I should note that the only AWS parameter type I wasn't sure how to infer was "MapList" (described here: https://docs.aws.amazon.com/systems-manager/latest/userguide/documents-syntax-data-elements-parameters.html#top-level).

Here is a full workflow example:

  1. Lets assume my Terraform plan (where I run the Terraform) lives here: ~/terraform/plans/my_plan. And my terraform module lives here: ~/terraform/modules/render_params

So, starting with the main.tf file in my_plan, to test all of the types (excluding mixed types), we have something that looks like this:

module "testing_some_renders" {
  source     = "../../modules/render_params"
  parameters = jsonencode({
    a = "b"
    b = "2"
    c = 2
    d = "01"
    e = 01
    f = {
      "a" = "b"
    }
    g = true
    h = false
    i = ["a", "b"]
  )
}
  1. The variables.tf file in the render_params module directory would look like this:
variable parameters {
  type        = string
  default     = ""
  description = "A jsonencoded string of key-value pairs whose content will be type inferred."
  1. The YAML file used in the templatefile() function call would live in render_params/templates/params.yaml And would look like this:
"parameters":
  ${indent(2, parameters)}
  1. Our module's main.tf would look like this:
locals {
  type_chart = {
    # https://docs.aws.amazon.com/systems-manager/latest/userguide/documents-syntax-data-elements-parameters.html#top-level
    "string"       = "String"
    "list(string)" = "StringList"
    "number"       = "Integer"
    "bool"         = "Boolean"
    "map(string)"  = "StringMap"
  }


  type_inferred_parameters = yamlencode({
    for k, v in jsondecode(var.parameters) : k => {
      type = can(tobool(v)) && v != null && can(tonumber(v)) == false ? local.type_chart["bool"] : (
        can(tolist(v)) && v != null ? local.type_chart["list(string)"] : (
          can(tomap(v)) && v != null ? local.type_chart["map(string)"] : (
            can(tonumber(v)) && format("%s", v) != v ? local.type_chart["number"] : (
              format("%s", v) == v && can(tobool(v)) == false ? local.type_chart["string"] : (
                "Indeterminable"
              )
            )
          )
        )
      )
      description = "Managed By Terraform."
      default = v
    }
  })

  rendered_yaml = templatefile("${path.module}/templates/params.yaml", {
    parameters = local.type_inferred_parameters
  })
}

output "final_render" {
  value = local.rendered_yaml
}
  1. Finally, our final output after the above render look like this:
"parameters":
  "a":
    "default": "b"
    "description": "Managed By Terraform."
    "type": "String"
  "b":
    "default": "2"
    "description": "Managed By Terraform."
    "type": "String"
  "c":
    "default": 2
    "description": "Managed By Terraform."
    "type": "Integer"
  "d":
    "default": "01"
    "description": "Managed By Terraform."
    "type": "String"
  "e":
    "default": 1
    "description": "Managed By Terraform."
    "type": "Integer"
  "f":
    "default":
      "a": "b"
    "description": "Managed By Terraform."
    "type": "StringMap"
  "g":
    "default": true
    "description": "Managed By Terraform."
    "type": "Boolean"
  "h":
    "default": false
    "description": "Managed By Terraform."
    "type": "Boolean"
  "i":
    "default":
    - "a"
    - "b"
    "description": "Managed By Terraform."
    "type": "StringList"

Upvotes: 2

Noah Sparks
Noah Sparks

Reputation: 1762

terraform v1.0+ introduces a new function type() for this purpose. See https://www.terraform.io/language/functions/type

Upvotes: -1

Martin Atkins
Martin Atkins

Reputation: 74249

Terraform v0.12.20 introduced a new function try which can be used to concisely select between different ways of retrieving a value, taking the first one that wouldn't produce an error.

variable "person" {
  type = any

  # Optional: add a validation rule to catch invalid types,
  # though this feature remains experimental in Terraform v0.12.20.
  # (Since this is experimental at the time of writing, it might
  # see breaking changes before final release.)
  validation {
    # If var.person.name succeeds then var.person is an object
    # which has at least the "name" attribute.
    condition     = can(var.person.name) || can(tostring(var.person))
    error_message = "The \"person\" argument must either be a person object or a string giving a person's name."
  }
}

locals {
  person = try(
    # The value of the first successful expression will be taken.

    {name = tostring(var.person)}, # If the value is just a string
    var.person,                    # If the value is not a string (directly an object)
  )
}

Elsewhere in the configuration you can then write local.person.name to obtain the name, regardless of whether the caller passed an object or a string.


The remainder of this answer is an earlier response that now applies only to Terraform versions between v0.12.0 and v0.12.20.

There is no mechanism for switching behavior based on types in Terraform. Generally Terraform favors selecting specific types so that module callers are always consistent and Terraform can fully validate the given values, even if that means a little extra verbosity in simpler cases.

I would recommend just defining details as an object and having the caller explicitly write out the object with the name attribute, in order to be more explicit and consistent:

variable "details" {
  type = object({
    name = string
  })
}
module "example" {
  source = "./modules/example"

  details = { name = "example" }
}

If you need to support two different types, the closest thing in the Terraform language would be to define two variables and detect which one is null:

variable "details" {
  type = object({
    name = string
  })
  default = null
}

variable "name" {
  type    = string
  default = null
}

local {
  name = var.name != null ? var.name : var.details.name
}

However since there is not currently a way to express that exactly one of those two must be specified, the module configuration you write must be ready to deal with the possibility that both will be set (in the above example, var.name takes priority) or that neither will be set (in the above example, the expression would produce an error, but not a very caller-friendly one).

Upvotes: 9

Related Questions