Terraform variable definition - terraform

I would like to create a deployments input and drive my deployments based on that var.
Here's an example deployments input,
deployments = {
dev-1 = {
dev-api-us = {
hosts = ["dev-api-us.lm.com", "dev-api-us.lm1.com"]
}
region = "us-east1"
}
dev-2 = {
dev-api-uk = {
hosts = ["dev-api-uk.lm.com", "dev-api-uk.lm1.com"]
}
region = "europe-west2"
}
}
Is that a valid input if so what will the corresponding variable definition look like?
I tried this, but it says the syntax isn't right
variable "deployments" {
description = "A map of deployment"
type = map(map(object({
hosts = list(string)
})
region = string
))
}
Any help is appreciated.

This data structure is a bit confusing because the second level seems to be a mixture of predefined attributes (region) and arbitrary map keys (like dev-api-us and dev-api-uk).
I think the best way to proceed here would be to change this structure so that the second level of map is separate from the object it's embedded in. You didn't mention a noun in your question to refer to dev-api-us and dev-api-uk are examples of, so I'm going to just call them "host prefixes" for the sake of having something to call them:
{
dev-1 = {
host_prefixes = {
dev-api-us = {
hosts = ["dev-api-us.lm.com", "dev-api-us.lm1.com"]
}
}
region = "us-east1"
}
dev-2 = {
host_prefixes = {
dev-api-uk = {
hosts = ["dev-api-uk.lm.com", "dev-api-uk.lm1.com"]
}
}
region = "europe-west2"
}
}
A suitable type constraint for this adjusted data structure could be:
variable "deployments" {
type = map(object({
host_prefixes = map(object({
hosts = set(string)
}))
region = string
}))
}
This altered structure works within Terraform's type system because it separates the object-typed parts from the map-typed parts. The Terraform language doesn't have any type constraint that allows both map and object behaviors to be combined into a single value.

Related

How can I split out an 'any' variable in terraform?

I'm trying to get multiple values out of an 'any' type variable. I'm new to terraform and open to recommendations. Specifically for this example, I'd like to know how I can output the 'bucket_name' value in my outputs.
variable "replica_config" {
type = any
default = {
role = "role_name"
rules = [
{
id = "full-s3-replication"
status = true
priority = 10
delete_marker_replication = false
destination = {
bucket = "bucket_name"
storage_class = "STANDARD"
replica_kms_key_id = "key_id"
account_id = "account_id"
replication_time = {
status = "Enabled"
minutes = 15
}
}
}
]
}
}
Current Output:
output "output4" {
value = flatten(var.replica_config["rules"])
}
Since you you have a list for rules, you can use a splat expression as such:
output "output4" {
value = var.replica_config.rules[*].destination.bucket
}
Keep in mind, the output of this expression will also be a list. If you want a single item instead of a list, you can use an index.
For example:
output "output4" {
value = var.replica_config.rules[0].destination.bucket
}

For_each loop with for expression based on value in map

