How to avoid code duplication for input variable declaration/instantiation in Terraform? - 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"
}
}

Related

Create a second loop in Terraform using a condition

I'm a true beginner with Terraform, and here is my problem:
I need to create multiple objects using the same resource of this type:
resource "jamf_smartComputerGroup" "test_smart_1" {
name = "Test Smart 1"
criteria {
priority = 0
name = "UDID"
search_type = "is"
search_value = "FAKE-UDID-THAT-ALSO-DOES-NOT-EXIST"
}
criteria {
priority = 1
name = "UDID"
search_type = "is not"
search_value = "FAKE-UDID-THAT-DOES-NOT-EXIST-LIKE-REALLY"
}
}
IMPORTANT: this resource can have zero or more criterias!
I have created the variables.tf and terraform.vartf files as follow:
variables.tf
variable "jamf_smartComputerGroup_list" {
type = list(object({
SMCG_NAME = string
SMCG_CRITERIA = list(object({
SMCG_CRITERIA_PRIORITY = number
SMCG_CRITERIA_NAME = string
SMCG_CRITERIA_TYPE = string
SMCG_CRITERIA_VALUE = string
}))
}))
}
terraform.vartf
jamf_smartComputerGroup_list = [
{
SMCG_NAME = "smcg_1"
SMCG_CRITERIA = [] # THIS OBJECT HAS ZERO CRITERIA
},
{
SMCG_NAME = "smcg_2"
SMCG_CRITERIA = [ # THIS OBJECT HAS ONE CRITERIA
{
SMCG_CRITERIA_PRIORITY = 0
SMCG_CRITERIA_NAME = "crit"
SMCG_CRITERIA_TYPE = "is not"
SMCG_CRITERIA_VALUE = "false"
}
]
},
{
SMCG_NAME = "smcg_3"
SMCG_CRITERIA = [ # THIS OBJECT HAS TWO CRITERIAS
{
SMCG_CRITERIA_PRIORITY = 0
SMCG_CRITERIA_NAME = "crit 1"
SMCG_CRITERIA_TYPE = "contains"
SMCG_CRITERIA_VALUE = "foo"
},
{
SMCG_CRITERIA_PRIORITY = 1
SMCG_CRITERIA_NAME = "crit 2"
SMCG_CRITERIA_TYPE = "exact match"
SMCG_CRITERIA_VALUE = "bar"
}
]
}
]
In the main.tf file I was able to loop through the objects, without criterias, using this:
resource "jamf_smartComputerGroup" "default" {
for_each = { for idx, val in var.jamf_smartComputerGroup_list : idx => val }
name = each.value.SMCG_NAME
}
But and I can't find the appropriate way to determine if one or more criterias are present; and if there is one more criterias, how to loop through them.
A far as I understand, I can't use two for_each verbs at the same time, and I can't use count with for_each.
Any examples will be appreciated :-) !
Regards,
Emmanuel Canault
You have to use dynamic blocks:
resource "jamf_smartComputerGroup" "test_smart_1" {
for_each = { for idx, val in var.jamf_smartComputerGroup_list : idx => val }
name = each.value.SMCG_NAME
dynamic "criteria" {
` for_each = each.value.SMCG_CRITERIA
content {
priority = criteria.SMCG_CRITERIA_PRIORITY
name = criteria.SMCG_CRITERIA_NAME
search_type = criteria.SMCG_CRITERIA_TYPE
search_value = criteria.SMCG_CRITERIA_VALUE
}
}
}
Thanks #Marcin!
It works with small adaptation : criteria.value.SMCG_... instead of criteria.SMCG_...
Regards,
Emmanuel

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!

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