Looping in for_each nested resources with terraform - terraform

I am trying to provision Multiple Azure service bus topics with Multiple subscriptions. I am able to create topics but am not able to loop over subscription variables to create subscriptions.
################
locals {
servicebus = {
"topic_1" = [{
subscription = ["subscription1", "subscription2", "subscription3"]
}],
"topic_2" = [{
subscription = ["subscription4", "subscription5", "subscription6"]
}],
"topic_3" = [{
subscription = ["subscription7", "subscription8", "subscription9"]
}]
}
service_bus = flatten([
for topicname, topic in local.servicebus : [
for subname in topic : {
name = topicname
subscription_name = subname.subscription
}
]
])
}
In servicebus_subscription resource block, unable to loop the subscription name but when I provide with index(each.value.subscription_name[0]), it is creating only one subscription
############ Creating Servicebus Topic ###############################
module "servicebus_topic" {
source = "./servicebus/topic"
for_each = {
for sname in local.service_bus : sname.name => sname
}
name = each.key
resource_group_name = azurerm_resource_group.rg.name
namespace_name = module.servicebus_namespace.name
max_size_in_megabytes = "1024"
depends_on = [module.servicebus_namespace.name]
}
########## Creating Servicebus Subscription ###############################
resource "azurerm_servicebus_subscription" "sbs" {
for_each = {
for sname in local.service_bus : sname.name => sname
}
name = each.value.subscription_name
topic_name = module.servicebus_topic[each.value.name].name
namespace_name = module.servicebus_namespace.name
resource_group_name = azurerm_resource_group.rg.name
max_delivery_count = "10"
}
Error:
Error: Incorrect attribute value type
│
│ on servicebus.tf line 77, in resource "azurerm_servicebus_subscription" "sbs":
│ 77: name = each.value.subscription_name
│ ├────────────────
│ │ each.value.subscription_name is tuple with 3 elements
│
│ Inappropriate value for attribute "name": string required.
╵
╷
│ Error: Incorrect attribute value type
│
│ on servicebus.tf line 77, in resource "azurerm_servicebus_subscription" "sbs":
│ 77: name = each.value.subscription_name
│ ├────────────────
│ │ each.value.subscription_name is tuple with 3 elements
│
│ Inappropriate value for attribute "name": string required.
local variable debug output with Terraform console:
> local.service_bus
[
{
"name" = "topic_1"
"subscription_name" = [
"subscription1",
"subscription2",
"subscription3",
]
},
{
"name" = "topic_2"
"subscription_name" = [
"subscription4",
"subscription5",
"subscription16",
]
},
]

You were very close. It should be:
service_bus = merge([
for topicname, topic in local.servicebus : {
for subname in topic[0].subscription :
"${topicname}-${subname}" => {
name = topicname
subscription_name = subname
}
}
]...)
then
resource "azurerm_servicebus_subscription" "sbs" {
for_each = local.service_bus
name = each.value.subscription_name
topic_name = module.servicebus_topic[each.value.name].name
namespace_name = module.servicebus_namespace.name
resource_group_name = azurerm_resource_group.rg.name
max_delivery_count = "10"
}

I have created a complete working sample based on the code of Marcin:
locals {
servicebus = {
"topic1" = [{
subscription = ["sub1", "sub2"]
}],
"topic2" = [{
subscription = ["sub1"]
}]
}
service_bus = merge([
for topicname, topic in local.servicebus : {
for subname in topic[0].subscription :
"${topicname}-${subname}" => {
name = topicname
subscription_name = subname
}
}
]...)
}
resource "azurerm_servicebus_topic" "topic" {
depends_on = [
module.servicebus
]
for_each = local.service_bus
name = each.value.name
resource_group_name = azurerm_resource_group.shared.name
namespace_name = module.servicebus.namespace_name
enable_partitioning = false
max_size_in_megabytes = 1024
}
resource "azurerm_servicebus_subscription" "subscription" {
for_each = local.service_bus
name = each.value.subscription_name
topic_name = each.value.name
namespace_name = module.servicebus.namespace_name
resource_group_name = azurerm_resource_group.shared.name
max_delivery_count = "10"
}
The only small issue is that it wont create the topic if there is no subscription. I have some topics where this is the case.

