Create resource for each value in a map - azure

I am trying to create a resource for each value in a map.
For example, I need to create a azuread_application_password resource. I would like a new secret per user of the application, so I have created a map that container the person using the application, and which parts of the application they can have access to:
variable "users" {
type = map(list(string))
}
users = {"user_1" : ["area_1", "area_2"], "user_2" : ["area_3", "area_4"]}
The value in the map also corresponds to the name of the application created via a azuread_application
resource "azuread_application_password" "client_secret" {
for_each = var.users
display_name = "Secret-${each.key}"
application_object_id = azuread_application.auth[each.value].object_id
}
I would like to end up with an azuread_application_password created for each value in the map, and the display name to be the key.
The above block does not work because each.value is a list(string)

You have to flatten your map:
locals {
users_flat = merge([
for user, areas in var.users: {
for area in areas:
"${user}-${area}" => {
"area" = area
"user" = user
}
}
]...)
}
then
resource "azuread_application_password" "client_secret" {
for_each = local.users_flat
display_name = "Secret-${each.value.user}"
application_object_id = azuread_application.auth[each.value.area].object_id
}

Related

How to use Terraform for_each resource block and count resource block

I am having a azurerm_postgresql_flexible_server resource using count and azurerm_postgresql_flexible_server_configuration using fore each. Please find the below code.
config= [{
"name" = shared_preload_libraries,
"values" = ["EXAMPLE1", "EXAMPLE2"]
},
{
"name" = "azure.extensions"
"values" = ["EXAMPLE1", "EXAMPLE2", "EXAMPLE3"]
}]
locals {
flat_config = merge([
for single_config in var.config: {
for value in single_config.values:
"${single_config.name}-${value}" => {
"name" = single_config.name
"value" = value
}
}
]...)
}
Below is my for_each resource
resource "azurerm_postgresql_flexible_server_configuration" "example" {
for_each = local.flat_config
name = each.value.name
server_id = azurerm_postgresql_flexible_server.example.id
value = each.value.value
}
currently I am having two azurerm_postgresql_flexible_server resources. azurerm_postgresql_flexible_server.example[0] and azurerm_postgresql_flexible_server.example[1]. Could you please let me know if there is a possibility to include some alternative options like count?
Splat Expressions did not work.
If you want to apply your two azurerm_postgresql_flexible_server for each instance of azurerm_postgresql_flexible_server_configuration, you need one more extra level of flattening:
locals {
flat_config = merge(flatten([
for single_config in var.config: [
for value in single_config.values: {
for idx, server in azurerm_postgresql_flexible_server.example:
"${single_config.name}-${value}-${idx}" => {
"name" = single_config.name
"value" = value
"flexible_server" = server
}
}
]
])...)
}
then
resource "azurerm_postgresql_flexible_server_configuration" "example" {
for_each = local.flat_config
name = each.value.name
server_id = each.value.flexible_server.id
value = each.value.value
}

Terraform - Output Index along with item

Is there a way to modify this output code to include the index number with the name? The current code below creates one output with Index '0', but each name should ideally be in its own Index (0, 1, 2, etc.)
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.1.0"
}
}
}
provider "azurerm" {
features {
}
}
variable "private_link_scopes" {
description = "A list of Azure private link scopes."
type = list(object({
name = string
resource_group_name = string
}))
}
locals {
rgs_map = {
for n in var.private_link_scopes : n.resource_group_name => {
name = n.resource_group_name
}
}
}
data "azurerm_resource_group" "rg" {
for_each = local.rgs_map
name = each.value.name
}
output "list" {
value = { for index, item in [var.private_link_scopes] : index => item[*].name }
}
It seems like there is a lot of structure change going on here. That may be due to other baggage or reasons that depend on it, but I think this could be simplified. I use locals in lieu of the variable, but I hope is help.
I'm not sure the splat operator there is what you want. That is the same as getting a list of all items' name attributes in that item value, but there would only be one per item so that seems strange.
locals {
scopes = [
{
name = "name_a"
rg_name = "rg_a"
},
{
name = "name_b"
rg_name = "rg_b"
}
]
}
data "azurerm_resource_group" "rg" {
# I don't see a reason to generate the intermediary map
# or to build another object in each map value. This is more
# simple. for_each with an object by name: rg_name
value = { for scope in local.scopes : scope.name => scope.rg_name }
name = each.value
}
output "list" {
# I don't see a need to use a splat expression here.
# You can just build a map with the index and the name
# here directly.
value = { for i, v in local.scopes : i => v.name }
}
In fact, if you don't need the resource group resources to be keyed by name, that can be simplified further to:
data "azurerm_resource_group" "rg" {
for_each = toset([for scope in local.scopes : scope.rg_name])
name = each.value
}

