How to format numeric variable in Terraform - terraform

I have a following (simplified) Terraform code:
variable "cluster_id" {
default = 1
}
resource "aws_instance" "instance" {
... some instance properties ...
tags {
"Name" = "${format("cluster-%02d", var.cluster_id)}"
}
}
And when I run terraform apply the plan shows:
tags.Name: "%!d(string=1)"
The cluster_id in format() is not handled as a number so formatting fails. I would expect that I get cluster-01 but that's not the case.
Am I doing something wrong or is it really not possible to use custom variables as numbers in formatting?

Terraform, pre 0.12, only supports string, list and map types as an input variable so despite you providing an integer (or a float or a boolean) it will be cast to a string.
Both Terraform and Go allow you to use the same padding for integers and strings though so you can just use the following to 0 pad the cluster_id:
resource "aws_instance" "instance" {
# ... some instance properties ...
tags {
"Name" = "${format("cluster-%02s", var.cluster_id)}"
}
}

Another option I found is to do ${format("cluster-%02d", var.cluster_id+0)}. Adding zero produces real number and then %02d works correctly. But using %02s is cleaner.

Related

Is there a condition in terraform same as CloudFormation?

I see people using count to block resource creation in terraform. I want to create some resources if a condition is set to true. Is there such a thing same as in CloudFormation?
You answered yourself, the most similar thing is the count
You can use it combined with a conditional expression, like
resource "x" "y"{
count = var.tag == "to_deploy" ? 1 : 0
}
But this is just a stupid example, you can put everything, also use functions
count = max(var.array) >= 3 ? 1 : 0
And if you need to put a condition on something more complex, you can evaluate to use a locals block where do all elaboration you need, and just use some bool, or what you want, resultant from that in conditional expression.
I would like to help you more, but I should know your specific case, what are the conditions you would have.
In CloudFormation a "condition" is a top-level object type alongside resources, outputs, mappings, etc.
The Terraform language takes a slightly more general approach of just having values of various data types, combining and transforming them using expressions. Therefore there isn't a concept exactly equivalent to CloudFormation's "conditions", but you can achieve a similar effect in other ways using Terraform.
For example, if you want to encode the decision rule in only a single place and then refer to it many times then you can define a Local Value of boolean type and then refer to that from multiple resource blocks. A local value of boolean type is essentially equivalent to a condition object in CloudFormation. The CloudFormation documentation page you linked to has, at the time of writing, an example titled "Simple condition" and the following is a roughly-equivalent version of that example in the Terraform language:
variable "environment_type" {
type = string
validation {
condition = contains(["prod", "test"], var.environment_type)
error_message = "Must be either 'prod' or 'test'."
}
}
locals {
create_prod_resources = (var.environment_type == "prod")
}
resource "aws_instance" "example" {
ami = "ami-0ff8a91507f77f867"
instance_type = "..."
}
resource "aws_ebs_volume" "example" {
count = local.create_prod_resources ? 1 : 0
availability_zone = aws_instance.example.availability_zone
}
resource "aws_volume_attachment" "example" {
count = local.create_prod_resources ? 1 : 0
volume_id = aws_ebs_volume.example[count.index].id
instance_id = aws_instance.example.id
device = "/dev/sdh"
}
Two different resource blocks can both refer to local.create_prod_resources, in the same way that the two resources MountPoint and NewVolume can refer to the shared condition CreateProdResources in the CloudFormation example.

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

how would be possbile to assign a value from a rout call to a terraform variable?

is it possible to get a value from a data call and assign it to a variable ?
i m trying to get some values with the calls like :
data "oci_core_vcns" "test_vcns" {
#Required
compartment_id = "${var.compartment_ocid}"
}
output "vcn_state" {
value = ${data.oci_core_vcns.test_vcns.virtual_networks[0].state}"
}
i dont want to reference to my scripts the return value
${data.oci_core_vcns.test_vcns.virtual_networks[0].state}
but i wanted to do something like
var.vcn_state = {data.oci_core_vcns.test_vcns.virtual_networks[0].state}
where vcn_state is declared as a variable in my variables.tf file
but this code is not working
Would be possible to create a variable like :
variable "vcn_state" {
type = "string"
default = ""
}
and then assign to it values from data calls ?
thanks
locals is what you are looking for
Input Variables in Terraform are most analogous to the parameters on a function: they are assigned by the caller and used by the module configuration.
What you are looking for here is an analog to local variables, which in Terraform are called Local Values, reflecting the fact that they have a fixed expression at declaration time and cannot vary during an operation or between operations.
variable "compartment_ocid" {
type = string
}
data "oci_core_vcns" "test_vcns" {
compartment_id = var.compartment_ocid
}
locals {
vcn_state = data.oci_core_vcns.test_vcns.virtual_networks[0].state
}
output "vcn_state" {
value = local.vcn_state
}
To complete the analogy to functions, note that output values (the output block type) serve as similar role as a function's return values.
The above example uses Terraform 0.12 syntax. If you are using Terraform 0.11 then the above patterns should work if you wrap each of the reference expressions in the "${ ... }" interpolation syntax.