Related

Object Merge Failure

I a trying to merge a variable definition for log analytics with some other values. Don't have the syntax correct here and when I try to use the local value (local.laws) in other areas of the code, it errors out saying "This object does not have an attribute named resource_group_name." Clearly, the merge is not doing what I want. Code below.
Variable Definition
variable "log_analytics_workspace" {
description = "A list of log analytics workspaces and their arguments."
type = list(object({
name = string
resource_group_name = string
location = string
sku = string #Free, PerNode, Premium, Standard, Standalone, Unlimited, CapacityReservation, and PerGB2018. Defaults to PerGB2018.
retention_in_days = number #Possible values are either 7 (Free Tier only) or range between 30 and 730.
daily_quota_gb = number #The workspace daily quota for ingestion in GB. Defaults to -1 (unlimited) if omitted.
reservation_capacity_in_gb_per_day = number
tags = map(string)
}))
}
INPUT
log_analytics_workspace = [
{
name = "law-eastus-dev-01"
resource_group_name = "rg-eastus-dev-01"
location = "eastus"
sku = "PerGB2018"
retention_in_days = 30
daily_quota_gb = 10
reservation_capacity_in_gb_per_day = 100
tags = {
"law" = "DEV"
}
}
]
LOCALS
###LOCALS###
locals {
laws = {
for_each = { for law in var.log_analytics_workspace : law.name => merge(
{
internet_ingestion_enabled = false
internet_query_enabled = false
cmk_for_query_forced = false
}) }
}
}
##Data Source for Resource Groups
data "azurerm_resource_group" "resource_groups" {
for_each = local.laws
name = each.value.resource_group_name
}
Error
│ Error: Unsupported attribute
│
│ on modules/loganalytics/main.tf line 17, in data "azurerm_resource_group" "resource_groups":
│ 17: name = each.value.resource_group_name
│ ├────────────────
│ │ each.value is object with 1 attribute "law-eastus-dev-01"
│
│ This object does not have an attribute named "resource_group_name".
I think you wanted the following:
locals {
laws = { for law in var.log_analytics_workspace : law.name => merge(
{
internet_ingestion_enabled = false
internet_query_enabled = false
cmk_for_query_forced = false
}, law)
}
}
and then
data "azurerm_resource_group" "resource_groups" {
for_each = local.laws
name = each.value.resource_group_name
}

Use Terraform Locals for list object {type = list(object({}))}

Please how do I simplify this configuration using locals, the code works fine as is but gets complex passing the variables manually each time.
VARIABLES:
variable "vm_all" {
type = list(object({}))
default = [
{
name = "vm1"
rg = "rg1"
sn = "sn1"
sn_prefix = ["10.0.1.0/24"]
},
{
name = "vm2"
rg = "rg2"
sn = "sn2"
sn_prefix = ["10.0.2.0/24"]
},
{
name = "vm3"
rg = "rg3"
sn = "sn3"
sn_prefix = ["10.0.3.0/24"]
}
]
}
CURRENT ITERATION USING LOCALS:(requires manually mapping the variables as shown above)
resource "example_resource" "resource1" {
for_each = {for vm_all in var.vm_all: vm_all.name => vm_all }
name = each.value.name
rg = each.value.rg
sn = each.value.sn
sn_prefix = each.value.sn_prefix
}
DESIRED METHOD OF PASSING VARIABLES:
variable "name" {
default = [
"vm1",
"vm2",
"vm3"
]
}
variable "rg_names" {
default = [
"rg1",
"rg2",
"rg3"
]
}
variable "subnets" {
default = [
"sn1",
"sn2",
"sn3"
]
}
variable "subnet_prefixes" {
default = [
"sn_prefix1",
"sn_prefix2",
"sn_prefix3"
]
}
QUESTION: How can I use locals in a more effective way to allow passing the variables as lists shown above and avoid the need to map manually?
You can combine them as follows:
locals {
vm_all = {for idx, name in var.name:
name => {
"name" = name
rg = var.rg_names[idx]
sn = var.subnets[idx]
sn_prefix = [var.subnet_prefixes[idx]]
}
}
}
then
resource "example_resource" "resource1" {
for_each = local.vm_all
name = each.value.name
rg = each.value.rg
sn = each.value.sn
sn_prefix = each.value.sn_prefix
}
Thank you #Marcin, really helpful but I'm not yet there, I feel so close though, I get the following error when I try to created nics using ids from the created subnets:
(The given key does not identify an element in this collection value.)
Error: Invalid index
│
│ on main.tf line 165, in resource "azurerm_network_interface" "nic":
│ 165: subnet_id = azurerm_subnet.subnet[each.value.sn].id
│ ├────────────────
│ │ azurerm_subnet.subnet is object with 10 attributes
│ │ each.value.sn is "subnet7"
│
│ The given key does not identify an element in this collection value.
see the main.tf below:
resource "azurerm_subnet" "subnet" {
for_each = local.vm_all
name = each.value.sn
resource_group_name = each.value.rg
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = each.value.sn_prefix
}
resource "azurerm_network_interface" "nic" {
for_each = local.vm_all
name = each.value.name
location = var.location
resource_group_name = each.value.rg
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.subnet[each.value.sn].id
private_ip_address_allocation = "Dynamic"
}
}

