How do you create scalar arrays/lists in Terraform? - terraform

myvar should be a list of security groups.
variable "myvar" {
default = null
}
If users specify it that list is concatenated with the default security group (pulled in from the data source).
If it's not specified just use the default security group.
This is not working:
local {
test = var.myvar != null ? concat(tolist(data.aws_security_group.data.id), var.myvar) : tolist(data.aws_security_group.data.id)
}
But this does work:
aaa = var.myvar != null ? concat(["aaaa"], ["bbbbb","ccccccc"]) : ["aaaa"]
So how to I convert a string to a scalar array/list? It seems like that's what Terraform needs and tolist() is not working.

Based on the given requirements, I think the most straightforward solution would be to set the default for the variable to [] and avoid the need for conditionals at all:
variable "additional_security_group_ids" {
type = list(string)
default = []
}
locals {
security_group_ids = concat(
[data.aws_security_group.default.id],
var.additional_security_group_ids,
)
}
Concatenating an empty list just produces the same list, so leaving the variable unset in the above would cause local.security_group_ids to contain only the default security group id.
Setting the default to null is useful when the absence of a value for that variable disables some feature entirely, or if the logic you need can't be conveniently expressed via defaults, but I'd always recommend using specific default values where possible because the result will tend to be easier to read and understand for future maintainers.

Is this what you're looking for?
value = var.myvar != null ? concat([data.aws_security_group.data.id], var.myvar) : [data.aws_security_group.data.id]

Proposing this as answer, but hoping there is a less crazy way
local {
test = var.myvar != null ? flatten(concat(tolist([data.aws_security_group.data.id]), [var.myvar])) : tolist([data.aws_security_group.data.id])
}

Related

TF: Loop through map with lookup and set variable accordingly

I have a map with some environment ID's as the key, then keywords as the values in a list.
variable "environments" {
type = map(list(string))
default = {
"env-one" = ["dev", "test", "stage", "staging"],
"env-two" = ["prod", "production", "live"]
}
}
I'm looking to use this to set the environment name based on the value of var.context["stage"].
So, if var.context["stage"] is equal to staging the value of environment will be dev
I was initially thinking to use lookup(), something like;
environment = "${lookup(var.environments, var.context["stage"])}"
However, I realise that's looking up the wrong way (finding the value as opposed to the key), and also it won't work as part of a map. So presumably I need to look through the map and run the lookup (albeit) backwards(?) on each iteration?
You would want to restructure the type into map(string). Then it would follow that the value would be:
{
"dev" = "env-one",
"test" = "env-one",
"stage" = "env-one",
"staging" = "env-one",
"prod" = "env-two",
"production" = "env-two",
"live" = "env-two"
}
You could also modify this to be map(object) to contain more information. Based on the usage described in the question, this would actually make more sense to be a local. If you were to place this data into a locals block named environments, then the key-value pair could be accessed (according to the question) like local.environments[var.context["stage"]].

Terraform: Create block only if variable matches certain values

I'm trying to create a module that creates interconnect-attachments, but some parts are only defined if the attachment is using ipsec encryption and if it's not, that block must not exist in the resource else it causes an error (even if it only contains a value set to null.)
I've tried using a dynamic, but I can't quite get the layout right to have it work:
resource "google_compute_interconnect_attachment" "interconnect-attachment" {
project = var.project
region = var.region
name = var.name
edge_availability_domain = var.availability_domain
type = var.type
router = google_compute_router.router.name
encryption = var.encryption
dynamic "ipsec_internal_addresses" {
for_each = var.encryption != "IPSEC" ? [] : [1]
content {
var.address
}
}
}
Essentially, if var.encryption is set to IPSEC then i want the following block included:
ipsec_internal_addresses = [
var.address,
]
The other issue is it appears a dynamic block expects some kind of assignment to happen, but the terraform examples just have the value inside the ipsec_internal_addresses so I'm unsure how to to achieve this.
ipsec_internal_addresses is not a block in the google_compute_interconnect_attachment resource. It is an argument. Therefore, you can use the normal pattern for specifying optional arguments where the conditional returns a null type if you do not want to specify a value. Using your conditional and variables:
ipsec_internal_addresses = var.encryption == "IPSEC" ? [var.address] : null
which will return and assign your [var.address] to ipsec_internal_addresses when var.encryption equals the string IPSEC. Otherwise, it will return null and the ipsec_internal_addresses argument will be ignored.

Any way to conditionalize variable in jsonencoded data?

