How feasible is this with Terraform? - 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
}

Related

Terraform : Is it possible to interpolate or loop through ressources from a list within the output block?

I am trying to output gcp project information by doing something like this :
output "projects" {
value = tomap({
for project_name in ["project_1", "project_2", "project_3"] :
project_name => tomap({
id = google_project."${project_name}".id
number = google_project."${project_name}".number
})
})
description = "Projects"
}
Or like this :
output "projects" {
value = tomap({
for_each = toset([google_project.project_1,google_project.project_2])
id = each.key.id
number = each.key.number
})
description = "Projects"
}
Is it at all possible to use resource names this way? Do I have to specify every resource by duplicating code?
E.g.
output "projects" {
value = tomap({
project_1 = tomap({
id = google_project.project_1.id
number = google_project.project_1.number
})
project_2 = tomap({
id = google_project.project_2.id
number = google_project.project_2.number
})
project_3 = tomap({
id = google_project.project_3 .id
number = google_project.pproject_3 .number
})
})
description = "Projects"
}
EDIT : declared resources.
In main.tf projects 1 to 3 are declared the same way.
resource "google_project" "project_3" {
name = var.projects.project_3.name
project_id = var.projects.project_3.id
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
in variables.tf
variable "projects" {
type = map(object({
name = string
id = string
}))
}
in variables.tfvars
projects = {
project_1= {
name = "project_1"
id = "project_1-12345"
}
project_2= {
name = "project_2"
id = "project_2-12345"
}
project_3= {
name = "project_2"
id = "project_2-12345"
}
}
I misunderstood your question originally. I see now that you want to reference a resource by a variable name. No you cannot do that. But your setup here doesn't really make sense, and seems more complex than it needs to be.
Consider if these options would improve your setup.
locals {
projects = { # This is equivalent to your input.
project_1 = {
name = "project_1"
id = "project_1-12345"
}
project_2 = {
name = "project_2"
id = "project_2-12345"
}
project_3 = {
name = "project_3"
id = "project_3-12345"
}
}
}
resource "google_project" "this" {
for_each = local.projects
name = each.key # or each.value.name / don't really need name
project_id = each.value.id
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
output "projects_from_input" {
description = "You can of course, just use the input."
value = local.projects
}
output "projects_explicit_values" {
description = "Alternatively, if you need a subset of resource values."
value = { for k, v in google_project.this : k => {
name = v.name
id = v.project_id
} }
}
output "complete_resources" {
description = "But you can just output the complete resource."
value = google_project.this
}
I edited my initial answer after seeing the Terraform resource that creates a project. The need is a way to get a resource name in the output bloc with interpolation.
I think if a single resource is used to create all the projets instead of one resource per projet, it's easier to expose this resource in the output bloc.
For example you can configure projects metadata information from a json file, or directly a local variable or a var if needed :
Example for a json file and local variable
mymodule/resource/projects.json :
{
"projects": {
"project_1": {
"id": "project_1",
"number": "23333311"
},
"project_2": {
"id": "project_2",
"number": "33399999"
}
}
}
Then retrieve projects as a variable from locals.tf file :
mymodule/locals.tf :
locals {
projects = jsondecode(file("${path.module}/resource/projects.json"))["projects"]
}
Create your projects in a single resource with a foreach :
resource "google_project" "projects" {
for_each = local.projects
name = each.key
project_id = each.value["id"]
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
Expose the projects resource in an output.tf file :
output "projects" {
value = google_project.projects
description = "Projects"
}
The same principle can be done with a var instead of local variable.

Terraform modules: creating multiple resources with modules and reference each other

I have defined a variable tenants which is a map:
variable tenants {
type = map
default = {
tenant1 = {
name = "Tenant1"
},
tenant2 = {
name = "Tenant2"
}
}
}
In the root file, I call a module tenant as follows:
module "tenant" {
source = "./modules/tenant"
for_each = var.tenants
tenant = each.value.name
}
The child module tenant looks as follows:
resource "mso_tenant" "tenant" {
name = var.tenant
}
And I have defined an output in the child module with:
output "mso_tenant" {
value = mso_tenant.tenant
In the output file under root, I do the following to print all tenants:
output "tenant_names" {
value = { for p in sort(keys(var.tenants)) : p => module.tenant[p].mso_tenant.name }
}
So far so good, all the above works.
Now I have another variable called schemas, also a map. A tenant can have multiple schema's. So I have defined the following
variable schemas {
type = map
default = {
schema1 = {
name = "Schema1",
template_name = "Template1",
tenant = <refer to tenant1> <==== refer to tenant module
},
schema2 = {
name = "Schema2"
template_name = "Template2",
tenant = <refer to tenant2> <==== refer to tenant module
},
schema3 = {
name = "Schema3"
template_name = "Template3",
tenant = <refer to tenant1> <==== refer to tenant module
},
}
}
In the main.tf file under root I want to do the following:
module "schema" {
source = "./modules/schema"
for_each = var.schemas
name = each.value.name
template_name = each.value.template_name
tenant_id = each.value.tenant
}
How could I reference the respective tenants in either the schema variable or else directly in the schema module?
Update:
Tried solution 1:
In variables file, I passed the tenant as follows:
schema1 = {
name = "Schema1",
template_name = "Template1",
tenant = module.tenant["tenant1"].mso_tenant
}
Error: Variables not allowed
Tried solution 2:
module "tenant" {
source = "./modules/tenant"
for_each = var.tenants
tenant = each.value.name
}
module "schema" {
source = "./modules/schema"
for_each = var.schemas
name = each.value.name
template_name = each.value.template_name
tenant_id = module.tenant[each.value.tenant].mso_tenant
}
Resulting in following error:
on main.tf line 30, in module "schema":
30: tenant_id = module.tenant[each.value.tenant].mso_tenant
|----------------
| each.value is object with 2 attributes
This object does not have an attribute named "tenant".
If you want to refer to the tenant resources of tenant1 you can use module.tenant["tenant1"].mso_tenant and assign this directly to the schema or in the input variables of the second module.
As you are using module for_each module.tenant is a map of objects keyed by the tenant_id. Each object consists of all the outputs of your tenant child module.
REFACTORED:
root variable definitions:
variable tenants {
type = map
default = {
tenant1 = {
name = "Tenant1"
},
tenant2 = {
name = "Tenant2"
}
}
}
variable schemas {
type = map
default = {
schema1 = {
name = "Schema1",
template_name = "Template1",
tenant_id = "tenant1"
},
schema2 = {
name = "Schema2"
template_name = "Template2",
tenant_id = "tenant2"
},
schema3 = {
name = "Schema3"
template_name = "Template3",
tenant_id = "tenant1"
},
}
}
calling the modules like this in the root:
module "tenant" {
source = "./modules/tenant"
for_each = var.tenants
tenant = each.value.name
}
module "schema" {
source = "./modules/schema"
for_each = var.schemas
name = each.value.name
template_name = each.value.template_name
tenant = module.tenant[each.value.tenant_id].mso_tenant
}
root output definitions could look like:
output "tenant_names" {
value = { for p in sort(keys(var.tenants)) : p => module.tenant[p].mso_tenant.name }
}
output "schemas" {
value = module.schema
}
this leads to the following outputs:
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
schemas = {
"schema1" = {
"schema" = {
"name" = "Schema1"
"template_name" = "Template1"
"tenant" = {
"name" = "Tenant1"
}
}
}
"schema2" = {
"schema" = {
"name" = "Schema2"
"template_name" = "Template2"
"tenant" = {
"name" = "Tenant2"
}
}
}
"schema3" = {
"schema" = {
"name" = "Schema3"
"template_name" = "Template3"
"tenant" = {
"name" = "Tenant1"
}
}
}
}
tenant_names = {
"tenant1" = "Tenant1"
"tenant2" = "Tenant2"
}
schema module used to simulate:
variable "name" {
type = string
}
variable "template_name" {
type = string
}
variable "tenant" {
type = any
}
output "schema" {
value = {
name = var.name
template_name = var.template_name
tenant = var.tenant
}
}
tenant module used to simulate:
variable "tenant" {
type = string
}
locals {
mso_tenant = {
tenant = {
name = var.tenant
}
}
}
output "mso_tenant" {
value = local.mso_tenant.tenant
}
hope in the full context this is easier and more clear ;)

terraform How to use conditional if in for_each into map object

I have maps of variables like this:
users.tfvars
users = {
"testterform" = {
path = "/"
force_destroy = true
email_address = "testterform#example.com"
group_memberships = [ "test1" ]
tags = { department : "test" }
ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAA4l7"
}
"testterform2" = {
path = "/"
force_destroy = true
email_address = "testterform2#example.com"
group_memberships = [ "test1" ]
tags = { department : "test" }
ssh_public_key = ""
}
I would like to upload ssh key only if ssh_public_key not empty for the user. But don't understand how to check this
#main.tf
resource "aws_iam_user" "this" {
for_each = var.users
name = each.key
path = each.value["path"]
force_destroy = each.value["force_destroy"]
tags = merge(each.value["tags"], { Provisioner : var.provisioner, EmailAddress : each.value["email_address"] })
}
resource "aws_iam_user_group_membership" "this" {
for_each = var.users
user = each.key
groups = each.value["group_memberships"]
depends_on = [ aws_iam_user.this ]
}
resource "aws_iam_user_ssh_key" "this" {
for_each = var.users
username = each.key
encoding = "SSH"
public_key = each.value["ssh_public_key"]
depends_on = [ aws_iam_user.this ]
}
It sounds like what you need here is a derived "users that have non-empty SSH keys" map. You can use the if clause of a for expression to derive a new collection from an existing one while filtering out some of the elements:
resource "aws_iam_user_ssh_key" "this" {
for_each = {
for name, user in var.users : name => user
if user.ssh_public_key != ""
}
username = each.key
encoding = "SSH"
public_key = each.value.ssh_public_key
depends_on = [aws_iam_user.this]
}
The derived map here uses the same keys and values as the original var.users, but is just missing some of them. That means that the each.key results will correlate and so you'll still get the same username value you were expecting, and your instances will have addresses like aws_iam_user_ssh_key.this["testterform"].
You can use a for loop to exclude those blanks.
For example, you can do it on local:
variable "users" {
default = {
"testterform" = {
path = "/"
force_destroy = true
tags = { department : "test" }
ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAA4l7"
}
"testterform2" = {
path = "/"
force_destroy = true
tags = { department : "test" }
ssh_public_key = ""
}
}
}
locals {
public_key = flatten([
for key, value in var.users :
value.ssh_public_key if ! contains([""], value.ssh_public_key)
])
}
output "myout" {
value = local.public_key
}
that will output:
myout = [
"ssh-rsa AAAAB3NzaC1yc2EAAA4l7",
]
As you can see the empty ones have been removed, and you can add other stuff you want to exclude on that contains array.
Then you can use that local.public_key in the for_each for your ssh keys

Terraform .12 nested loop

variables.tf
variable "teams" {
type = map(any)
default = {}
}
input_value:
teams = {
{
team_id = "abc"
role_names = ["owner"]
},
{
team_id = "bcd"
role_names = ["read", "write"]
}
}
}
main.tf:
resource "mongodbatlas_project" "project" {
name = "testing"
org_id = "123456"
dynamic "teams" {
for_each = var.teams
content {
id = teams.value.team_id
names = [teams.value.role_names]
}
}
}
I have been trying the above code and it is not working. Is there an easier way to assign nested team value to the variable?
The teams variable does not seem to be correct for me and there are syntax errors (e.g. extra }in teams). I think it should be list, not map:
variable "teams" {
type = list(any)
default = []
}
and then
teams = [
{
team_id = "abc"
role_names = ["owner"]
},
{
team_id = "bcd"
role_names = ["read", "write"]
}
]
Then your resource could be:
resource "mongodbatlas_project" "project" {
name = "testing"
org_id = "123456"
dynamic "teams" {
for_each = toset(var.teams)
content {
id = teams.value.team_id
names = teams.value.role_names
}
}
}
When using dynamic blocks the iterator is called same as the block name.

Terraform resource for_each with nested dynamic block keeps re-applying the same changes

So I have created a google_bigquery module to create datasets and set access.
The module iterates over a map of list of maps. It uses the each.key to create the datasets then iterates over the list of maps to create the dynamic access.
The module works as in:
It has no errors nor warning
It deploys the resources
It populates the remote statefile appropriately.
The issue is that everytime I ran terraform it wants to re-apply the same changes, over and over again.
Clearly something is not right but not sure what.
here is the code
main.tf
locals {
env = basename(path.cwd)
project = basename(abspath("${path.cwd}/../.."))
project_name = coalesce(var.project_name, format("%s-%s", local.project, local.env))
}
data "google_compute_zones" "available" {
project = local.project_name
region = var.region
}
provider "google" {
project = local.project_name
region = var.region
version = "~> 2.0" #until 3.0 goes out of beta
}
terraform {
required_version = ">= 0.12.12"
}
resource "google_bigquery_dataset" "main" {
for_each = var.datasets
dataset_id = upper("${each.key}_${local.env}")
location = var.region
delete_contents_on_destroy = true
dynamic "access" {
for_each = flatten([ for k, v in var.datasets : [
for i in each.value : {
role = i.role
user_by_email = i.user_by_email
group_by_email = i.group_by_email
dataset_id = i.dataset_id
project_id = i.project_id
table_id = i.table_id
}]])
content {
role = lookup(access.value,"role", "")
user_by_email = lookup(access.value,"user_by_email","")
group_by_email = lookup(access.value,"group_by_email","")
view {
dataset_id = lookup(access.value,"dataset_id","")
project_id = lookup(access.value,"project_id","")
table_id = lookup(access.value,"table_id", "")
}
}
}
access {
role = "READER"
special_group = "projectReaders"
}
access {
role = "OWNER"
group_by_email = "Group"
}
access {
role = "OWNER"
user_by_email = "ServiceAccount"
}
access {
role = "WRITER"
special_group = "projectWriters"
}
}
variables.tf
variable "region" {
description = ""
default = ""
}
variable "env" {
default = ""
}
variable "project_name" {
default = ""
}
variable "owner_group" {
description = ""
default = ""
}
variable "owner_sa" {
description = ""
default = ""
}
variable "datasets" {
description = "A map of objects, including dataset_isd abd access"
type = map(list(map(string)))
}
terraform.tfvars
datasets = {
dataset01 = [
{
role = "WRITER"
user_by_email = "email_address"
group_by_email = ""
dataset_id = ""
project_id = ""
table_id = ""
},
{
role = ""
user_by_email = ""
group_by_email = ""
dataset_id ="MY_OTHER_DATASET"
project_id ="my_other_project"
table_id ="my_test_view"
}
]
dataset02 = [
{
role = "READER"
user_by_email = ""
group_by_email = "group"
dataset_id = ""
project_id = ""
table_id = ""
},
{
role = ""
user_by_email = ""
group_by_email = ""
dataset_id ="MY_OTHER_DATASET"
project_id ="my_other_project"
table_id ="my_test_view_2"
}
]
}
So the problem is that the dynamic block (the way I wrote it) can generate this output
+ access {
+ role = "WRITER"
+ special_group = "projectWriters"
+ view {}
}
this is applied, no errors, but it will want to re-apply it over and over
The issue seems to be that the provider API response doesn't include the empty view{}
Any suggestion how I could make the view block conditional on the values of it being not null?
I fixed the problem. I changed the module slightly and the variable type.
I have split the roles and the views into their own lists of maps within the parent map of datasets.
There are conditionals in each block so the dynamic block is only applied if the roles exists or views exists.
Also realized the dynamic block was iterating on the wrong iterator.
The dynamic block was iterating on var.datasets which was causing the permissions assigned to each dataset to be applied to all datasets. So now it has been changed to iterate on each.value (from the resource for_each).
Here is the new code that works
MAIN.TF
resource "google_bigquery_dataset" "main" {
for_each = var.datasets
dataset_id = upper("${each.key}_${local.env}")
location = var.region
delete_contents_on_destroy = true
dynamic "access" {
for_each = flatten([for i in each.value : [
for k, v in i : [
for l in v :
{
role = l.role
user_by_email = l.user_by_email
group_by_email = l.group_by_email
special_group = l.special_group
}]
if k == "roles"
]])
content {
role = access.value["role"]
user_by_email = access.value["user_by_email"]
group_by_email = access.value["group_by_email"]
special_group = access.value["special_group"]
}
}
dynamic "access" {
for_each = flatten([for i in each.value : [
for k, v in i : [
for l in v :
{
dataset_id = l.dataset_id
project_id = l.project_id
table_id = l.table_id
}]
if k == "views"
]])
content {
view {
dataset_id = access.value["dataset_id"]
project_id = access.value["project_id"]
table_id = access.value["table_id"]
}
}
}
}
VARIABLES.TF
variable "datasets" {
description = "A map of objects, including datasets IDs, roles and views"
type = map(list(map(list(map(string)))))
default = {}
}
continued....
Terraform.tfvars
datasets = {
dataset01 = [
{
roles = [
{
role="WRITER"
user_by_email="email_address"
group_by_email=""
special_group=""
}
]
views = [
{
dataset_id="MY_OTHER_DATASET"
project_id="my_other_project"
table_id="my_test_view"
}
]
}
]
dataset02 = [
{
roles = [
{
role="READER"
user_by_email=""
group_by_email="group"
special_group=""
}
]
views=[
{
dataset_id="MY_OTHER_DATASET"
project_id="my_other_project"
table_id="my_test_view_2"
}
]
}
]
}

Resources