Terragrunt or Terraform define an object in variable with not known number of key/values - terraform

I have a list of objects (buckets) that I'd like to create with my module
buckets = [
{
name = "my_bucket",
location = "EU"
storage_class = "STANDARD"
force_destroy = false
versioning = true
uniform_bucket_level_access = false
public_bucket = true
labels = {
"app" = "my_app"
}
some fields are required, (like name and location) some are not and I gave them default value in resources if not provided in here; - the full module and everything is operational, but it worked without defining 'type' in variable - so it just took everything, but my question is how do I define that object in variable block ?
variable "buckets" {
#type = list(object()) #?
type = set(object({
name = string
location = string
storage_class = string
force_destroy = boolean
versioning = boolean
uniform_bucket_level_access = boolean
public_bucket = boolean
labels = object({
string = string # ?
})
}))
}
how would and object look like that has optional fields

https://github.com/hashicorp/terraform/issues/19898
https://discuss.hashicorp.com/t/map-object-with-optional-attributes/6714
It Doesn't seem possible yet apparently..

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"
}
}

Create aws_ses_receipt_rule in predefined order with for_each loop

I am trying to define SES rule sets with an order defined by a collection of rules in a variable.
I have tried the solution in https://github.com/hashicorp/terraform-provider-aws/issues/24067 to use the after property of the resource, and It does create the first rule, and fails when creating the second, and all subsequent rules, because the first rule does not exist yet (the one with after=null). I guess it needs some time to finalize. depends_on does not work with dynamic dependencies, as far as i know, so this will not make it either.
If I re-run the apply, then the second rule is created, but all the other rules fail.
my recipients_by_position map is indexed by 0-padded position (i.e. "01", "02", etc):
This is my code
locals {
recipients = {
"mail1-recipient1" = {
short_name = "mail1"
domain = "mail1.domain.com"
recipient = "recipient1"
position = 1
target_bucket = "bucket1"
}
"mail1-recipient2" = {
short_name = "mail1"
domain = "mail1.domain.com"
recipient = "recipient2"
position = 2
target_bucket = "bucket1"
}
"mail2-recipient1" = {
short_name = "mail2"
domain = "mail2.domain.com"
recipient = "recipient1"
position = 3
target_bucket = "bucket2"
}
}
spec_by_domain = {
"mail1.domain.com" = {
irrelevant ={}
}
"mail2.domain.com" = {
irrelevant ={}
}
}
recipients_by_position = {for r in local.recipients: "${format("%02s",r.position)}" => r}
}
resource "aws_ses_domain_identity" "domains" {
for_each = local.spec_by_domain
domain = each.key
}
resource "aws_ses_receipt_rule_set" "main" {
rule_set_name = "new-rules"
}
# store it in S3
resource "aws_ses_receipt_rule" "store" {
for_each = local.recipients_by_position
after = each.value.position == 1 ? null : "${format("%02s",each.value.position - 1)}"
# name = "${each.value.short_name}-store_on_s3-${each.value.recipient}"
name = each.key
rule_set_name = aws_ses_receipt_rule_set.main.rule_set_name
recipients = ["${each.value.recipient}#${each.value.domain}"]
enabled = true
scan_enabled = true
s3_action {
bucket_name = aws_s3_bucket.mailboxes[each.value.domain].bucket
object_key_prefix = each.value.recipient
position = 1
}
}
apply fails with a bunch of
Error: Error creating SES rule: RuleDoesNotExist: Rule does not exist: xx
with xx from 01 to whatever number of rules were defined

Complex object map iterable for Terraform aws_ecs_task_definition

I have the following aws_ecs_task_definition defined:
resource aws_ecs_task_definition static_task_definition {
for_each = toset({for customers in var.customers: customers.CustomerProvider => customers})
container_definitions = format(
"[%s]",
join(
",",
concat(
[replace(module.manager_container_definition.json, "%CUSTOMERPROVIDER%", each.value["CustomerProvider"]), replace(module.listgetter_container_definition.json, "%CUSTOMERPROVIDER%", each.value["CustomerProvider"])],
each.value["ReadOnlyProvider"] ? [] : [replace(module.deleter_container_definition.json, "%CUSTOMERPROVIDER%", each.value["CustomerProvider"])]
)
)
)
volume {
name = "sftp-upload-key"
host_path = "/var/microservices/${var.ecs_service_name}/${each.value["CustomerProvider"]}/sftp_uploader"
}
cpu = var.task_cpu
memory = var.task_memory
network_mode = "awsvpc"
requires_compatibilities = ["EC2"]
task_role_arn = module.ecs_task_role.role_arn
Here is what the customers var looks like:
variable customers {
type = list(object({
CustomerProvider = string
ReadOnlyProvider = bool
}))
default = [
{
CustomerProvider = "C"
ReadOnlyProvider = true
},
{
CustomerProvider = "AW"
ReadOnlyProvider = false
},
{
CustomerProvider = "AA"
ReadOnlyProvider = false
},
{
CustomerProvider = "AC"
ReadOnlyProvider = false
}
]
validation {
condition = length(var.customers) == length(distinct([for o in var.customers: o.CustomerProvider]))
error_message = "CustomerProvider entries must be unique in list."
}
}
What I am trying to achieve is a little complex - I basically want to make a task def for each CustomerProvider, but also use the boolean information in the object to determine which container definitions are included in the task. My iterable is thus each individual object and pair of attributes. The error I get is below:
Error: Invalid function argument
on .terraform/modules/service/task_definitions.tf line 181, in resource "aws_ecs_task_definition" "scaling_task_definition":
181: for_each = toset({for customers in var.customers: customers.CustomerProvider => customers})
|----------------
| var.customers is list of object with 4 elements
Invalid value for "v" parameter: cannot convert object to set of any single
type.
Any ideas on how to resolve the error while preserving my intent?

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