Terraform for loop in a for loop - azure

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
}

Related

How to merge/combine two variables in to one variable in terraform?

I've been trying to merge/combine two variables into a single one (different one as : var1+var2 = merged into var3)
I am trying to create a for_each loop on the code and my variables are :
variable "apps" {
type = map(object({
app_name = string
labels = map(string)
annotations = map(string)
image = string
}))
default = {
"app1_name" = {
app_name = "app1_name"
labels = {
"name" = "stream-frontend"
"tier" = "web"
"owner" = "product"
}
annotations = {
"serviceClass" = "web-frontend"
"loadBalancer/class" = "external"
}
image = "nxinx"
}
"app2_name" = {
app_name = "app2_name"
labels = {
"name" = "stream-frontend"
"tier" = "web"
"owner" = "product"
}
annotations = {
"serviceClass" = "web-frontend"
"loadBalancer/class" = "external"
}
image = "nginx"
}
"app3_name" = {
app_name = "app3_name"
labels = {
"name" = "stream-database"
"tier" = "shared"
"owner" = "product"
}
annotations = {
"serviceClass" = "disabled"
"loadBalancer/class" = "disabled"
}
image = "Mongo"
}
}
}
variable "acl" {
type = map(object({
acl_name = string
ingress = string
egress = string
port = string
protocol = string
}))
default = {
"frontend" = {
acl_name = "acl_frontend"
ingress = "stream-frontend"
egress = "0.0.0.0/0"
port = "80"
protocol = "TCP"
},
"backend" = {
acl_name = "acl_backend"
ingress = "stream-backend"
egress = "0.0.0.0/0"
port = "80"
"protocol" = "TCP"
},
"database" = {
acl_name = "acl_database"
"ingress" = "stream-database"
"egress" = "172.17.0.0/24"
"port" = "27017"
"protocol" = "TCP"
}
}
}
Making a for_each loop to access values of the variables etc.
resource "kubernetes_network_policy" "acl" {
for_each = var.merged_vars
metadata {
name = format("%s-acl", each.value.acl_name)
namespace = each.value.acl_name
}
spec {
policy_types = ["Ingress", "Egress"]
pod_selector {
match_labels = {
tier = each.value.labels.tier
}
}
ingress {
from {
namespace_selector {
match_labels = {
name = each.value.ingress
}
}
}
ports {
port = each.value.port
protocol = each.value.protocol
}
}
egress {
to {
ip_block {
cidr = each.value.egress
}
}
}
}
}
eventually I need to have a way to access the "apps" and "acl" parameters as "var.apps.labels" and "var.acl.port" etc.
thank you for your help!
been trying:
variable "merged_vars" {
default = merge(var.apps, var.acl)
}
and the result i got is :
│ Error: Function calls not allowed
│
│ on NewVars.tf line 96, in variable "merged_vars":
│ 96: default = merge(var.apps, var.acl)
│
│ Functions may not be called here.
alo tried with concat and got the same result
As the error states, you can't create dynamic variables. But you can create local variables dynamically. So you can do:
locals {
merged_vars = merge(var.apps, var.acl)
}
And for the for_each you will use local:
for_each = local.merged_vars

Using Local values to define Azure Databricks User block

I would like to give access to two users to use Azure Databricks using the below block :
resource "databricks_user" "dbuser" {
display_name = local.name.display_name
user_name = local.name.user_name
workspace_access = true
}
I have locals defined as below :
locals {
name = {
display_name = ["Vincent Casinha",
"Susan Young"
]
user_name = ["vincent.casinha#contoso.com",
"susan.young#contoso.com"]
}
}
While trying to run terraform plan i get the error attached. How do i use the local values properly in the databricks_user block for the argumens display_name & user_name?
locals {
display_name = ["Vincent Casinha", "Susan Young"]
user_name = ["vincent.casinha#contoso.com","susan.young#contoso.com"]
}
#assuming length of display_name and user_name are same
resource "databricks_user" "dbuser" {
count = length(local.display_name)
display_name = local.display_name[count.index]
user_name = local.user_name[count.index]
workspace_access = true
}
#or you can do like below
locals {
name = [
{
display_name = "Vincent Casinha"
user_name = "vincent.casinha#contoso.com"
},
{
display_name = "Susan Young"
user_name = "susan.young#contoso.com"
}
]
}
resource "databricks_user" "dbuser" {
for_each = local.name
display_name = each.value.display_name
user_name = each.value.user_name
workspace_access = true
}

Looping in for_each nested resources with 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.

How to create multiple instances with multiple subnet ids using terraform?