Since the title is not descriptive enough let me introduce my problem.
I'm creating DRY module code for CDN that contains profile/endpoint/custom_domain.
Variable cdn_config would hold all necessary/optional parameters and these are created based on the for_each loop.
Variable looks like this:
variable "cdn_config" {
profiles = {
"profile_1" = {}
}
endpoints = {
"endpoint_1" = {
custom_domain = {
}
}
}
}
Core of this module is working - in the means that it would create cdn_profile "profile_1" then cdn_endpoint "endpoint_1" will be created and assigned to this profile then cdn_custom_domain will be created and assigned to "endpoint_1" since it's the part of "endpoint_1" map.
Then I realize, what in case I want to create "cdn_custom_domain" only and specify resource ID manually?
I was thinking that adding the optional parameter "standalone" could help, so it would look like this:
variable "cdn_config" {
profiles = {
"profile_1" = {}
}
endpoints = {
"endpoint_1" = {
custom_domain = {
}
}
"endpoint_standalone" = {
custom_domain = {
standalone = true
cdn_endpoint_id = "xxxxx"
}
}
}
}
Having this "standalone" parameter eq true "endpoint_standalone" map should be totally ignored from looping in the azurerm_cdn_endpoint resource creation.
So far this direction is my only guess, clearly, it's not working - if I add "endpoint_standalone" it complains that not all required parameters are specified so it's surely finding it.
resource "azurerm_cdn_endpoint" "this" {
for_each = {for k in keys(var.cdn_config.endpoints) : k => var.cdn_config.endpoints[k] if lookup(var.cdn_config.endpoints[k],"standalone",null) != "true"}
I would be grateful if you have a solution for this problem.
You are comparing a bool type to a string type, so the logical comparison will always return false:
for_each = {for k in keys(var.cdn_config.endpoints) : k => var.cdn_config.endpoints[k] if lookup(var.cdn_config.endpoints[k],"standalone",null) != true }
While we are here, we can also improve this for expression:
for_each = { for endpoint, params in var.cdn_config.endpoints : endpoint => params if lookup(params.custom_domain, "standalone", null) != true }

Produce single item maps procedurally from nested map in terraform

I have a nested map variable of account name and ID by OU, like:
variable "aws_accounts" {
type = map(map(any))
default = {
first_ou = {
first_account = "111111111"
second_account = "222222222"
}
second_ou = {
third_account = "333333333"
fourth_account = "444444444"
}
}
}
This is great for passing a map of account_name to account_id as a sub-variable to do things by ou and the modules in question are constructed to accept a map input.
I would like to also render a local so that I can also reference single accounts but get a map value for them without having to maintain a separate list of variables, like
local.first_account = {
first_account = "111111111"
}
local.second_account = {
second_account = "222222222"
}
local.third_account = {
third_account = "33333333"
}
etc.
I have tried various techniques but without success:
I cannot work out how to refer to each map in the array iteratively- most documentation seems to be based on lists and when I try to do a for_each I get
The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.
Based on your example, it seems like you want to take your two-level map and turn it into a single-level map where the keys are the account names and the "OU names" are just discarded.
Here's one way to achieve that:
locals {
account_ids = merge(values(var.aws_accounts)...)
}
This first uses values to take the values from the top-level map, producing a list of maps.
It then uses merge to take all of the elements from each of the maps and combine them into a single new map. I used the ... symbol to tell Terraform that it should treat each element of the list as a separate argument to merge, rather than just passing the whole list as a single argument.
After merging these together you could potentially split them apart again, creating a separate map each, using a for expression.
locals {
account_maps = tomap({
for k, id in local.account_ids :
k => { (k) = id }
})
}
Maybe not exactly what you're looking for, but could be helpful:
locals {
accounts = merge(var.aws_accounts["first_ou"], var.aws_accounts["second_ou"])
}
If you need to do this in a more dynamic way:
locals {
accounts = zipmap(
flatten([for item in var.aws_accounts : keys(item)]),
flatten([for item in var.aws_accounts : values(item)])
)
}
Now you can access each account with local.accounts["first_account"] etc.
OK So with help from Martin Atkins and Bryan Heden I have found an answer to this. It isn't exactly pretty but it does work:
variable "aws_accounts" {
type = map(map(any))
default = {
first_ou = {
first_account = "111111111"
second_account = "222222222"
}
second_ou = {
third_account = "333333333"
fourth_account = "444444444"
}
}
}
locals {
# gives single map from nested map
account_ids = merge(values(var.aws_accounts)...)
# gives separate structured map for each key
single_accounts_maps = {
for account, id in local.account_ids :
account => {
account = account
id = id
}
}
# gives map where values = keys plus values
single_accounts_maps_joined = zipmap(
flatten([for item in var.aws_accounts : keys(item)]),
[for item in local.single_accounts_maps :
join(" = ", values(item))]
)
# gives nested map by key = {key = "value"}
single_accounts_maps_keys_values = {
for item in local.single_accounts_maps_joined :
(split(" = ", item)[0]) => {
(split(" = ", item)[0]) = (split(" = ", item)[1])
}
}
}
Output that I wanted:
terraform console
> local.single_accounts_maps_keys_values
{
"first_account" = {
"first_account" = "111111111"
}
"fourth_account" = {
"fourth_account" = "444444444"
}
"second_account" = {
"second_account" = "222222222"
}
"third_account" = {
"third_account" = "333333333"
}
}
After the discussion with Martin Atkins and his subsequent edits below, I am recommending his answer instead as simpler, more legible and more graceful, although the tomap() nesting appears to be unneeded, i.e. do
locals {
account_ids = merge(values(var.aws_accounts)...)
account_maps = {
for k, id in local.account_ids :
k => { (k) = id }
}
}

How do I do variable definitions in locals?

So I have this variable that I want to interpolate like this:
variable "secondary_ranges" {
type = map(object({
secondary_range = object({
range_name = string
ip_cidr_range = string
})
}))
default = {
pods = {
secondary_range = {
range_name = "pods"
ip_cidr_range = "${var.some_other_var_or_datasource}"
}
}
services = {
secondary_range = {
range_name = "services"
ip_cidr_range = "${var.some_other_var_or_datasource}"
}
}
}
}
I can do this in locals but I loose my custom type definition
locals {
default = {
pods = {
secondary_range = {
range_name = "pods"
ip_cidr_range = "${var.some_other_var_or_datasource}"
}
}
services = {
secondary_range = {
range_name = "services"
ip_cidr_range = "${var.some_other_var_or_datasource}"
}
}
}
}
Is there a way to apply a custom type in locals?
You can make the local value end up having a type that conforms to that variable type constraint by including some type conversion functions (tomap and tostring):
locals {
default = tomap({
pods = {
secondary_range = {
range_name = "pods"
ip_cidr_range = tostring(var.some_other_var_or_datasource)
}
}
services = {
secondary_range = {
range_name = "services"
ip_cidr_range = tostring(var.some_other_var_or_datasource)
}
}
})
}
The { ... } syntax creates a value of an object type, but we can pass such an object to tomap to have Terraform convert it to an equivalent map type. It'll do that by checking that all of the object attribute values have the same type and selecting that type as the map's element type, and so the resulting value will have the same type as long as the objects inside have the right structures.
In the example above I helped ensure that the result would have the right type by using tostring with each of the two variable references. That wouldn't actually be necessary if that variable were defined as type = string because Terraform would convert it to string automatically anyway, but it doesn't hurt to be explicit if you care about producing a specific result type.
The ability to set a type constraint is there to help a caller of your module know how to call it, so there isn't a direct equivalent of it for values defined within the module itself, but if you use type conversion functions like in the above you can get the same result a different way.
Personally I don't typically worry too much about the types of local values, because they are used only within the current module anyway, but I will be more explicit about type conversions in output values so that the caller can get a better signal about how the value is intended to be used and so that future changes are less likely to inadvertently cause an output value to change to a different type.

Terraform: Populate an object with reasonable, then partially override?

I am looking for a way to have a variable of type "object" (to force a certain structure), and then override certain parts. consider this variable declaration:
variable "prod_vpc" {
type = object({
name = string,
region = string,
single_nat_gw = bool,
create = bool,
supernet = string
})
default = {
name = "PROD"
region = "eu-central-1"
single_nat_gw = true
create = false,
supernet = "0.0.0.0/0"
}
}
in this case, i might want to have this in my auto.tfvars:
prod_vpc = {
create = true,
supernet = "10.0.8.0/24"
}
because all other values are good as they are. the only way i can think of is to do this is with type = map(string) and conditionals in a locals block, i.e.:
variable "vpc_defaults" {
type = object({
name = string,
region = string,
single_nat_gw = bool,
create = bool,
supernet = string
})
default = {
name = "PROD"
region = "eu-central-1"
single_nat_gw = true
create = false,
supernet = "0.0.0.0/0"
}
}
variable "prod_vpc" {
type = map(string)
default = {}
}
then
prod_vpc = {
create = true,
supernet = "10.0.8.0/24"
}
and then:
locals {
create = coalesce(var.prod_vpc["create"], vpc_defaults.create)
}
(i haven't tried this, but i think something along these lines should work)
any other ideas for pre-defining defaults and only overriding when needed?
regards
ruben
Even if you had used a plain map rather than object type, it would not have been possible to do this in this way, and the reasoning is set out in the docs here. It is suggested there that you should use locals instead to provide the defaults and then explicitly merge them.
Meanwhile, as noted by James Bardin in this related GitHub issue, the object keys are required in order to set a default. If you want those to be null, then you have to explicitly set them as such.
James added:
You also have the option of setting the default as null (or not having a default, and requiring the caller set a value). In that case however, you need to avoid passing a null object into merge, which you can do using a condition or coalesce:
merged_map_keys = merge(local.default_map_keys, coalesce(var.prod_vpc, {}))
It would be nice if you could do something like this:
variable "prod_vpc" {
type = object({
name = string,
region = string,
single_nat_gw = bool,
create = bool,
supernet = string
})
default = {}
}
locals {
default_map_keys = {
name = "PROD"
region = "eu-central-1"
single_nat_gw = true
create = false,
supernet = "0.0.0.0/0"
}
merged_map_keys = merge(local.default_map_keys, var.prod_vpc)
}
Then you could call it with:
prod_vpc = {
create = true,
supernet = "10.0.8.0/24"
}
There is more info in the open GitHub issue.
Since Terraform v1.3, you can use Optional Object Type Attributes!
So in your case, you could do something like this:
variable "prod_vpc" {
type = object({
name = optional(string, "PROD"),
region = optional(string, "eu-central-1"),
single_nat_gw = optional(bool, true),
create = optional(bool, false),
supernet = optional(string, "0.0.0.0/0")
})
}
Note that you should/can remove the default {} section!

Resources