Create a second loop in Terraform using a condition - terraform

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

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
}

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!

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?

Merging module output map

I'm trying out the new for_each function on a module, which itself outputs some values that I need to pass into another resource.
module "vnets" {
source = "../caf-virtual-network"
for_each = var.vnet_list
ARM_ENVIRONMENT = var.ARM_ENVIRONMENT
ARM_LOCATION = var.ARM_LOCATION
ARM_SUBSCRIPTION_ID = var.ARM_SUBSCRIPTION_ID
diagnostics_map = local.diagnostics_map
location = var.ARM_LOCATION
netwatcher = local.netwatcher
networking_object = each.value
tags = var.global_settings.tags
virtual_network_rg = "${module.names.standard["resource-group"]}-${each.value.vnet.resource_group_name}"
depends_on = [
module.resource_groups_networking
]
}
I can grab the output of the module for one or more of those objects by specifying something like this
output "subnets" { value = module.vnets["vnet_shared_services_object"].vnet_subnets } , which in turn looks like this:
"vnet_shared_services_object" = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
}
Here I'm specifying the output of ONE object, but I want to dynamically specify the output of both objects in one hit.
So I want this;
"vnet_shared_services_object" = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
}
"vnet_transit_object" = {
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
...output to look like this:
subnets = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
So i know doing the following will work, but the point i'm trying to make is that i don't know how many vnet modules i'm going to produce, and thus i need to make this dynamic:
output merge{
value = merge({
for key, value in module.vnets["vnet_shared_services_object"].vnet_subnets:
key => value
},
{
for key, value in module.vnets["vnet_transit_object"].vnet_subnets:
key => value
})
}
Using the guide on Terraform to flatten (https://www.terraform.io/docs/configuration/functions/flatten.html) the output object works, but it's not how i wish for it to be:
output stuff {
value = flatten([
for key, value in module.vnets: [
for subnet, id in value.vnet_subnets: {
"${subnet}" = id
}
]
])
}
...which equats to:
stuff = [
{
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/rg-dev-uks-asdf-vnet-shared-services/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
},
{
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/rg-dev-uks-asdf-vnet-shared-services/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
},
...and so on
]
an FYI, this does not help me :(
output {
value = merge(
for key, value in module.vnets:
key => value.vnets_subnets
)
}
Any help on this would be greatly appreciated!
I'm not sure if I correctly understand the input maps, but I tried to replicate the issue creating some mock variables.
For that I created the following variables:
variable "vnets" {
default = {
"vnet_shared_services_object" = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
}
}
}
variable "vnet_subnets" {
default = {
"vnet_transit_object" = {
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
}
}
Then the output was defiend as:
output stuff {
value = {for k,v in flatten([
for key, value in merge(var.vnets, var.vnet_subnets):
[for subkey1, subval1 in value: {"${subkey1}" = subval1}]
]): keys(v)[0] => values(v)[0]}
}
which resulted in:
stuff = {
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
A colleague was able to answer this question with the following code:
locals {
subnet_list = {
for key, value in module.vnets:
key => value.vnet_subnets
}
subnet_map = merge(values(local.subnet_list)...)
}
it is the ... operator which is the key takeaway from this. you can look it up here; https://www.terraform.io/docs/configuration/expressions.html#expanding-function-arguments
... will expand a list of items to function parameters, hence you can call merge to merge a list of map

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