I have 2 services test1,test2 and for each service i have to create 6 vm's.This 6 vm's should be placed in 3 subnet id's which created in 3 different zones in a same region
In this services,test1 will be in private subnets and test2 will be in public subnets.So i have to pass that correct subnet id when creating ec2 instances
root module:
provider "aws" {
region = var.region
}
module "ecom-vpc" {
source = "./modules/vpc"
}
module "ecom-public-subnet" {
source = "./modules/subnets/public"
vpc-id = module.ecom-vpc.vpc-id
}
module "ecom-private-subnet" {
source = "./modules/subnets/private"
vpc-id = module.ecom-vpc.vpc-id
}
module "ecom-instances-sg" {
source = "./modules/sg"
vpc-id = module.ecom-vpc.vpc-id
}
module "ecom-vm-instances" {
source = "./modules/vm"
priv-subnet-ids = module.ecom-private-subnet.ecom_private_subnets
pub-subnet-ids = module.ecom-public-subnet.ecom_public_subnets
instances-sg = module.ecom-instances-sg.ecom-inst-sg
}
From child modules - vpc,subnets,ec2
variable "service-names" {
type = list(any)
default = ["ecom-app-TEST1","ecom-app-TEST2"]
}
variable "availability_zones" {
type = map
default = {
ap-south-1a = {
private_subnet = "10.0.1.0/24"
public_subnet = "10.0.4.0/24"
}
ap-south-1b = {
private_subnet = "10.0.2.0/24"
public_subnet = "10.0.5.0/24"
}
ap-south-1c = {
private_subnet = "10.0.3.0/24"
public_subnet = "10.0.6.0/24"
}
}
}
resource "aws_vpc" "ecom-vpc" {
cidr_block = var.ecom-cidr
}
output "vpc-id" {
value = aws_vpc.ecom-vpc.id
}
resource "aws_subnet" "ecom-private" {
for_each = var.availability_zones
vpc_id = var.vpc-id
cidr_block = each.value.private_subnet
availability_zone = each.key
map_public_ip_on_launch = false
tags = {
Name = "${split("-", each.key)[2]}"
Subnet_Type = "private"
}
}
output "ecom_private_subnets" {
value = aws_subnet.ecom-private
}
resource "aws_subnet" "ecom-public" {
for_each = var.availability_zones
vpc_id = var.vpc-id
cidr_block = each.value.public_subnet
availability_zone = each.key
map_public_ip_on_launch = true
tags = {
Name = "${split("-", each.key)[2]}"
Subnet_Type = "public"
}
depends_on = [aws_internet_gateway.igw
]
}
output "ecom_public_subnets" {
value = aws_subnet.ecom-public
}
I'm trying to achieve the same by creating a locals which combines service names,priv,public subnet id's,instance count(2).But the problem is i'm not able to make it because i'm not able to create a unique combination of keys
locals {
service_subnets = {
for pair in setproduct(var.service-names, values(var.priv-subnet-ids),values(var.pub-subnet-ids),range(var.instance_count)) :
"${pair[0]}:${pair[1].availability_zone}-${pair[3]}" => {
service_name = pair[0]
priv-subnet = pair[1]
pub-subnet = pair[2]
}
}
}
resource "aws_instance" "ecom-instances" {
for_each = local.service_subnets
ami = data.aws_ami.ecom.id
instance_type = "t3.micro"
tags = {
Name = each.value.service_name
Service = each.value.service_name
}
vpc_security_group_ids = [var.instances-sg[each.value.service_name].id]
subnet_id = "${split("-", each.value.service_name)[2] == "TEST1" ? each.value.pub-subnet.id : each.value.priv-subnet.id }"
}
I'm getting the below error.
Two different items produced the key "ecom-app-TEST1:ap-south-1c-1" in this 'for' expression. If duplicates are expected, use the ellipsis (...)
│ after the value expression to enable grouping by key.
If i add ... and change it as below then it is converted as tuple and i'm not sure how to get and pass the value from each.value in the aws_instance resource
locals {
service_subnets = {
for pair in setproduct(var.service-names, values(var.priv-subnet-ids),values(var.pub-subnet-ids),range(var.instance_count)) :
"${pair[0]}:${pair[1].availability_zone}-${pair[3]}" => {
service_name = pair[0]
priv-subnet = pair[1]
pub-subnet = pair[2]
}
... }
}
on modules/vm/main.tf line 60, in resource "aws_instance" "ecom-instances":
│ 60: subnet_id = "${split("-", each.value.service_name)[2] == "TEST1" ? each.value.pub-subnet.id : each.value.priv-subnet.id }"
│ ├────────────────
│ │ each.value is tuple with 3 elements
│
│ This value does not have any attributes.
Please Guide me
You could try adding the index to your for loop and making it part of your name, might help you avoid elipsis/tuple conversion.
locals {
service_subnets = {
for index, pair in setproduct(var.service-names, values(var.priv-subnet-ids),values(var.pub-subnet-ids),range(var.instance_count)) :
"${pair[0]}:${pair[1].availability_zone}-${pair[3]}=${index}" => {
service_name = pair[0]
priv-subnet = pair[1]
pub-subnet = pair[2]
}
}
}

tfe_variable JSON to HCL support

I'm hitting Error: Incorrect attribute value type when passing var_3 below to a tfe_variable of type HCL.
Is there a way to convert the decoded JSON variable to HCL?
My config.json:
{
"vars": {
"var_1": "foo",
"var_2": "bar",
"var_3": {
"default": "foo"
}
}
}
My terraform config:
variable "tfe_token" {}
provider "tfe" {
hostname = "app.terraform.io"
token = var.tfe_token
}
data "tfe_workspace" "this" {
name = "my-workspace-name"
organization = "my-org-name"
}
locals {
json_config = jsondecode(file("config.json"))
}
resource "tfe_variable" "workspace" {
for_each = local.json_config.vars
workspace_id = data.tfe_workspace.this.id
key = each.key
value = each.value
category = "terraform"
hcl = true
sensitive = false
}

Resources