TF: Loop through map with lookup and set variable accordingly - terraform

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"]].

Related

Assign provided value if variable/local does not exist

I would like to set the disk_size doing something like the following:
resource "google_compute_disk" "terraform-additional-persistent-disk" {
name = "terraform-additional-persistent-disk"
zone = local.zone
type = local.default_disk_type
size = exists(local.default_disk_size) ? local.default_disk_size : 50
image = local.default_ubuntu_image
labels = {
created_by = "terraform"
}
}
However, I have not been able to find a exists function in Terraform. The main aim is to take the value of a variable/local if it exists and, if it has not been declared anywhere, then take the value I pass as second argument.
I have been checking other questions like 1 but neither can nor try function are helping me achieve my goal. I will always get either A local value with the name "default_disk_size" has not been declared or An input variable with the name "default_disk_size" has not been declared (depending on whether I use a non-existing local or var).
I have even tried to run the following, but it will always raise an exception if the variable/local has not been set. Is there a way of achieving this without explicitly declaring the variable with a default value of null/""?
Thanks!
resource "google_compute_disk" "terraform-additional-persistent-disk" {
name = "terraform-additional-persistent-disk"
zone = local.zone
type = local.default_disk_type
size = merge({sizee=50}, {sizee = local.default_disk_sizee})["sizee"]
image = local.default_ubuntu_image
labels = {
created_by = "terraform"
}
}
I think you're coming at this with the idea that input variables and locals may or may not exist at the time this resource is created, like they are system environment variables or something. However in Terraform, those things have to be explicitly declared in one of the .tf files in the same folder as the file your google_compute_disk.terraform-additional-persistent-disk is declared.
There would be no way in Terraform's syntax to have either local or input variables appear dynamically at runtime, they have to be declared ahead of time in your code. They will always exist.
If you want to allow someone using your Terraform code the option of passing in a variable or not, you have to explicitly declare the variable, and give it a default value. Then the person using your Terraform code can optionally override that default value. Like this:
variable "disk_size" {
type = number
default = 50
description = "The size of the additional persistent disk"
}
resource "google_compute_disk" "terraform-additional-persistent-disk" {
name = "terraform-additional-persistent-disk"
zone = local.zone
type = local.default_disk_type
size = var.disk_size
image = local.default_ubuntu_image
labels = {
created_by = "terraform"
}
}
Then when someone uses your Terraform code, if they don't specify a value for the disk_size input variable, the default of 50 will be used, but if they do specify something, then the value they specified will be used.

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.

How do you create scalar arrays/lists in 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])
}

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

Is it possible to generate a variable name in terraform

So i want to get the variable in the terraform remote state, however we have a number of different one per environment on the shared route53
So for a given environement, we want to pull the zone id out as such;
zone_id = data.terraform_remote_state.route_53.route53_zone_${var.environment}_id
How would I do this please.
In general, it is not possible to use arbitrary dynamic strings as variable names.
However, in this particular case the outputs from terraform_remote_state are collection values and so you can use the index syntax to access a dynamically-built key from your map value:
data.terraform_remote_state.outputs.route53["route53_zone_${var.environment}_id"]
With that said, if possible I would recommend structuring the output values better so that the Route53 zone ids are given as a map by environment, so that this can be obtained in a more intuitive way.
For example, you could make your route53 output be a map of objects whose keys are the environment names:
data.terraform_remote_state.outputs.route53[var.environment].zone_id
output "route53" {
value = tomap({
production = {
zone_id = aws_route53_zone.production.id
}
staging = {
zone_id = aws_route53_zone.staging.id
}
})
}
Or, if you have a variety of different per-environment settings you could structure it as a single output value that is a map of all of those per environment settings keyed by environment name:
data.terraform_remote_state.outputs.environments[var.environment].route53_zone_id
output "environments" {
value = tomap({
production = {
ec2_vpc_id = aws_vpc.production.id
route53_zone_id = aws_route53_zone.production.id
}
staging = {
ec2_vpc_id = aws_vpc.staging.id
route53_zone_id = aws_route53_zone.staging.id
}
})
}
This doesn't change anything about the ultimate result, but grouping things by your environment keys in your outputs is likely to make your intent clearer to future maintainers of these configurations.
(You might also consider whether it'd be better to have a separate configuration/state per environment rather than managing them altogether, but that is a big topic in itself.)

Resources