Say I have the simplified following snippet to create a task definition as json.
...
task_container_definitions = jsonencode([{
name : var.name,
image : "${var.image}:${var.tag}",
cpu : var.cpu,
memory : var.memory,
}])
...
Say I want to add a variable to optionally create an additional definition so it looks something like this:
variable "another_definition" {
type = any
default = {}
}
...
task_container_definitions = jsonencode([{
name : var.name,
image : "${var.image}:${var.tag}",
cpu : var.cpu,
memory : var.memory,
},
var.another_definition
])
And define it as follows.
another_definition = {
name = "another_container"
image = "another_container"
cpu = 10
memory = 512
essential = true
}
I am able to get this to to output as expected as long as the variable is defined.
...
+ {
+ cpu = 10
+ essential = true
+ image = "another_container"
+ memory = 512
+ name = "another_container"
},
But if the variable is not defined, I see empty {} added to the output when I do a terraform plan, which is not what I expect. I have tried using null as well as the default but get an error.
...
+ {},
Is there a way to toggle this variable off so that if it is not defined then it doesn't show up in the outputted json definition? Is there a better approach than what I am attempting?
I was a little confused at first as to what you were asking, thinking that you were asking for the functionality of the merge function, and I mention that only in case I was right the first time, but I think I now understand your problem as that you want this task_container_definitions to have either one or two elements, depending on whether var.another_definition is set.
There's no single function for that particular situation, but I think we can combine some language features together to get that result.
First, let's decide that the variable being set means that it has a non-null value, and thus its default value should be null to represent the "unset" case:
variable "another_definition" {
type = any
default = null
validation {
# The time constraint above is looser than we really
# want, so this validation rule also enforces that
# the caller can't set this to something inappropriate,
# like a single string or a list.
condition = (
var.another_definition != null ?
can(keys(var.another_definition)) :
true
)
error_message = "Additional task container definition must be an object."
}
}
In Terraform it's a pretty common situation to need to convert between a value that might be null and a list that might have zero or one elements, or vice-versa, and so Terraform has some language features to help with that. In this case we can use a splat expression to concisely represent that. Let's see how that looks in terraform console first just to give a sense of what we're achieving with this:
$ terraform console
> null[*]
[]
> "hello"[*]
[
"hello",
]
> { object = "example" }[*]
[
{
"object" = "example"
},
]
Notice that when I applied the [*] operator to null it returned an empty list, but when I applied it to these other values it converted them to a single-element list. This is how the [*] operator behaves when you apply it to something that isn't a list; see the splat operator docs if you want to learn about the different behavior for lists, which isn't really relevant here because of the validation rule I added above which prevents the var.another_definition value from being a list.
Another tool we have in our Terraform toolbox here is the concat function, which takes one or more lists and returns a single list with the input elements all concatenated together in the given order. We can use this to combine your predefined list that's populated from var.name, var.cpu, etc with the zero-or-one element list created by [*], in order to create a list with their one or two elements:
locals {
task_container_definitions = concat(
[
name = var.name
image = "${var.image}:${var.tag}"
cpu = var.cpu
memory = var.memory
],
var.another_definition[*],
)
task_container_definitions_json = jsonencode(local.task_container_definitions)
}
If any of the arguments to concat are empty lists then they are effectively ignored altogether, because they contribute no elements to the result, and so this achieves (what I hope is) the desired result, by making the "other definition" appear in the result only when it's set to something other than null.

What is the equivalent of "" for booleans in terraform?

I have a module that controls a handful of similar resources, and many of the settings in those resources are the same; so I've created global defaults that all of the resources in my module can refer to.
I want to have a set of default variables in my module and a set of variables that can override the default if the caller of my module decides to pass those in. What I've been using for strings is below (these are all in the same variables.tf file in my module).
My defaults:
variable "default_env" {default="test"}
My placeholder variables to allow calling resources to set them:
variable "x_env" {default=""}
variable "y_env" {default=""}
variable "z_env" {default=""}
And my attempt at guiding the user of the module towards which variables should be available for being overridden:
locals {
env = "${var.x_env != "" ? var.x_env : "${var.default_env}"}"
env = "${var.y_env != "" ? var.y_env : "${var.default_env}"}"
env = "${var.z_env != "" ? var.z_env : "${var.default_env}"}"
}
However, I can't figure out how to do this properly with booleans because I can't figure out how to create an empty boolean variable. My only option seems to be to also set a default value as part of my override variables:
variable "x_lock" {default=true}
Is there a way I can declare this in such a way that we don't have to maintain two sets of default values (1: variable "default_lock" {default=true}, 2: variable "x_lock" {default=true})?
I've tried doing:
variable "x_lock" {
type = bool
default = ""
}
But I obviously get an error that "" is not compatible with bool.
How else can I go about this?
The absence of a value is represented in Terraform by the keyword null, which is valid for any type.
Given that, in order to distinguish between true, false, and not set at all you can define a variable like this:
variable "x_lock" {
type = bool
default = null
}
Note that it's not really true to say that this is "the equivalent of an empty string for booleans". An empty string is not equal to null, and so if you want to explicitly represent the absence of a string it can often be best to use null for that too:
variable "x_env" {
type = string
default = null
}
...that way you can recognize an empty string as distinct from no string at all, similar to distinguishing false from no boolean at all.
null has a slightly different meaning in the context of a default than it does elsewhere. Setting default = null specifies that an input variable is optional without providing a default value for it. Or, if you like, saying that its default value is null.
An advantage of using null in this way is that you can pass that argument on as-is to any optional resource argument and it will be interpreted as if that argument were not set at all, rather than as if it were set to a default value.
There is also a further "special power" for null: if you use the splat operator [*] with a non-list value then it will return a single-element list for a non-null value and an empty list for a null value. This can be particularly useful if you are intending to use the "null-ness" of the value to decide whether or not to create a resource or a nested block:
variable "subscription_id" {
type = string
default = null
}
data "azurerm_subscription" "example" {
# Will be an empty set if `var.subscription_id` is null, or
# a single-item set if it is a non-null string.
for_each = toset(var.subscription_id[*])
subscription_id = each.key
}

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.
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).
terraform v1.0+ introduces a new function type() for this purpose. See https://www.terraform.io/language/functions/type

Resources