How to loop correctly in terraform for_each?

Objective: Loop through azure subnets via terraform.
Code That I use:
Main.tf:
resource "azurerm_network_security_group" "nsg" {
name = "nsg-vnet-hub-${var.env}-indoundDNS"
location = azurerm_resource_group.rg[0].location
resource_group_name = azurerm_resource_group.rg[0].name
tags = {
environment = "${var.env}"
costcentre = "12345"
}
}
resource "azurerm_monitor_diagnostic_setting" "nsg" {
for_each = var.subnets
name = lower("${each.key}-diag")
target_resource_id = azurerm_network_security_group.nsg[each.key].id
storage_account_id = azurerm_storage_account.storeacc.id
log_analytics_workspace_id = azurerm_log_analytics_workspace.logws.id
dynamic "log" {
for_each = var.nsg_diag_logs
content {
category = log.value
enabled = true
retention_policy {
enabled = false
}
}
}
}
My root module variable.tf :
variable "subnets" {
type = map(object({
name = string
}))
default = {
"s1" = { name = "dns_snet"},
"s2" = { name = "common_snet"},
"s3" = { name = "gw_snet"},
"s4" = { name = "data_snet"}
}
}
Problem I am facing:
Error:
network_security_group_id = azurerm_network_security_group.nsg[each.key].id
│ ├────────────────
│ │ azurerm_network_security_group.nsg is object with 7 attributes
│ │ each.key is "s3"
│
│ The given key does not identify an element in this collection value
Just updated this post, now I get error as above. I am referring to below documentation
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group
You have only a single instance of azurerm_network_security_group.nsg. Thus there is nothing to iterate over. To fix your error, it should be:
target_resource_id = azurerm_network_security_group.nsg.id

Terraform for loop in a for loop