How to create unstructured data in terraform

I'm trying to create configurations in terraform that I can later pass to modules (I'm doing this to work around the lack of "count" in modules).
The closest thing I got was using a null_data_source but the problem with that is that it only supports a single level of properties in inputs:
data "null_data_source" "my_data" {
count = var.my_data_count
inputs = {
settings = { ... } //this doesn't work
}
}
Then I looked at the docs of how to create a custom provider but couldn't work around the types that terraform supports - TypeMap will automatically turned into map[string]string unless I pass in the Elem property but that also only accepts terraform defined types (it doesn't accept standard golang types e.g.: map[string]interface{} or interface{}).
Does anyone know a way to get unstructured data as config like this?
There is no such thing as "unstructured data" in Terraform: every value has an associated type. However, in Terraform 0.12 introduced two structural types that allow for different element/attribute types to be mixed together inside a single value, which is not possible for the collection types.
You can use Local Values if you need to factor out the expressions for these structural values for use in multiple locations:
locals {
your_data = {
settings = {
foo = "bar"
baz = []
}
}
}
Although the details of this often don't matter, Terraform will see the above as being of the following type:
object({
settings = object({
foo = string
baz = tuple([])
})
})
As the author of a module, you can associated with each variable a type constraint that can both check that the given value has the appropriate type and give Terraform some hints to interpret such a value differently. For example, if baz in the above example were a list of strings whose length isn't fixed by the module (often the case) then you can specify it as such in your type constraint:
variable "example" {
type = object({
settings = object({
foo = string
baz = list(string)
})
})
}
Then the caller can pass in the local value we constructed earlier:
module "example" {
source = "./modules/example"
example = local.your_data
}
Terraform will then take the tuple([]) value from the local value and convert it automatically to list(string), in this case creating an empty list of strings.
For Terraform 0.11 your options are more limited, because it does not have structural types. In that case, the usual approach is to flatten the structure into many separate variables and set them separately, but then it's not possible to conveniently construct them all in one place and pass them as a single value.

Terraform - How to restrict an input variable to a list of possible choices

I have a variable that the user will input during run time. Lets say the variable name is region. However, I want the execution to be only successful if the user picks a value from one of the values defined in a list/ choices.
how can I restrict it so the user's selection has to match values that are considered acceptable in the variable definition?
Stumbled across this question.
Since v0.13.0 input validation has been possible directly via the input variables. Thus you can actually achieve this with a snippet such as below.
variable "test_variable" {
type = string
description = "some test value"
validation {
condition = contains(["item1", "item2", "item3"], var.test_variable)
error_message = "Valid values for var: test_variable are (item1, item2, item3)."
}
}
Read more here - https://www.hashicorp.com/blog/custom-variable-validation-in-terraform-0-13
One solution:
variable "values_list" {
description = "acceptable values"
type = "list"
default = ["true", "false"]
}
variable "somevar" {
description = "must be true or false"
}
resource "null_resource" "is_variable_value_valid" {
count = "${contains(var.values_list, var.somevar) == true ? 0 : 1}"
"ERROR: The somevar value can only be: true or false" = true
}
If you pass a value different than "true" or "false" for the "somevar" variable, Terraform will throw an error and stop. The disadvantage is that you have to list all values in the default block of values_list.
Source: https://stackoverflow.com/a/54256780/1364793
Terraform currently has no first-class feature for this, but you can achieve the desired effect (albeit with a less-helpful error message) by asking Terraform to look the value up in a map:
variable "example" {
description = "must be a, b, or c"
}
locals {
allowed_example_values = { for v in ["a", "b", "c"] : v => v }
checked_example = local.allowed_example_values[var.example] # will fail if var.example is invalid
}
Because the values in the allowed_example_values map are the same as the keys, you can then use local.checked_example in place of var.example everywhere else in the module to set up the dependency graph such that it's impossible for an invalid value to be used.
Some caveats/limitations:
You can't customize the error message that Terraform will return when the value is invalid. Instead, Terraform will return the generic error about the key not matching any element in the map. However, it will include the source code snippet from the affected line in the error message (in Terraform 0.12 or later) so the comment at the end of that line should be visible in the error message and thus provide an additional clue to the user as to what might be wrong.
This works fully only for string values, because map keys are always strings in Terraform. Using other primitive types can work as a result of Terraform's automatic conversions from bool and number to string, but you should be sure to explicitly declare the type of the variable (using e.g. type = number) to ensure that Terraform will normalize incoming values into the expected type before looking them up in the map. This technique will not work at all for collection and structural types.
I wouldn't suggest using strings "true" and "false" since Terraform has a bool type which can represent that more intuitively. If you are using Terraform 0.11 and earlier then it's not possible to constrain to bool, but in Terraform 0.12 and later you can declare a variable as type = bool and then Terraform will handle the validation automatically.

Resources