How to set dynamic arguments for terraform resource - terraform

Background
I am working to wrap a few resources into a module, and need to ensure that it is reusable.
Now working on a resource, lets all it mysql resource.
The terraform code of this module looks like this
resource "azurerm_mysql_flexible_server" "mysql" {
# some arguments
xx_id = var.xx_id
# some other arguments
}
According to the document from Terraform, resource can have some arguments that is optional, which means if I don't set it terraform will consider it as not exists.
Now, I want to conditionally set the xx_id, when var.enabled is true, set xx_id from var.xx_id, otherwise don't set it.
What I have tried
Set default value to ""
This equals to assign "", so not working.
Set nullable to true
Got this error message:
The argument "xx_id" is required, but no definition was found.

variable "xx_id" {
type = string
nullable = true
default = null
}
As last when I try the above code, it works.
Looks like:
nullable: Allow to assign null, but in previous attempt no value assigned to it, so it will report error, because this variable is still required.
default: Set null to it as allow null.

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 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: how to declare a variable as required (or optional) in variables.tf?

Here I have a variables.tf to define the Input variables.
# Input variable definitions
variable "project" {
description = "project name, e.g. paylocity, paychex, ultipro"
type = string
}
variable "environment" {
description = "the environment of project, e.g. production, sandbox, staging"
type = string
default = "sandbox"
}
Is there a way to mark a variable as required or optional?
The pseudo-code in my mind looks like this.
variable "project" {
description = "project name, e.g. paylocity, paychex, ultipro"
type = string
presence = required (or optional)
}
The purpose of variables.tf
variables.tf - here, you define the variables that must have values in order for your Terraform code to validate and run. You can also define default values for your variables in this file. Note that you don't need to define all of your variables in a file named
from What is the difference between variables.tf and terraform.tfvars?
Input Variables: Default Value
The variable declaration can also include a default argument. If present, the variable is considered to be optional and the default value will be used if no value is set when calling the module or running Terraform. The default argument requires a literal value and cannot reference other objects in the configuration.
From Terraform Documentation
Summary
All defined variables must have values in order to run Terraform code.
Once you set a default value for a variable, it becomes optional.
I'm using terraform 1.3.1. I think that this problem can solve with argument nullable.
The nullable argument in a variable block controls whether the
module caller may assign the value null to the variable.
This feature is available in Terraform v1.1.0 and later.
Example:
variable "example" {
type = string
nullable = false
}
Reference: https://developer.hashicorp.com/terraform/language/values/variables?optInFrom=terraform-io#disallowing-null-input-values
Since 0.14 there is an experimental (in time of writing) function that can allow you this. Here are some documents on the matter.
By default, for required attributes, Terraform will return an error if the source value has no matching attribute. Marking an attribute as optional changes the behavior in that situation: Terraform will instead just silently insert null as the value of the attribute, allowing the receiving module to describe an appropriate fallback behavior.
https://www.terraform.io/language/expressions/type-constraints#experimental-optional-object-type-attributes
https://danielrandell93.medium.com/terraform-optional-values-73407f1d5ce5

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