I have the following Azure AD service principal in my terraform module.
module "test_app" {
source = "../../../../../my-adapplication/"
app_owners = ["riker#domain.com","picard#domain.com"]
app_roles = [
{
allowed_member_types = [
"User"
]
description = "Read access to Test app"
display_name = "Test App Read"
is_enabled = true
value = "TestApp.Read"
id = random_uuid.test_app_read.result
},
{
allowed_member_types = [
"User"
]
description = "Write access to Test app"
display_name = "Test App Write"
is_enabled = true
value = "TestApp.Write"
id = random_uuid.test_app_write.result
},
{
allowed_member_types = [
"User"
]
description = "Admin access to Test app"
display_name = "Test App Admin"
is_enabled = true
value = "TestApp.Admin"
id = random_uuid.test_app_admin.result
}
]
app_role_assignments = [
{
app_role_id = random_uuid.test_app_read.result #"TestApp.Read"
principal_object_id = data.azuread_group.group_role_read.object_id
},
{
app_role_id = random_uuid.test_app_write.result #"TestApp.Write"
principal_object_id = data.azuread_group.group_role_write.object_id
},
{
app_role_id = random_uuid.test_app_admin.result #"TestApp.Admin"
principal_object_id = data.azuread_group.group_role_write.object_id
}
]
}
resource "random_uuid" "test_app_read" {
}
resource "random_uuid" "test_app_write" {
}
resource "random_uuid" "test_app_admin" {
}
data "azuread_group" "group_role_read" {
display_name = "group-role-read"
}
data "azuread_group" "group2_role_read" {
display_name = "group2-role-read"
}
data "azuread_group" "group_role_write" {
display_name = "group-role-write"
}
data "azuread_group" "group_role_admin" {
display_name = "group-role-admin"
}
The my-adapplication module file looks like this:
resource "azuread_application" "app" {
...
...
dynamic "app_role" {
for_each = var.app_roles
content {
id = app_role.value["id"]
allowed_member_types = app_role.value["allowed_member_types"]
description = app_role.value["description"]
display_name = app_role.value["display_name"]
enabled = app_role.value["is_enabled"]
value = app_role.value["value"]
}
}
}
resource "azuread_service_principal" "sp" {
application_id = azuread_application.app.application_id
}
resource "azuread_app_role_assignment" "role" {
for_each = { for a in var.app_role_assignments : a.app_role_id => a }
app_role_id = each.value["app_role_id"]
principal_object_id = each.value["principal_object_id"]
resource_object_id = azuread_service_principal.sp.object_id
}
The issue I am having is related to the app_role_assignments. If I pass in only a single principal_object_id it works. However if I pass in multiple principal_object_ids it doesn't work. For example TestApp.Read below:
app_role_assignments = [
{
app_role_id = random_uuid.test_app_read.result #"TestApp.Read"
principal_object_id = [data.azuread_group.group_role_read.object_id,data.azuread_group.group2_role_read.object_id]
},
{
app_role_id = random_uuid.test_app_write.result #"TestApp.Write"
principal_object_id = data.azuread_group.group_role_write.object_id
},
{
app_role_id = random_uuid.test_app_admin.result #"TestApp.Admin"
principal_object_id = data.azuread_group.group_role_write.object_id
}
]
The error received is:
Error: Incorrect attribute value type
│
│ on .terraform/modules/test_app/main.tf line 116, in resource "azuread_app_role_assignment" "role":
│ 116: principal_object_id = each.value["principal_object_id"]
│ ├────────────────
│ │ each.value["principal_object_id"] is tuple with 2 elements
│
│ Inappropriate value for attribute "principal_object_id": string required.
╵
How do I get terraform to loop over this principal_object_id list? I guess I am after a loop inside a loop. Is there a better way of doing this than the way I am above?
Is it possible to do this using for_each so I don't have the problems with list order changing if i use count/for.
Many thanks in advance.
You have to re-organize your app_role_assignments and then flatten it. If you want principal_object_id to have more then one value, it should always be a list, even for a single element:
app_role_assignments = [
{
app_role_id = random_uuid.test_app_read.result #"TestApp.Read"
principal_object_id = [data.azuread_group.group_role_read.object_id,data.azuread_group.group2_role_read.object_id]
},
{
app_role_id = random_uuid.test_app_write.result #"TestApp.Write"
principal_object_id = [data.azuread_group.group_role_write.object_id]
},
{
app_role_id = random_uuid.test_app_admin.result #"TestApp.Admin"
principal_object_id = [data.azuread_group.group_role_write.object_id]
}
]
then you can flatten is as:
locals {
app_role_assignments_flat = merge([
for val in var.app_role_assignments: {
for principal_object_id in val["principal_object_id"]:
"${val.app_role_id}-${principal_object_id}" => {
app_role_id = val.app_role_id
principal_object_id = principal_object_id
}
}
]...) # please do NOT remove the dots
}
then
resource "azuread_app_role_assignment" "role" {
for_each = local.app_role_assignments_flat
app_role_id = each.value["app_role_id"]
principal_object_id = each.value["principal_object_id"]
resource_object_id = azuread_service_principal.sp.object_id
}

