Terraform - override a single value in a map - terraform

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.

Related

How to avoid code duplication for input variable declaration/instantiation in Terraform?

I want to create a variable 'blueprint' and instantiate it several times. How do I achieve this?
Example :
my_project
|--main.tf
|--variables.tf
|--variables.tfvars
In the main.tf I declare resources that use variables,
In variables.tf module I want this sample variable
variable "project_info" {
type = object({
name = string
id = string
})
}
In variables.tfvars I want this (Kind of)
project_1_info = {
name = "prj-1"
id = "prj-1-192388"
}
project_2_info = {
name = "prj-2"
id = "prj-2-120917"
}
At the moment, what I declare in the variables.tf module is the following :
variable "project_1_info" {
type = object({
name = string
id = string
})
}
variable "project_2_info" {
type = object({
name = string
id = string
})
}
Which I find quite repetitive by duplicating declarations, makes maintenance and readability worse.
Any suggestions? Can this even be achieved with HCL?
Usually you would use map for that:
variable "project" {
type = map(object({
name = string
id = string
}))
}
with a value:
project = {
project1name = {
name = "prj-1"
id = "prj-1-192388"
},
project2name = {
name = "prj-2"
id = "prj-2-192388"
},
project3name = {
name = "prj-3"
id = "prj-3-192388"
}
}

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
}

dynamic block in module parameters

I'm trying to create terraform that calls a module and I need to be able to include a dynamic block in the parameters of the module call
this is the sort of thing i'm trying to do
main.tf
module "eks" {
source = "../../modules/eks"
node_groups = [
{
name = "gp1"
gp_instance_count = 4
},
{
name = "gp2"
gp_instance_count = 2
}
]
}
variables.tf
variable "node_groups" {
type = list(object({
name = string
gp_instance_count = number
}))
}
eks.tf
module "eks" {
source = "terraform-aws-modules/eks/aws"
dynamic self_managed_node_groups {
for_each = var.node_groups
content {
self_managed_node_groups.value["name"] = {
capacity_rebalance = true
use_mixed_instances_policy = true
desired_size = self_managed_node_groups.value[".gp_instance_count"]
}
}
}
What I'm hoping for here is to iterate around var.node_groups and create a "self_managed_node_groups" section.
This would pass the following to the module
gp1 = {
capacity_rebalance = true
use_mixed_instances_policy = true
desired_size = 4
} ,
gp2 = {
capacity_rebalance = true
use_mixed_instances_policy = true
desired_size = 2
}
I'm getting the error
87: self_managed_node_groups.value["name"] = {
An argument or block definition is required here. To set an argument, use the
equals sign "=" to introduce the argument value.```
if I hardcode the self_managed_node_groups.value["name"] value then I get the error
Blocks of type "dynamic" are not expected here.
It feels like what I'm trying to do is quite straightforward and i'm just missing something simple.
I'd appreciate any help at all on this!

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