How to convert list(string) to Python's set format in Terraform - string

I would like to apply a variable as a resource's argument, using Python's set format syntax of {"element-1","element-2",...}. A guiding principle is to let the end-user load the module and enter the details as comfortably as possible.
I've created a module with the OPTIONAL LANGUAGES variable:
variable "OPTIONAL_LANGUAGES" {
type = list(string)
description = "List of ISO 639-1 two-letter language codes."
}
So when the end-user will use it, the module will look similar to that:
module "my_module_instance_name" {
source = "../modules/my_module"
OPTIONAL_LANGUAGES = ["aa","ab","ac"]
}
The thing is, that in the same module, I would like to apply this variable inside a resource's argument, using the following format:
{"aa","ab","ac"}
Is there a more user-friendly solution than passing it as a simple string of OPTIONAL_LANGUAGES = "{\"aa\",\"ab\",\"ac\"}"?

Related

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

Terraform resource as a module input variable

When developing a terraform module, I sometimes find myself in the need to define different input variables for the same resources. For example, right now I need the NAME and ARN of the same AWS/ECS cluster for my module, so, I defined two variables in my module: ecs_cluster_arn and ecs_cluster_name.
For the sake of DRY, it would be very nice if I could just define the input variable ecs_cluster of type aws_ecs_cluster and the just use whatever I need inside my module.
I can't seem to find a way to do this. Does anyone know if it's possible?
You can define an input variable whose type constraint is compatible with the schema of the aws_ecs_cluster resource type. Typically you'd write a subset type constraint that contains only the attributes the module actually needs. For example:
variable "ecs_cluster" {
type = object({
name = string
arn = string
})
}
Elsewhere in the module, you can use var.ecs_cluster.name and var.ecs_cluster.arn to refer to those attributes. The caller of the module can pass in anything that's compatible with that type constraint, which includes a whole instance of the aws_ecs_cluster resource type, but would also include a literal object containing just those two attributes:
module "example" {
# ...
ecs_cluster = aws_ecs_cluster.example
}
module "example" {
# ...
ecs_cluster = {
name = "blah"
arn = "arn:aws:yada-yada:blah"
}
}
In many cases this would also allow passing the result of the corresponding data source instead of the managed resource type. Unfortunately for this pairing in particular the data source for some reason uses the different attribute name cluster_name and therefore isn't compatible. That's unfortunate, and not the typical design convention for pairs of managed resource type and data source with the same name; I assume it was a design oversight.
module "example" {
# ...
# This doesn't actually work for the aws_ecs_cluster
# data source because of a design quirk, but this would
# be possible for most other pairings such as
# the aws_subnet managed resource type and data source.
ecs_cluster = data.aws_ecs_cluster.example
}

How to extend terraform module input variable schema without breaking existing clients?

I have a module with the following input variable:
variable "apsvc_map" {
description = "The App Services sharing the same App Service Plan. Maps an App Service name to its properties."
type = map(object({
identity_ids = list(string),
disabled = bool
}))
}
Now I would like to add a new property to the schema - no_custom_hostname_binding. The new version would be:
variable "apsvc_map" {
description = "The App Services sharing the same App Service Plan. Maps an App Service name to its properties."
type = map(object({
identity_ids = list(string),
disabled = bool
no_custom_hostname_binding = bool
}))
}
And this change can be made backwards compatible in the module code with the help of the try function, because omitting the new property is equivalent to providing it with the false value.
However, terraform treats this schema strictly and would not allow passing an input without the new field:
2020-05-30T15:34:20.8061749Z Error: Invalid value for module argument
2020-05-30T15:34:20.8062005Z
2020-05-30T15:34:20.8062205Z on ..\..\modules\web\main.tf line 47, in module "web":
2020-05-30T15:34:20.8062336Z 47: apsvc_map = {
2020-05-30T15:34:20.8062484Z 48: dfhub = {
2020-05-30T15:34:20.8062727Z 49: disabled = false
2020-05-30T15:34:20.8065156Z 50: identity_ids = [local.identity_id]
2020-05-30T15:34:20.8065370Z 51: }
2020-05-30T15:34:20.8065459Z 52: }
2020-05-30T15:34:20.8065538Z
I understand from the error that terraform complains because I did not specify the value for the new property in the input.
So, there are three solutions:
Update all the existing code to add the new property - out of the question.
Tag the new version of the module differently and let the new code reference the new tag, while the old code continues to reference the old tag - in the long run would lead to proliferation of tags, creating all kinds of bizarre Cartesian multiplications of features in the tag names. Ultimately - out of the question.
Relax the input variable schema by commenting out the optional properties and use try in the code.
The last option is not ideal, because the documentation for the module would not list the optional properties. But from the code management perspective - it is the best.
So the question is - can input object properties be defined as optional? Ideally, it should include the default value, but I am OK with the try approach for now.
EDIT 1
I actually thought I could pass unknown properties in the object, but no. Once the schema is given it is nothing less nothing more. So, the only backwards compatible solution is to use map(any) in my case.
Optional arguments in object variable have been suggested for Terraform:
https://github.com/hashicorp/terraform/issues/19898
Unfortunately as of May 30 2020, there has not been any progress on this.
That is the most upvoted issue on their repo, all we can do is keep upvoting and hopefully, that will be implemented soon.
And you are right the alternatives are just out of the question or plain hackish
Given your options, your preferences, and the fact that Terraform 0.12 doesn't support and Terraform 0.13 likely won't support optional or default values on objects, I think you have a fourth option:
variable "apsvc_map" {
description = "The App Services sharing the same App Service Plan. Maps an App Service name to its properties."
default = {}
type = map(object({
identity_ids = list(string),
disabled = bool
}))
}
variable "no_custom_hostname_binding" {
description = "Whether or not an App Service should disable hostname binding. Maps an App Service name to an override of the no_custom_hostname_binding property."
type = map(bool)
}
From there, you can use it like this:
lookup(var.no_custom_hostname_binding[local.awsvpc_map_key], null)
And declare overrides like this:
no_custom_hostname_binding = {
"vpc_key" = true
}
in expressions where you need to know that parameter. This is not super-elegant, but without optional parameters, you don't have many good alternatives.
You can follow this pattern to add as many optional overrides as you need and add more later also without breaking clients.

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

Resources