Terraform error while fetching value of subnet in oci

I have this code where I am trying to fetch the id of subnet via subnet name:
The code looks like this :
resource "oci_containerengine_node_pool" "node_pool" {
for_each = var.nodepools
cluster_id = oci_containerengine_cluster.cluster[0].id
compartment_id = var.compartment_id
depends_on = [oci_containerengine_cluster.cluster]
kubernetes_version = var.cluster_kubernetes_version
name = each.value["name"]
node_config_details {
placement_configs {
availability_domain = var.availability_domain
subnet_id = oci_core_subnet.snet-workers[each.value.subnet_name].id
}
size = each.value["size"]
}
node_shape = each.value["node_shape"]
node_shape_config {
#Optional
memory_in_gbs = each.value["memory"]
ocpus = each.value["ocpus"]
}
node_source_details {
image_id = each.value["image_id"]
source_type = "IMAGE"
}
ssh_public_key = file(var.ssh_public_key_path)
}
My subnet code looks like:
resource "oci_core_subnet" "snet-workers" {
cidr_block = lookup(var.subnets["snet-workers"], "subnet_cidr")
compartment_id = var.compartment_id
vcn_id = oci_core_virtual_network.base_vcn.id
display_name = lookup(var.subnets["snet-workers"], "display_name")
dns_label = lookup(var.subnets["snet-workers"], "dns_label")
prohibit_public_ip_on_vnic = true
security_list_ids = [oci_core_security_list.private_worker_nodes.id]
route_table_id = oci_core_route_table.rt-nat.id
}
variables looks like:
variable "subnets" {
description = "List of subnets to create for an environment"
type = map(object({
subnet_cidr = string
display_name = string
dns_label = string
}))
}
my tfvars looks like
nodepools = {
np1 = {
name = "np1"
size = 3
ocpus = 8
memory = 120
image_id = "test"
node_shape = "VM.Standard2.8"
subnet_name = "snet-worker1"
}
np2 = {
name = "np2"
size = 2
ocpus = 8
memory = 120
image_id = "test"
node_shape = "VM.Standard2.8"
subnet_name = "snet-worker2"
}
}
But in terraform plan I am getting error as
Error: Invalid index
│
│ on ../modules/oke/oke.tf line 39, in resource "oci_containerengine_node_pool" "node_pool":
│ 39: subnet_id = oci_core_subnet.snet-workers[each.value.subnet_name].id
│ ├────────────────
│ │ each.value.subnet_name is "snet-worker2"
│ │ oci_core_subnet.snet-workers is object with 22 attributes
│
│ The given key does not identify an element in this collection value.
╵
╷
│ Error: Invalid index
│
│ on ../modules/oke/oke.tf line 39, in resource "oci_containerengine_node_pool" "node_pool":
│ 39: subnet_id = oci_core_subnet.snet-workers[each.value.subnet_name].id
│ ├────────────────
│ │ each.value.subnet_name is "snet-worker1"
│ │ oci_core_subnet.snet-workers is object with 22 attributes
│
│ The given key does not identify an element in this collection value.
Can someone help
The following:
subnet_id = oci_core_subnet.snet-workers[each.value.subnet_name].id
would only work if you had used for_each while creating oci_core_subnet.snet-workers. Since you are not using for_each, it should be:
subnet_id = oci_core_subnet.snet-workers.id
UPDATE
To keep using original version:
resource "oci_core_subnet" "snet-workers" {
for_each = var.subnets
cidr_block = each.value["subnet_cidr"]
compartment_id = var.compartment_id
vcn_id = oci_core_virtual_network.base_vcn.id
display_name = leach.value[""display_name"]
dns_label = each.value["dns_label"]
prohibit_public_ip_on_vnic = true
security_list_ids = [oci_core_security_list.private_worker_nodes.id]
route_table_id = oci_core_route_table.rt-nat.id
}
You have to define a "oci_core_subnet" for each entry in "subnets" variable. Something like this:
resource "oci_core_subnet" "snet-workers" {
for_each = var.subnets
cidr_block = each.value.subnet_cidr
...
}

Resources