Conditionally skipping a variable assignment in a terraform module - terraform

I'm currently using the terraform-aws-eks module and wanted to setup a managed node group in an existing cluster. However, I only want this node group to appear in our dev environment (but still want the cluster to remain unchanged). Is there a way to skip a variable assignment conditionally for a module? I tried the below approach but get an error if var.deploy_managed_node_group = false. Terraform version 0.14.11.
module "eks" {
source = "./modules/eks-17.24.0"
cluster_enabled_log_types = var.cluster_enabled_log_types
cluster_name = local.eks_cluster_name
cluster_version = local.eks_version
iam_path = "/eks/"
manage_aws_auth = true
map_users = local.eks_users
map_roles = local.eks_roles
subnets = module.eks_vpc.private_subnets
vpc_id = module.eks_vpc.vpc_id
worker_groups = local.worker_groups
node_groups = var.deploy_managed_node_group ? local.node_groups : null
}
Error: Iteration over null value
node_groups variable from module:
variable "node_groups" {
description = "Map of map of node groups to create. See `node_groups` module's documentation for more details"
type = any
default = {}
}

When using types in Terraform such as set, list, or map, the omitted value should be "empty" instead of null if the value is utilized for iteration instead of an argument. Therefore:
node_groups = var.deploy_managed_node_group ? local.node_groups : {}
would be the ideal ternary here as the falsey value returned by the conditional is an empty map constructor.

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.

How to concatenate strings in Terraform output with for loop?

