I am creating modules using for_each loop. I want to access a previous module from within the module block to get a variable but it stops me from doing so because of the cycle.
locals{
deployment_plan = ["a", "b", "c"]
}
module "tier" {
source = "./modules/deployment"
for_each = { for tier,data in local.tier_config : tier => data }
tier_cfg = each.value
predecessors = [module.tier[local.deployment_plan[index(local.deployment_plan, each.key) - 1]].last_release_phase] : []
}
I see an error when assigning predecessor for the module as I am accessing module in cycle. Although I want to access the previous module.
You can't access module.tier before it is created. Thus you get the error. In your case you have to create 3 modules, for each of your deployment_plan:
locals{
deployment_plan = ["a", "b", "c"]
}
module "tier_a" {
source = "./modules/deployment"
for_each = { for tier,data in local.tier_config : tier => data }
tier_cfg = each.value
predecessors = []
}
module "tier_b" {
source = "./modules/deployment"
for_each = { for tier,data in local.tier_config : tier => data }
tier_cfg = each.value
predecessors = [module.tier_a.last_release_phase]
}
module "tier_c" {
source = "./modules/deployment"
for_each = { for tier,data in local.tier_config : tier => data }
tier_cfg = each.value
predecessors = [module.tier_b.last_release_phase]
}
Related
I'm creating multiple Google Cloud Projects using a module which creates a custom service account for each one and provides that as an output eg
module "app_projects" {
count = var.number_application_projects
etc
}
I then have a list of IAM roles I want to assign to each Project Service account like this:
sa_roles = ["roles/run.developer","roles/appengine.deployer"]
resource "google_project_iam_member" "proj_sa_iam_roles" {
count = var.number_application_projects
for_each = toset(var.sa_roles)
project = module.app_projects[count.index].project_id
role = each.value
member = "serviceAccount:${module.app_projects[count.index].service_account_email}"
}
This runs into the error about "count" and "for_each" being mutually-exclusive, and I can't for the life of me figure out what to use instead, any help would be appreciated!
You can probably use setproduct, but the standard method is using flatten.
They even have a section tailored for similar use cases. Flattening nested structures for for_each.
Here is an example, that doesn't use your exact resource, but should be instructive and you can actually run it to test it out.
modules/app-project/variables.tf
variable "name" {}
modules/app-project/outputs.tf
output "name" {
value = var.name
}
modules/member/variables.tf
variable "project" {}
variable "role" {}
modules/member/outputs.tf
output "project_role" {
value = "${var.project}-${var.role}"
}
main.tf
locals {
roles = ["rad", "tubular"]
}
module "app_project" {
source = "./modules/app-project"
count = 2
name = "app-project-${count.index}"
}
module "project_role" {
source = "./modules/member"
for_each = { for pr in flatten([for p in module.app_project[*] : [
for r in local.roles : {
app_project_name = p.name
role = r
}]
]) : "${pr.app_project_name}-${pr.role}" => pr }
project = each.value.app_project_name
role = each.value.role
}
output "project_roles" {
value = values(module.project_role)[*].project_role
}
terraform plan output
Changes to Outputs:
+ project_roles = [
+ "app-project-0-rad",
+ "app-project-0-tubular",
+ "app-project-1-rad",
+ "app-project-1-tubular",
]
In your case specifically, I think something like this would work:
resource "google_project_iam_member" "proj_sa_iam_roles" {
for_each = { for i, pr in flatten([for p in module.app_project[*] : [
for r in var.sa_roles : {
app_project = p
role = r
}]
]) : "${i}-${pr.role}" => pr }
project = each.value.app_project.project_id
role = each.value.role
member = "serviceAccount:${each.value.app_project.service_account_email}"
}
I would like to create an AWS account with SSO Account Assignments in the same first terraform run without hit the for_each limitation with dynamic values that cannot be predicted during plan.
I've tried to separate the aws_organizations_account resource from aws_ssoadmin_account_assignment in completely separate TF module and also I tried to use depends_on between those resources and modules.
What is the simplest and correct way to fix this issue?
Terraform v1.2.4
AWS SSO Account Assignments Module
Closed Pull Request that did not fix this issue
main.tf file (aws module)
resource "aws_organizations_account" "account" {
name = var.aws_account_name
email = "${var.aws_account_name}#gmail.com"
tags = {
Name = var.aws_account_name
}
parent_id = var.aws_org_folder_id
}
data "aws_identitystore_group" "this" {
for_each = local.group_list
identity_store_id = local.identity_store_id
filter {
attribute_path = "DisplayName"
attribute_value = each.key
}
}
data "aws_identitystore_user" "this" {
for_each = local.user_list
identity_store_id = local.identity_store_id
filter {
attribute_path = "UserName"
attribute_value = each.key
}
}
data "aws_ssoadmin_instances" "this" {}
locals {
assignment_map = {
for a in var.account_assignments :
format("%v-%v-%v-%v", aws_organizations_account.account.id, substr(a.principal_type, 0, 1), a.principal_name, a.permission_set_name) => a
}
identity_store_id = tolist(data.aws_ssoadmin_instances.this.identity_store_ids)[0]
sso_instance_arn = tolist(data.aws_ssoadmin_instances.this.arns)[0]
group_list = toset([for mapping in var.account_assignments : mapping.principal_name if mapping.principal_type == "GROUP"])
user_list = toset([for mapping in var.account_assignments : mapping.principal_name if mapping.principal_type == "USER"])
}
resource "aws_ssoadmin_account_assignment" "this" {
for_each = local.assignment_map
instance_arn = local.sso_instance_arn
permission_set_arn = each.value.permission_set_arn
principal_id = each.value.principal_type == "GROUP" ? data.aws_identitystore_group.this[each.value.principal_name].id : data.aws_identitystore_user.this[each.value.principal_name].id
principal_type = each.value.principal_type
target_id = aws_organizations_account.account.id
target_type = "AWS_ACCOUNT"
}
main.tf (root)
module "sso_account_assignments" {
source = "./modules/aws"
account_assignments = [
{
permission_set_arn = "arn:aws:sso:::permissionSet/ssoins-0000000000000000/ps-31d20e5987f0ce66",
permission_set_name = "ReadOnlyAccess",
principal_type = "GROUP",
principal_name = "Administrators"
},
{
permission_set_arn = "arn:aws:sso:::permissionSet/ssoins-0000000000000000/ps-955c264e8f20fea3",
permission_set_name = "ReadOnlyAccess",
principal_type = "GROUP",
principal_name = "Developers"
},
{
permission_set_arn = "arn:aws:sso:::permissionSet/ssoins-0000000000000000/ps-31d20e5987f0ce66",
permission_set_name = "ReadOnlyAccess",
principal_type = "GROUP",
principal_name = "Developers"
},
]
}
The important thing about a map for for_each is that all of the keys must be made only of values that Terraform can "see" during the planning step.
You defined local.assignment_map this way in your example:
assignment_map = {
for a in var.account_assignments :
format("%v-%v-%v-%v", aws_organizations_account.account.id, substr(a.principal_type, 0, 1), a.principal_name, a.permission_set_name) => a
}
I'm not personally familiar with the aws_organizations_account resource type, but I'm guessing that aws_organizations_account.account.id is an attribute whose value gets decided by the remote system during the apply step (once the object is created) and so this isn't a suitable value to use as part of a for_each map key.
If so, I think the best path forward here is to use a different attribute of the resource that is defined statically in your configuration. If var.aws_account_name has a static value defined in your configuration (that is, it isn't derived from an apply-time attribute of another resource) then it might work to use the name attribute instead of the id attribute:
assignment_map = {
for a in var.account_assignments :
format("%v-%v-%v-%v", aws_organizations_account.account.name, substr(a.principal_type, 0, 1), a.principal_name, a.permission_set_name) => a
}
Another option would be to remove the organization reference from the key altogether. From what you've shared it seems like there is only one account and so all of these keys would end up starting with exactly the same account name anyway, and so that string isn't contributing to the uniqueness of those keys. If that's true then you could drop that part of the key and just keep the other parts as the unique key:
assignment_map = {
for a in var.account_assignments :
format(
"%v-%v-%v",
substr(a.principal_type, 0, 1),
a.principal_name,
a.permission_set_name,
) => a
}
I'm trying to iterate over a simple list of maps. Here's a segment of what my module code looks like:
resource "helm_release" "nginx-external" {
count = var.install_ingress_nginx_chart ? 1 : 0
name = "nginx-external"
repository = "https://kubernetes.github.io/ingress-nginx"
chart = "ingress-nginx"
version = var.nginx_external_version
namespace = "default"
lint = true
values = [
"${file("chart_values/nginx-external.yaml")}"
]
dynamic "set" {
for_each = { for o in var.nginx_external_overrides : o.name => o }
content {
name = each.value.name
value = each.value.value
}
}
}
variable "nginx_external_overrides" {
description = "A map of maps to override customizations from the default chart/values file."
type = any
}
And here's a snippet of how I'm trying to call it from terragrunt:
nginx_external_overrides = [
{ name = "controller.metrics.enabled", value = "false" }
]
When trying to use this in a dynamic block, I'm getting:
Error: each.value cannot be used in this context
A reference to "each.value" has been used in a context in which it
unavailable, such as when the configuration no longer contains the value in
its "for_each" expression. Remove this reference to each.value in your
configuration to work around this error.
Ideally, I would be able to pass any number of maps in nginx_external_overrides to override the settings in the yaml being passed, but am struggling to do so. Thanks for the help.
If you are using for_each in dynamic blocks, you can't use each. Instead, in your case, it should be set:
dynamic "set" {
for_each = { for o in var.nginx_external_overrides : o.name => o }
content {
name = set.value.name
value = set.value.value
}
}
For example here is a resource (very simplified) being deployed with for_each (the for_each isnt where Im having problems, I can do that all day - its trying to get the data in the ovf_network_map correctly interpolated is where Im having problems):
resource "vsphere_virtual_machine" "vmFromLocalOvf" {
for_each = var.customers[var.customer][var.idc].vms
...snip...
ovf_deploy {
local_ovf_path = "cucm_11.5_vmv8_v1.1.ovf"
ovf_network_map = {
for net in ["INSIDE", "OUTSIDE"]:
"eth${count.index}" => data.vsphere_network.net.id
}
For this simplified example, the goal there is to end up where ovf_network_map contains { "eth0" = data.vsphere_network.INSIDE.id, "eth1" = data.vsphere_network.OUTSIDE.id }
(obviously, that data objects will be further interpolated there, but hopefully the issue comes across here of what Im trying to accomplish).
There are 2 errors:
The "count" object can be used only in "resource" and "data" blocks, and only
when the "count" argument is set.
also
A data resource "vsphere_network" "net" has not been declared, Obviously my interpolation there is wrong. Hopefully the interpolation I need here is possible - Im probably going about this the wrong way - any ideas?
Edit to add: Ive been able to figure out the numeric counting for eth0, eth1 with this: eth${index(slice(var.customers[var.customer][var.idc].vms[each.key], 3, length(var.customers[var.customer][var.idc].vms[each.key]) - 1), net)}" => data.vsphere_network.net.id
So now all thats left - im stuck with trying to "double" interpolate the "net" there in data.vsphere_network.net.id as I get error A data resource "vsphere_network" "net" has not been declared
You won't get a count variable or a corresponding count.index in a for_each, so this can't work:
resource "vsphere_virtual_machine" "vmFromLocalOvf" {
for_each = var.customers[var.customer][var.idc].vms
...snip...
ovf_deploy {
local_ovf_path = "cucm_11.5_vmv8_v1.1.ovf"
ovf_network_map = {
for net in ["INSIDE", "OUTSIDE"]:
"eth${count.index}" => data.vsphere_network.net.id
}
It is possible to get a map from indexes to values as follows and then use each.key as you would have used count.index:
resource "vsphere_virtual_machine" "vmFromLocalOvf" {
for_each = zipmap(
range(length(var.customers[var.customer][var.idc].vms)),
var.customers[var.customer][var.idc].vms
)
...snip...
ovf_deploy {
local_ovf_path = "cucm_11.5_vmv8_v1.1.ovf"
ovf_network_map = {
for net in ["INSIDE", "OUTSIDE"]:
"eth${each.key}" => data.vsphere_network.net.id
}
Alain had the right idea on zipmap. But instead of a double interpolation (which isnt supported) I had to abstract my data out into a locals{} variable and thats where the zipmap resided:
data "vsphere_network" "networks" {
count = length(var.customers[var.customer][var.idc].networks)
name = "CUST-${substr(var.customer, 5, length(var.customer) - 4)}-UC-${var.customers[var.customer][var.idc].networks[count.index]}"
datacenter_id = data.vsphere_datacenter.dc.id
distributed_virtual_switch_uuid = data.vsphere_distributed_virtual_switch.dvs.id
}
locals {
netids = zipmap(var.customers[var.customer][var.idc].networks, data.vsphere_network.networks.*.id)
}
resource "vsphere_virtual_machine" "vmFromLocalOvf" {
for_each = var.customers[var.customer][var.idc].vms
name = "${substr(var.customer, 0, 4)}-${each.key}"
ovf_deploy {
local_ovf_path = "ov/${var.customers[var.customer][var.idc].vms[each.key].1}"
ovf_network_map = {
for net in slice(var.customers[var.customer][var.idc].vms[each.key], 3, length(var.customers[var.customer][var.idc].vms[each.key]) - 1) :
"eth${index(slice(var.customers[var.customer][var.idc].vms[each.key], 3, length(var.customers[var.customer][var.idc].vms[each.key]) - 1), net)}" => local.netids[net]
}
}
vapp {
properties = {
"DeploymentOption.value" = var.customers[var.customer][var.idc].vms[each.key].2
}
}
}
and
I am trying to work with terraform modules to create event subscription pointing to storage queue as an endpoint to it.
Below is the module
resource "azurerm_eventgrid_event_subscription" "events" {
name = var.name
scope = var.scope
subject_filter = var.subject_filter
storage_queue_endpoint = var.storage_queue_endpoint
}
and terraform is
module "storage_account__event_subscription" {
source = "../modules/event"
name = "testevent"
scope = test
subject_filter = {
subject_begins_with = "/blobServices/default/containers/test/blobs/in"
}
storage_queue_endpoint = {
storage_account_id = test
queue_name = test
}
}
Error message:
: subject_filter {
Blocks of type "subject_filter" are not expected here.
Error: Unsupported block type
on azure.tf line 90, in module "storage_account__event_subscription":
: storage_queue_endpoint {
Blocks of type "storage_queue_endpoint" are not expected here.
How do i parse the optional fields properly in terraform modules ?
In you module:
resource "azurerm_eventgrid_event_subscription" "events" {
name = var.name
scope = var.scope
subject_filter = {
subject_begins_with = var.subject_begins_with
}
storage_queue_endpoint = var.storage_queue_endpoint
}
Formatting is off here so make sure to run terraform fmt to account for my poor formatting. Also add the variable to the variables.tf file.
Your Terraform file:
module "storage_account__event_subscription" {
source = "../modules/event"
name = "testevent"
scope = test
subject_begins_with = "/blobServices/default/containers/test/blobs/in"
storage_queue_endpoint = {
storage_account_id = test
queue_name = test
}
}
You create the full structure in the module and then you assign the variables in the terraform file.
Anything that will have the same, or generally the same, value can have a default value set in the variables.tf as well so that you get smaller chunks in the TF file.