Referencing a created subnet and associate it with a given EIP for a NLB (aws provider)

I'm trying to parametrize the creation of a NLB, and provision in the same plan the necessary public subnets.
The subnets are specified as a variable of the plan:
variable "nlb_public_subnets" {
type = list(object({
name = string
network_number = number
availability_zone = string
elastic_ip = string
}))
default = [
{
name = "sftp_sub_A"
network_number = 1
availability_zone = "eu-west-1a"
elastic_ip = "X.Y.Z.T"
},
{
name = "sftp_sub_B"
network_number = 2
availability_zone = "eu-west-1b"
elastic_ip = "XX.YY.ZZ.TT"
}
]
}
variable "common_tags" {
description = "A map containing the common tags to apply to all resources"
type = map(string)
default = {}
}
locals {
vpc_id = "dummy"
base_cidr = "10.85.23.0/24"
publicSubnets = { for s in var.nlb_public_subnets :
s.name => {
name = s.name
cidr_block = cidrsubnet(var.base_public_subnet_cidr_block, 6,
s.network_number )
availability_zone = s.availability_zone
elastic_ip = s.elastic_ip
}
}
}
I'm specifying a name, a network number (to compute the cidr block), an availability zone, and an elastic IP to map to when creating the NLB.
Here I'm creating the subnets:
#Comment added after solution was given
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
resource "aws_subnet" "sftp_nlb_subnets" {
for_each = { for subnet in local.publicSubnets :
subnet.name => subnet
}
cidr_block = each.value.cidr_block
vpc_id = local.vpc_id
availability_zone = each.value.availability_zone
tags = {
Name = each.key
Visibility = "public"
Purpose = "NLB"
}
}
Now I need to create my NLB, and this is where I'm struggling on how to associate the freshly created subnets with the Elastic IP provided in the configuration:
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets: subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags,{
Name=var.name
})
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = ????Help???
}
}
}
Could I somehow look up the configuration object with the help of the subnet name in the tags?
UPDATE1
Updated the dynamic block, as it had a typo.
UPDATE2
#tmatilai nailed the answer!
Here's the modified aws_lb block:
#
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets : subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags, {
Name = var.name
})
dynamic "subnet_mapping" {
#subnet_mapping.key will contain subnet.name, so we can use it to access the Map data.aws_eip.nlb (also indexed by subnet.name) to get the eip allocation_id
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
The trick is to realize that both aws_subnet.sftp_nlb_subnets and data.aws_eip.nlb are a Map, indexed by the key of local.publicSubnets. This allows us to use this common key (the subnet name) in the map aws_subnet.sftp to look up information in the data (data.aws_eip.nlb) obtained from the original input, local.publicSubnets.
Thanks. This is a neat trick.
Passing the IP address of the elastic IPs sounds strange. If you create the EIPs elsewhere, why not pass the (allocation) ID of them instead?
But with this setup, you can get the allocation ID with the aws_eip data source:
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
But maybe it would make more sense to create the EIPs also here. For example something like this:
resource "aws_eip" "nlb" {
for_each = local.publicSubnets
vpc = true
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = aws_eip.nlb[subnet_mapping.key].id
}
}
}

How feasible is this with Terraform?

