Complex object map iterable for Terraform aws_ecs_task_definition - terraform

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?

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

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 to reference instance argument value created with for_each meta-argument in another instance in the same map

Updated with a more illustrative example.
My end goal is to have Terraform create instances of a resource generated with the for_each meta argument in a specific sequence. HCL is known to be a declarative language and when Terraform applies a configuration it can create resources randomly unless you use the depends_on argument or refer from one resource (instance) to another. However, the depends_on argument does not take values that are "calculated", so I don't know how to use it in modules.
For this reason, in order to force Terraform to create instances of a resource in a specific sequence, I decided to try to make the value of a certain argument in an instance it creates "calculated" based on the values of the same argument from another instance.
Below you can find a more practical example based on using one of the providers, but the question is more general and pertains to Terraform as such.
Let's take a test module that instantiates the cloudflare_page_rule resource:
# Module is placed to module\main.tf
terraform {
experiments = [module_variable_optional_attrs]
}
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 3.10.0"
}
}
}
variable "zone" {
type = string
description = "The DNS zone name which will be added, e.g. example.com."
}
variable "page_rules" {
type = list(object({
page_rule_name = string
target = string
actions = object({
forwarding_url = optional(object({
url = string
status_code = number
}))
})
priority = optional(number)
status = optional(string)
depends_on = optional(string)
}))
description = "Zone's page rules."
default = []
}
//noinspection HILUnresolvedReference
locals {
page_rule_dependencies = { for p in var.page_rules : p.page_rule_name => p.depends_on if p.depends_on != null }
}
# https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/zone
resource "cloudflare_zone" "this" {
zone = var.zone
}
# https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/page_rule
//noinspection HILUnresolvedReference
resource "cloudflare_page_rule" "this" {
for_each = var.page_rules != null ? { for p in var.page_rules : p.page_rule_name => p } : {}
zone_id = cloudflare_zone.this.id
target = each.value.target
actions {
//noinspection HILUnresolvedReference
forwarding_url {
status_code = each.value.actions.forwarding_url.status_code
url = each.value.actions.forwarding_url.url
}
}
priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority
status = each.value.status
}
output "page_rule_dependencies" {
value = local.page_rule_dependencies
}
And a configuration that is used to create resources:
terraform {
required_version = ">= 0.15.0"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 3.10.1"
}
}
}
variable "cloudflare_api_token" {
type = string
sensitive = true
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
module "acme_com" {
source = "./module"
zone = "acme.com"
page_rules = [
{
page_rule_name = "page_rule_1"
target = "acme.com/url1"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url1"
}
}
priority = 1
},
{
page_rule_name = "page_rule_2"
target = "acme.com/url2"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url2"
}
}
priority = 2
depends_on = "page_rule_1"
},
{
page_rule_name = "page_rule_3"
target = "acme.com/url3"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url3"
}
}
priority = 3
depends_on = "page_rule_2"
}
]
}
output "page_rule_dependencies" {
value = module.acme_com.page_rule_dependencies
}
In this particular example, I've added the depends_on argument to the page_rules variable (don't confuse this argument with the depends_on meta argument). For the value of the depends_on argument, I specified the name of a page_fule on which another page_fule depends.
Next, I created a local variable page_rule_dependencies, the value of which, after calculations, is the following (you can check this yourself by replacing the priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority construct with priority = each.value.priority and executing terraform apply):
page_rule_dependencies = {
"page_rule_2" = "page_rule_1"
"page_rule_3" = "page_rule_2"
}
Further, in the priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority construct, I refer to the values ​​of the local variable, thereby forming a "reference" to the page_fule instance, on which the current instance depends:
When creating page_rule_1, the value of its argument priority = 1.
When creating page_rule_2, the value of its argument priority = cloudflare_page_rule.this["page_rule_1"].priority + 1.
When creating page_rule_3, the value of its argument priority = cloudflare_page_rule.this["page_rule_2"].priority + 1.
However, I get an Error: Cycle: module.acme_com.cloudflare_page_rule.this["page_rule_3"], module.acme_com.cloudflare_page_rule.this["page_rule_2"], module.acme_com.cloudflare_page_rule.this["page_rule_1"] error.
Either I'm doing something wrong, or it's some kind of Terraform limitation/bug. Is there a way to get rid of this error?
P.S. Resulting graph after terraform graph -draw-cycles | dot -Tsvg > graph.svg or terraform graph -draw-cycles -type=plan | dot -Tsvg > graph-plan.svg (the same result):
P.P.S. I use Terraform v1.1.7.

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.

Optional list element processing in Terraform