I have multiple aws_glue_catalog_table resources and I want to create a single output that loops over all resources to show the S3 bucket location of each one. The purpose of this is to test if I am using the correct location (because it is a concatenation of variables) for each resource in Terratest. I cannot use aws_glue_catalog_table.* or aws_glue_catalog_table.[] because Terraform does not allow to reference a resource without specifying its name.
So I created a variable "table_names" with r1, r2, rx. Then, I can loop over the names. I want to create the string aws_glue_catalog_table.r1.storage_descriptor[0].location dynamically, so I can check if the location is correct.
resource "aws_glue_catalog_table" "r1" {
name = "r1"
database_name = var.db_name
storage_descriptor {
location = "s3://${var.bucket_name}/${var.environment}-config/r1"
}
...
}
resource "aws_glue_catalog_table" "rX" {
name = "rX"
database_name = var.db_name
storage_descriptor {
location = "s3://${var.bucket_name}/${var.environment}-config/rX"
}
}
variable "table_names" {
description = "The list of Athena table names"
type = list(string)
default = ["r1", "r2", "r3", "rx"]
}
output "athena_tables" {
description = "Athena tables"
value = [for n in var.table_names : n]
}
First attempt: I tried to create an output "athena_tables_location" with the syntax aws_glue_catalog_table.${table} but does does.
output "athena_tables_location" {
// HOW DO I ITERATE OVER ALL TABLES?
value = [for t in var.table_names : aws_glue_catalog_table.${t}.storage_descriptor[0].location"]
}
Second attempt: I tried to create a variable "table_name_locations" but IntelliJ already shows an error ${t} in the for loop [for t in var.table_names : "aws_glue_catalog_table.${t}.storage_descriptor[0].location"].
variable "table_name_locations" {
description = "The list of Athena table locations"
type = list(string)
// THIS ALSO DOES NOT WORK
default = [for t in var.table_names : "aws_glue_catalog_table.${t}.storage_descriptor[0].location"]
}
How can I list all table locations in the output and then test it with Terratest?
Once I can iterate over the tables and collect the S3 location I can do the following test using Terratest:
athenaTablesLocation := terraform.Output(t, terraformOpts, "athena_tables_location")
assert.Contains(t, athenaTablesLocation, "s3://rX/test-config/rX",)
It seems like you have an unusual mix of static and dynamic here: you've statically defined a fixed number of aws_glue_catalog_table resources but you want to use them dynamically based on the value of an input variable.
Terraform doesn't allow dynamic references to resources because its execution model requires building a dependency graph between all of the objects, and so it needs to know which exact resources are involved in a particular expression. However, you can in principle build your own single value that includes all of these objects and then dynamically choose from it:
locals {
tables = {
r1 = aws_glue_catalog_table.r1
r2 = aws_glue_catalog_table.r2
r3 = aws_glue_catalog_table.r3
# etc
}
}
output "table_locations" {
value = {
for t in var.table_names : t => local.tables[t].storage_descriptor[0].location
}
}
With this structure Terraform can see that output "table_locations" depends on local.tables and local.tables depends on all of the relevant resources, and so the evaluation order will be correct.
However, it also seems like your table definitions are systematic based on var.table_names and so could potentially benefit from being dynamic themselves. You could achieve that using the resource for_each feature to declare multiple instances of a single resource:
variable "table_names" {
description = "Athena table names to create"
type = set(string)
default = ["r1", "r2", "r3", "rx"]
}
resource "aws_glue_catalog_table" "all" {
for_each = var.table_names
name = each.key
database_name = var.db_name
storage_descriptor {
location = "s3://${var.bucket_name}/${var.environment}-config/${each.key}"
}
...
}
output "table_locations" {
value = {
for k, t in aws_glue_catalog_table.all : k => t.storage_descriptor[0].location
}
}
In this case aws_glue_catalog_table.all represents all of the tables together as a single resource with multiple instances, each one identified by the table name. for_each resources appear in expressions as maps, so this will declare resource instances with addresses like this:
aws_glue_catalog_table.all["r1"]
aws_glue_catalog_table.all["r2"]
aws_glue_catalog_table.all["r3"]
...
Because this is already a map, this time we don't need the extra step of constructing the map in a local value, and can instead just access this map directly to build the output value, which will be a map from table name to storage location:
{
r1 = "s3://BUCKETNAME/ENVNAME-config/r1"
r2 = "s3://BUCKETNAME/ENVNAME-config/r2"
r3 = "s3://BUCKETNAME/ENVNAME-config/r3"
# ...
}
In this example I've assumed that all of the tables are identical aside from their names, which I expect isn't true in practice but I was going only by what you included in the question. If the tables do need to have different settings then you can change var.table_names to instead be a variable "tables" whose type is a map of object type where the values describe the differences between the tables, but that's a different topic kinda beyond the scope of this question, so I won't get into the details of that here.

how to dynamically pass parameter to terraform module and run a module based on a condition

I have a requirement to run modules on conditional basis and also to build the parameter list for the called module dynamically from map variable.
My main.tf file looks like below
provider "aws" {
region = var.region
}
module "CreateResource1" {
source = "./modules/CreateResource1"
ProductName = "Test1"
ProductColour = "Red"
ProductShape = "Hexagone"
}
module "CreateResource2" {
source = "./modules/CreateResource2"
ProductName = "Test2"
ProductType = "xyz"
ProductModel = "abc"
ProductPrice = ""
}
the requirement is a conditional variable module_name which user will pass and based on that i need to execute any one of the modules as per condition.
Also instead of passing the parameter like ProductName, ProductColour, ProductShape as a separate variables the user will be passing them as a dict variable and i would like to build the variable dynamically with both key and value for the module.
Input from user will be like below.
module "Resource" {
module_name = "CreateResource1"
parameters = {
ProductName = "Test1"
ProductColour = "Red"
ProductShape = "Hexagone"
}
}
based on above inputs i need to select the module to run and build parameters for the module.
as i am new to terraform any leads will be appreciated.
Terraform Version used is 1.0.0
Regards
If possible, I would just change the modules to accept a map like the calling module:
(Disclaimer: partial, untested example)
modules/resource/main.tf
variable "module_name" {
type = string
}
variable "parameters" {
type = map
}
module "CreateResource1" {
source = "../CreateResource1"
count = var.module_name == "CreateResource1" ? 1 : 0
parameters = var.parameters
}
module "CreateResource2" {
source = "../CreateResource2"
count = var.module_name == "CreateResource2" ? 1 : 0
parameters = var.parameters
}
main.tf
module "Resource" {
source = "./modules/resource"
module_name = "CreateResource1"
parameters = {
ProductName = "Test1"
ProductColour = "Red"
ProductShape = "Hexagone"
}
}
The modules that are called would need a small modification to define a parameter input variable of type map and then the values could be accessed in the code using a lookup function (e.g. lookup(var.parameters, "ProductName", "") - note the 3rd param allows you to specify a default if the element does not exist in the map. There is no reason why that value cannot be taken from an input variable, so if you don't want to hard code it here, you could for example, pass it in from a default set in the calling module, and passed into all/some of the children). So for example:
modules/CreateResource1/main.tf (Partial example)
variable "parameters" {
type = map
}
resource "some_resource_type" "some_resource_name" {
ProductName = lookup(var.parameters, "ProductName", "Product1")
ProductColour = lookup(var.parameters, "ProductColour", "Red")
ProductShape = lookup(var.parameters, "ProductShape", "Circle")
}
So in this example, the resource created by CreateResource1 requires 3 params. If they exist in the map that is passed by module Resource then they will be used, and for any that are not present in the map that is passed in, the defaults will be used (in this case "Product1", "Red" & "Circle").

How can I make Terraform replace a null value with a default value?

The Terraform documentation indicates this should already be happening:
https://www.terraform.io/docs/language/expressions/types.html
null: a value that represents absence or omission. If you set an argument of a resource or module to null, Terraform behaves as though you had completely omitted it — it will use the argument's default value if it has one, or raise an error if the argument is mandatory.
I'm calling a module "foo" that has the following variable file:
variable "bar" {
type = string
default = "HelloWorld"
}
Example 1
When I call it using this code:
module "foo" {
source = "../modules/foo"
bar = null
}
The result is an error. Invalid value for "str" parameter: argument must not be null. Trigger when bar is being used.
Example 2
When I call it using this code (omitting it, rather than nulling it):
module "foo" {
source = "../modules/foo"
# bar = null
}
The result is that it works. The "bar" variable is defaulted to "HelloWorld".
This appears to be a bug In Terraform that someone else also raised but wasn't resolved.
https://github.com/hashicorp/terraform/issues/27730
Does anyone know a solution or a work around?
Version information:
Terraform v1.0.5
on linux_amd64
+ provider registry.terraform.io/hashicorp/google v3.51.0
+ provider registry.terraform.io/hashicorp/null v3.1.0
+ provider registry.terraform.io/hashicorp/random v3.1.0
+ provider registry.terraform.io/hashicorp/time v0.7.2
Workaround
Based on #Matt Schuchard's comment and some research there's a ugly solution using the conditional check:
variable "foo" {
type = string
default = "HelloWorld"
}
locals {
foo = var.foo == null ? "HelloWorld" : var.foo
}
Why
My use case is an attempt to avoid duplicated code. I have 2 very similar modules, one being a subset of the other. The solution I'm using is to put the modules in sequence calling each, i.e. a grandparent, parent and child.
I want to have the variables available to the "grandparent" but if they're omitted then the module below "child" should set them using a default value, e.g. "HelloWorld". But to exposed those variables all the way through the family line I have to include them in all modules and in the high modules (grandparent and parent) I want to default them to null, allowing them to be optional but also still causing them to be set to a default in the "child" further down the line.
...I think I need a diagram.
As of Terraform 1.1.0, variable declarations now support a nullable argument. It defaults to true to preserve the existing behavior. However, any variable with nullable=false that is unspecified or set to null will instead be assigned the default value.
main.tf:
variable "nullable" {
type = string
default = "Used default value"
}
output "nullable" {
value = coalesce(var.nullable, "[null]")
}
variable "non_nullable" {
type = string
default = "Used default value"
nullable = false
}
output "non_nullable" {
value = coalesce(var.non_nullable, "[null]")
}
terraform.tfvars
nullable = null
non_nullable = null
Note the use of coalesce in the output blocks. Terraform elides any outputs that are set to null, so this ensures that any null value still shows something in the output.
After applying this configuration, we can see from running terraform output that when nullable=true (the default) a variable keeps an explicitly set null value but with nullable=false any null value is ignored in favor of the default.
# terraform output
non_nullable = "Used default value"
nullable = "[null]"
This is for older versions of Terraform
Use the nullable approach if using Terraform 1.1.x or above and if you do not need to support older versions of terraform
You can set the default to null and set the real default value in the local via a coalesce function
Use try
This works for a null and empty string
variable "foo" {
type = string
default = null
}
locals {
# return var.foo if it's not null
# return "HelloWorld" if var.foo is null
foo = try(length(var.foo), 0) > 0 ? var.foo : "HelloWorld"
}
output "foo" {
value = local.foo
}
Use coalesce
This only works for a null string and not an empty string
variable "foo" {
type = string
default = null
}
locals {
# return var.foo if it's not null
# return "HelloWorld" if var.foo is null
foo = coalesce(var.foo_coalesce, "HelloWorld")
}
output "foo" {
value = local.foo
}
Using the default value of null returns HelloWorld
$ terraform apply -auto-approve
...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
foo = "HelloWorld"
Using a new value negates the default set in the local
$ terraform apply -auto-approve -var="foo=HelloUniverse"
...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
foo = "HelloUniverse"
I hope a combination of contains() with a condition can solve the problem (terraform versions > 1.xx)
I could solve a for_each solution based on this combination.
In the following example, the .value part contains a set of attributes.
A bit lazy to experiment with the exact problem ;) , but this may help.
your_target_attr = contains(keys(each.value), "your_possible_attribute_value") ? each.value.your_possible_attribute_value : var.your_default_value

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.

Resources