I'm trying to write a Terraform module whose purpose is to add members (users and service principals) to Azure AD groups. Here's my code:
# This is our input which contains the display names of users, SPNs and groups
memberships_service_principals = {
"spn1" = "group1"
"spn2" = "group2"
}
memberships_users = {
"user1" = "group1"
"user1" = "group2"
"user2" = "group2"
}
# Here we get each SPN's group object ID
data "azuread_group" "groups_service_principals" {
for_each = var.memberships_service_principals
display_name = each.value
}
# Here we get each SPN's object ID
data "azuread_service_principal" "service_principals" {
for_each = var.memberships_service_principals
display_name = each.key
}
# Here we get each user's group object ID
data "azuread_group" "groups_users" {
for_each = var.memberships_users
display_name = each.value
}
# Here we get each user's object ID
data "azuread_user" "users" {
for_each = var.memberships_users
user_principal_name = each.key
}
# Here we construct and merge two maps of groups => object IDs
locals {
service_principal_object_ids = { for k, v in var.memberships_service_principals : data.azuread_service_principal.service_principals["${k}"].object_id => data.azuread_group.groups_service_principals["${k}"].object_id }
user_object_ids = { for k, v in var.memberships_users : data.azuread_user.users["${k}"].object_id => data.azuread_group.groups_users["${k}"].object_id }
member_object_ids = merge(local.service_principal_object_ids, local.user_object_ids)
}
# Here we configure the memberships by passing the group and SPN/user object IDs
resource "azuread_group_member" "group_members" {
for_each = var.member_object_ids
group_object_id = each.value
member_object_id = each.key
}
Unfortunately, the above code doesn't work because a map in Terraform can have only unique keys. This rule gets violated in these cases:
When a user/SPN is specified more than once in order to be member of more than one group
Even if the map gets inverted (so as to have groups as keys instead of users/SPNs), group names and IDs are also not unique
I tried to define my input as follows but I still have no idea how to come up with a map of unique keys which to pass to create the memberships via 'resource "azuread_group_member"':
locals {
test_input1 = [
{principal = "user1", groups = ["group1", "group2"]},
{principal = "user2", groups = ["group2"]}
]
test_input2 = [
{ "user1" = ["group1", "group2"] },
{ "user2" = ["group2"] }
]
}
Any help and/or ideas will be greatly appreciated!
I don't fully understand your setup, but you could create a map from your variables with unique keys as follows:
variable "memberships_users" {
default = {
"user1" = "group1"
"user1" = "group2"
"user2" = "group2"
}
}
locals {
service_users = merge([
for k1,v1 in var.memberships_service_principals:
{
for k2,v2 in var.memberships_users:
"${k1}-${k2}" => {
memberships_service_principal = k1
memberships_service_principal_group = v1
memberships_user = k2
memberships_user_group = v2
}
}
]...)
}
which gives:
service_users = {
"spn1-user1" = {
"memberships_service_principal" = "spn1"
"memberships_service_principal_group" = "group1"
"memberships_user" = "user1"
"memberships_user_group" = "group2"
}
"spn1-user2" = {
"memberships_service_principal" = "spn1"
"memberships_service_principal_group" = "group1"
"memberships_user" = "user2"
"memberships_user_group" = "group2"
}
"spn2-user1" = {
"memberships_service_principal" = "spn2"
"memberships_service_principal_group" = "group2"
"memberships_user" = "user1"
"memberships_user_group" = "group2"
}
"spn2-user2" = {
"memberships_service_principal" = "spn2"
"memberships_service_principal_group" = "group2"
"memberships_user" = "user2"
"memberships_user_group" = "group2"
}
}
Then you could use that:
# SAMPLE CODE. Its not clear what you wish to accomplish,
# thus some changes probably will be required!
resource "azuread_group_member" "group_members" {
for_each = local.service_users
group_object_id = data.azuread_group.groups_service_principals[each.value.memberships_service_principal].object_id
member_object_id =data.azuread_group.groups_users[each.value.memberships_user].object_id
}

Looping using for or For_each | Terraform 0.12

I need to create multiple subnets in GCP within a network. I am planning to use Terraform 0.12 syntax for the same as follows:
project_name = [
"order-dev",
"ship-dev"
]
variable "project_name" {
type = list(string)
description = "Name of the project"
}
resource "google_compute_subnetwork" "subnetwork" {
name = "${var.project_name}-subnetwork"
ip_cidr_range = var.subnet_ip_cidr_range
region = var.region
network = google_compute_network.network.self_link
}
Is there anyway to use for or for_each expression in this scenario, i am aware of using element and doing this. But want to try a different approach if possible?
variable "project_name" {
type = set(string)
}
resource "google_compute_subnetwork" "subnetwork" {
for_each = var.project_name
name = "${each.key}-subnetwork"
ip_cidr_range = var.subnet_ip_cidr_range
region = var.region
network = google_compute_network.network.self_link
}
Try using the count meta-argument
With your sample, something like this
project_name = [
"order-dev",
"ship-dev"
]
variable "project_name" {
type = list(string)
description = "Name of the project"
}
resource "google_compute_subnetwork" "subnetwork" {
count = length(var.project_name)
name = "${var.project_name[count.index]}-subnetwork"
ip_cidr_range = var.subnet_ip_cidr_range
region = var.region
network = google_compute_network.network.self_link
}
Another option is for_each with key-value pairs, but you only have access to one value and I don't think you can use a list variable like your sample.
resource "google_compute_subnetwork" "subnetwork" {
for_each = {
order = "order-dev"
ship = "ship-dev"
}
name = "${key.value}-subnetwork"
ip_cidr_range = var.subnet_ip_cidr_range
region = var.region
network = google_compute_network.network.self_link
}
Resources:
https://www.terraform.io/docs/configuration/resources.html#count-multiple-resource-instances-by-count
https://blog.gruntwork.io/terraform-tips-tricks-loops-if-statements-and-gotchas-f739bbae55f9
https://www.terraform.io/docs/configuration/resources.html#for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings

Resources