I am trying to add redrive policies to existing queues.
I have managed to define a list like this:
variable "sqsq_primary" {
type = "list"
default = [
{
name = "PrimaryQueue1"
maxReceiveCount = -1
deadLetterQueue = ""
},
{
name = "PrimaryQueue2"
maxReceiveCount = 5
deadLetterQueue = "PrimaryQueue2_DL"
},
{
name = "PrimaryQueue3"
maxReceiveCount = 20
deadLetterQueue = "PrimaryQueue3_DL"
}
]
}
I have defined a list of DL queues like this:
variable "sqsq_primary_dl" {
type = "list"
default = [
"PrimaryQueue2_DL",
"PrimaryQueue3_DL"
]
}
In my module I define resources like this:
resource "aws_sqs_queue" "q" {
count = "${length(var.sqsq_primary)}"
name = "${lookup(var.sqsq_primary[count.index], "name")}-${var.environment}"
## Conditionally Sets A Redrive Policy ##
redrive_policy = "${lookup(var.sqsq_primary[count.index], "deadLetterQueue") != "" ? "{\"deadLetterTargetArn\":\"arn:aws:sqs:${var.region}:${var.acc_number}:${lookup(var.sqsq_primary[count.index], "deadLetterQueue")}-${var.environment}\",\"maxReceiveCount\":${lookup(var.sqsq_primary[count.index], "maxReceiveCount")}}" : ""}"
depends_on = ["aws_sqs_queue.qdl"]
}
resource "aws_sqs_queue" "qdl" {
count = "${length(var.sqsq_primary_dl)}"
name = "${element(var.sqsq_primary_dl, count.index)}-${var.environment}"
}
This works. However, I don't like the duplicated information which is the names of the DL queues.
So the question is, how could I get rid of the second list? How could I iterate in the second resource over the first list instead and only create a DL queue if deadLetterQueue != "" ?
Thanks for your help!
I think you may have encountered a limitation of terraform interpolation. Unless you deconstruct your list of maps to separate maps, the best is probably below.
If you keep your definitions for queues with no dl at the bottom and use a static value for minus maths on the dl resource count, the plan stays the same as before.
As a side note, it's dead letter not dead leater.
variable "sqsq_primary" {
type = "list"
default = [
{
name = "PrimaryQueue2"
maxReceiveCount = 5
deadLeaterQueue = "PrimaryQueue2_DL"
},
{
name = "PrimaryQueue3"
maxReceiveCount = 20
deadLeaterQueue = "PrimaryQueue3_DL"
},
{
name = "PrimaryQueue1"
maxReceiveCount = -1
deadLeaterQueue = ""
}
]
}
resource "aws_sqs_queue" "q" {
count = "${length(var.sqsq_primary)}"
name = "${lookup(var.sqsq_primary[count.index], "name")}-${var.environment}"
## Conditionally Sets A Redrive Policy ##
redrive_policy = "${lookup(var.sqsq_primary[count.index], "deadLeaterQueue") != "" ? "{\"deadLetterTargetArn\":\"arn:aws:sqs:${var.region}:${var.acc_number}:${lookup(var.sqsq_primary[count.index], "deadLeaterQueue")}-${var.environment}\",\"maxReceiveCount\":${lookup(var.sqsq_primary[count.index], "maxReceiveCount")}}" : ""}"
depends_on = ["aws_sqs_queue.qdl"]
}
resource "aws_sqs_queue" "qdl" {
count = "${length(var.sqsq_primary) - 1}"
name = "${lookup(var.sqsq_primary[count.index], "deadLeaterQueue")-var.environment}"
}
My colleague has come up with a solution that seems slightly more flexible than the one provided by #henry-dobson.
We have also refactored it so now it doesn't require the deadLeaterQueue value - we conform to a naming standard now, so the resulting names of the DL queues are different from the ones in the question.
variable "sqsq_primary" {
type = "list"
default = [
{
name = "PrimaryQueue1"
maxReceiveCount = 0
},
{
name = "PrimaryQueue2"
maxReceiveCount = 5
},
{
name = "PrimaryQueue3"
maxReceiveCount = 20
}
]
}
data "empty_data_source" "deadletterq" {
count = "${length(var.sqsq_primary)}"
inputs = {
dl = "${lookup(var.sqsq_primary[count.index], "maxReceiveCount", "") > 0 ? "${replace(lookup(var.sqsq_primary[count.index], "name"),"Queue","DeadLetterQueue")}" : ""}"
}
}
resource "aws_sqs_queue" "q" {
count = "${length(var.sqsq_primary)}"
name = "${lookup(var.sqsq_primary[count.index], "name")}-${var.environment}"
## Conditionally Sets A Redrive Policy ##
redrive_policy = "${lookup(var.sqsq_primary[count.index], "maxReceiveCount") > 0 ? "{\"deadLetterTargetArn\":\"arn:aws:sqs:${var.region}:${var.acc_number}:${replace(lookup(var.sqsq_primary[count.index], "name"),"Queue","DeadLetterQueue")}-${var.environment}\",\"maxReceiveCount\":${lookup(var.sqsq_primary[count.index], "maxReceiveCount")}}" : ""}"
depends_on = ["aws_sqs_queue.qdl"]
}
resource "aws_sqs_queue" "qdl" {
count = "${length(compact(data.empty_data_source.deadletterq.*.outputs.dl))}"
name = "${element(compact(data.empty_data_source.deadletterq.*.outputs.dl), count.index)}-${var.environment}"
}

Resources