How do I do variable definitions in locals? - terraform

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.

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 }

Terraform variable definition

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.

Terraform - override a single value in a map

I would like to know if it is possible to merge two map of maps without replacing the main map object.
My map object is defined as follows:
variable "apps" {
type = map(object({
is_enabled = bool
cost_center = string
}))
default = {}
}
locals {
default_apps = {
"api-1" = {
is_enabled = false
cost_center = "1234"
},
"api-2" = {
is_enabled = false
cost_center = "1235"
},
}
apps = merge(
local.default_apps,
var.apps
)
}
If define my tfars as follows, to override the value of api-1['s_enabled']
apps = {
"api-1" = {
is_enabled = true
}
}
I get the following error:
Error: Invalid value for input variable
The environment variable TF_VAR_apps does not contain a valid value for
variable "apps": element "api-1": attribute "cost_center" is required.
It works if I define my tfvars like so:
apps = {
"api-1" = {
is_enabled = true
cost_center = "1234"
}
}
My goal is to override a single value of one of the pre defined local variables under default_apps (e.x is_enabled) in tfvars.
Edit: requirements
The error is not about your merge but about your tfars. The following variable is invalid in your case:
apps = {
"api-1" = {
is_enabled = true
}
}
as you explicitly defined it as:
type = map(object({
is_enabled = bool
cost_center = string
}))
Your apps is missing cost_center which is required. If you use object type, everything that you specify in type definition must be provided:
Values that match the object type must contain all of the specified keys, and the value for each key must match its specified 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