Terraform - Output a map with key as value of for_each while resource creation - terraform

I am trying to solve below :
At first, Create resources based on the entries of the list provided to the resource. Below is the tf code, i have written for it :
resource "azurerm_key_vault" "application_key_vault" {
foreach = toset(var.app_names)
name = "${each.value}-kv"
resource_group_name = azurerm_resource_group.aks_resource_group.name
location = var.location
tenant_id = local.tenant_id
sku_name = "standard"
dynamic "contact" {
for_each = var.key_vault_contact_emails
content {
email = contact.value
}
}
network_acls {
default_action = "Deny"
bypass = "AzureServices"
virtual_network_subnet_ids = local.key_vault_allowed_subnets_set
}
tags = local.all_tags
depends_on = [azurerm_resource_group.aks_resource_group]
}
Now, lets say "app_names" has values ["app1", "app2", "app3"]. And the keyvaults created have ids ["id1", "id2", "id3"].
Is there a way i can create a map of above dynamically , which looks like this :
{
"app1" : "id1",
"app2" : "id2",
"app3" : "id3",
}
I tried using "output" something like this, but not able to figure out how should I get app_name which is used in creation of each keyvault :
output "application_app_name_by_key_vault_id_map" {
value = { for akv in azurerm_key_vault.application_key_vault : <not sure how to get app_name here> => akv.id }
}

Since you are creating the azurerm_key_vault resource with for_each, it acts like any other key value map. In other words, you can do the following:
output "application_app_name_by_key_vault_id_map" {
value = { for k, v in azurerm_key_vault.application_key_vault: k => v.id }
}

Related

Terraform Invalid for_each argument local will be known only after apply

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
}

Terraform - Reference a for_each resource from another for_each resource

I have a terraform file with the following contents:
resource "aws_iam_group" "developers" {
name = each.value
for_each = toset(var.groups)
}
resource "aws_iam_group_membership" "developers_team" {
name = "Developers Team"
users = [each.value]
for_each = toset(var.group_users)
group = aws_iam_group.developers.name
}
I would like to reference aws_iam_group from aws_iam_group_membership. How would I do that? The current terraform file is not working.
I tried this:
group = aws_iam_group.developers[each.value] //This will not work since it uses the for_each of
its own code block
The variable file is as below:
variable "groups" {
type = list(string)
default = [
"terraform_group1",
"terraform_group2",
"terraform_group3",
]
}
variable "group_users" {
type = list(string)
default = [
"terraform_test_user1",
"terraform_test_user2"
]
}
Edit:
I tried the below, but it is not working
resource "aws_iam_group_membership" "developers_team" {
name = "Developers Team"
users = [for group_user in var.group_users : group_user]
for_each = toset(var.groups)
group = aws_iam_group.developers[each.key]
}
Apparently, this is working:
resource "aws_iam_group" "developer" {
name = "truedeveloper"
}
resource "aws_iam_group_membership" "developers_team" {
name = "Developers_Team"
users = [for group_user in var.group_users : group_user]
for_each = toset(var.groups)
group = aws_iam_group.developer.name
}

Terraform issues when using for_each local variable created based on another local

I am trying to create azure keyvault secrets using locals which reference data resources. I am iterating over an array containing my environments and creating a list of maps where
each item is the set of secrets for a given environment.
Using another local, I then proceed to merge these maps into a single one by creating two lists, one with keys and another with values and then zipping them.
I finally use for_each on the second local to create the resource.
If I run my root module without creating the actual secret resources ("azurerm_key_vault_secret) and a second time with it, it all works fine.
If I try to do it all in one go, as I want to implement on my CI/CD I get the error message:
|Error: Invalid for_each argument
|on variables.tf line 239, in resource “azurerm_key_vault_secret” “example”:
│239: for_each = nonsensitive(local.example_map)
│ local.example_map will be known only after apply
|The “for_each” value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.
If anybody has any idea how i could make work. It seems to me that this data transformation within locals doesn't quite work.
Maybe I am going about the whole thing wrong. Any pointers would be highly appreciated.
Here is the code I am trying to make work:
variable "environment" {
default = [ "dev", "prod"]
}
locals {
example = distinct(flatten([
for namespace in var.environment : {
"${environment}-password1" = "${environment}-password",
"${environment}-password2" = "{\"connection_string\" : \"${data.azurerm_storage_account.storage_account_example["${environment}"].primary_connection_string}\"}",
"${environment}-password3" = "{\"client_id\" : \"${jsondecode("${data.azurerm_key_vault_secret.other_credentials["${environment}"].value}").clients["example"].client_id}\"}",
"${environment}-password4" = "{\"password\" : \"${data.azurerm_key_vault_secret.k_password.value}\"}",
"${environment}-password5" = "{\"azurestorageaccountname\" : \"${data.azurerm_storage_account.example.name}\", \"azurestorageaccountkey\" : \"${data.azurerm_storage_account.example.primary_access_key}\"}",
"${environment}-password6" = "{\"connection_string\" : \"${module.some_module.connection_string}\"}",
}]))
example_map = zipmap(
flatten(
[for item in local.example : keys(item)]
),
flatten(
[for item in local.example : values(item)]
)
)
}
resource "azurerm_key_vault_secret" "example" {
for_each = nonsensitive(local.example_map)
name = each.key
value = each.value
key_vault_id = module.keyvault.id
content_type = "password"
}
Here is the data structures created by local.example and local.example_map
"example": {
"value": [
{
"dev-password1" = "dev-password",
"dev-password2" = "{\"connection_string\" : \"DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=blablablablblabalbalbalbalblablablablablalbalbalbl==;EndpointSuffix=foo.bar.net\"}",
"dev-password3" = "{\"client_id\" : \"myclientID\"}",
"dev-password4" = "{\"password\" : \"password123\"}",
"dev-password5" = "{\"azurestorageaccountname\" : \"somestorageaccount\", \"azurestorageaccountkey\" : \"XXXxxxxXXXXxxxxXXXxxxxxxe++++++NNNNNNNNNCCCccccccccccccccccc==}\"}",
"dev-password6" = "{\"connection_string\" : \"${module.some_module.connection_string}\"}"
},
{
"prod-password1" = "prod-password",
"prod-password2" = "{\"connection_string\" : \"DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=blablablablblabalbalbalbalblablablablablalbalbalbl==;EndpointSuffix=foo.bar.net\"}",
"prod-password3" = "{\"client_id\" : \"myclientID\"}",
"prod-password4" = "{\"password\" : \"password123\"}",
"prod-password5" = "{\"azurestorageaccountname\" : \"somestorageaccount\", \"azurestorageaccountkey\" : \"XXXxxxxXXXXxxxxXXXxxxxxxe++++++NNNNNNNNNCCCccccccccccccccccc==}\"}",
"prod-password6" = "{\"connection_string\" : \"DefaultEndpointsProtocol=https;AccountName=yetanotherone;AccountKey=blablablablblabalbalbalbalblablablablablalbalbalbl==;EndpointSuffix=foo.bar.net\"}"
}
]
}
"example_map": {
"value": {
"dev-password1" = "dev-password",
"dev-password2" = "{\"connection_string\" : \"DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=blablablablblabalbalbalbalblablablablablalbalbalbl==;EndpointSuffix=foo.bar.net\"}",
"dev-password3" = "{\"client_id\" : \"myclientID\"}",
"dev-password4" = "{\"password\" : \"password123\"}",
"dev-password5" = "{\"azurestorageaccountname\" : \"somestorageaccount\", \"azurestorageaccountkey\" : \"XXXxxxxXXXXxxxxXXXxxxxxxe++++++NNNNNNNNNCCCccccccccccccccccc==}\"}",
"dev-password6" = "{\"connection_string\" : \"DefaultEndpointsProtocol=https;AccountName=yetanotherone;AccountKey=blablablablblabalbalbalbalblablablablablalbalbalbl==;EndpointSuffix=foo.bar.net\"}"
"prod-password1" = "prod-password",
"prod-password2" = "{\"connection_string\" : \"DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=blablablablblabalbalbalbalblablablablablalbalbalbl==;EndpointSuffix=foo.bar.net\"}",
"prod-password3" = "{\"client_id\" : \"myclientID\"}",
"prod-password4" = "{\"password\" : \"password123\"}",
"prod-password5" = "{\"azurestorageaccountname\" : \"somestorageaccount\", \"azurestorageaccountkey\" : \"XXXxxxxXXXXxxxxXXXxxxxxxe++++++NNNNNNNNNCCCccccccccccccccccc==}\"}",
"prod-password6" = "{\"connection_string\" : \"DefaultEndpointsProtocol=https;AccountName=yetanotherone;AccountKey=blablablablblabalbalbalbalblablablablablalbalbalbl==;EndpointSuffix=foo.bar.net\"}"
},
"type": [
"object",
{
"dev-password1": "string",
"dev-password2": "string",
"dev-password3": "string",
"dev-password4": "string",
"dev-password5": "string",
"dev-password6": "string",
"prod-password1": "string",
"prod-password2": "string",
"prod-password3": "string",
"prod-password4": "string",
"prod-password5": "string",
"prod-password6": "string",
}
]
}
Also what confuses me the most is that if I work with the following data structure, which is hard coding instead of doing the first transformation based on namespaces. The entry getting information from another module doesn't cause any problems and it all works wonderfully.
locals {
hardcoding_namespaces = {
"dev-password1" = "dev-password"
"dev-password2" = "{\"connection_string\" : \"${data.azurerm_storage_account.storage_account_example["dev"].primary_connection_string}\"}"
"dev-password3" = "{\"connection_string\" : \"${module.some_module.connection_string}\"}"
"prod-password1" = "prod-password"
"prod-password2" = "{\"connection_string\" : \"${data.azurerm_storage_account.storage_account_example["prod"].primary_connection_string}\"}"
"prod-password3" = "{\"connection_string\" : \"${module.some_module.connection_string}\"}"
}
}
resource "azurerm_key_vault_secret" "another_example" {
for_each = local.hardcoding_namespaces
name = each.key
value = each.value
key_vault_id = module.keyvault.id
content_type = "password"
}
if the resulting data structure is the same, why for_each works for one and not for the other?
[1]: https://i.stack.imgur.com/cTq5f.png
from the doc
Sensitive values, such as sensitive input variables, sensitive outputs, or sensitive resource attributes, cannot be used as arguments to for_each. The value used in for_each is used to identify the resource instance and will always be disclosed in UI output, which is why sensitive values are not allowed. Attempts to use sensitive values as for_each arguments will result in an error.(visite
https://www.terraform.io/language/meta-arguments/for_each#limitations-on-values-used-in-for_each)
Keys () will always return a sensitive value if the input is sensitive, so instead try the following :
example_map = zipmap(
flatten(
[for item,value in local.example : item]
),
flatten(
[for item, value in local.example : value]
)
)
This is probably because of module.some_module.connection_string. You can't use dynamic values in for_each. As the error message says, you have to use target to first create those dynamic resources, and then your for_each will work.

Terraform access map

I am trying to access all groups and create groups in the below terraform code. But I am facing error This object does not have an attribute named "groups". Is there any logic I am missing here in the resource "og" "example"
for_each=toset(flatten(local.instances[*].groups)). Thanks
locals {
instances = {
test1 = {
baseUrl = "url1"
subDomain = "sd1"
groups = [
"app1",
"app2",
],
}
test2 = {
baseUrl = "url2"
subDomain = "sd2"
groups = [
"t1",
"t2",
],
}
}
}
resource "og" "example" {
for_each = toset(flatten(local.instances[*].groups))
name = each.value
description = "${each.value}-access"
}
Your local variable is a map, not a list. So it should be:
for_each = toset(flatten(values(local.instances)[*].groups))

How to handle empty list for dynamic block

This is regarding an Azure resource, app_service, but I think it’s a more general HCL question…
You can specify IP restrictions to an app_service using a dynamic block e.g.
locals {
ip_addresses = [ "192.168.250.1" ]
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "West Europe"
}
resource "azurerm_app_service_plan" "example" {
name = "example-appserviceplan"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
sku {
tier = "Standard"
size = "S1"
}
}
resource "azurerm_app_service" "example" {
name = "example-app-service"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
app_service_plan_id = azurerm_app_service_plan.example.id
site_config {
dotnet_framework_version = "v4.0"
scm_type = "LocalGit"
}
app_settings = {
"SOME_KEY" = "some-value"
}
connection_string {
name = "Database"
type = "SQLServer"
value = "Server=some-server.mydomain.com;Integrated Security=SSPI"
}
dynamic "ip_restriction" {
for_each = toset(local.ip_addresses)
content {
ip_address = each.value
}
}
}
However, to remove the restrictions you need to explicit assign ip_restriction to the empty list, i.e.
resource "azurerm_app_service" "example" {
...
ip_restriction = []
}
What I don’t see is how to do this conditionally - if I make two resources and have those conditional my app_service will be created/destroyed whereas I need it updated in place.
I'm afraid that the dynamic block does not support an empty list when using a conditional expression. Read more reference here.
After my validation, the conditional expression like for_each = var.some_variable == "" ? [] : [1] does not work when var.some_variable set to null but this could work seperately when for_each = var.some_variable and var.some_variable set to null.
So, in this case, as the answer from #rkm, you can use the for loop like this working sample for me.
variable "ip_restrictions" {
default = [
# {
# ip_address = "1.1.1.1/32"
# virtual_network_subnet_id = null
# subnet_id = null
# name = "aaa"
# priority = 110
# action = "Allow"
# },
# {
# ip_address = "2.2.2.2/32"
# virtual_network_subnet_id = null
# subnet_id = null
# name = "bbb"
# priority = 112
# action = "Allow"
# },
]
}
resource "azurerm_app_service" "example" {
name = "nn-example-app-service"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
app_service_plan_id = azurerm_app_service_plan.example.id
site_config {
ip_restriction = [
for s in var.ip_restrictions :
{
ip_address = s.ip_address
virtual_network_subnet_id = s.virtual_network_subnet_id
subnet_id = s.subnet_id
name = s.name
priority = s.priority
action = s.action
}
]
}
}
This is special terraform syntax called Attributes as Blocks. Resource arguments defined using nested block syntax implicitly define a fixed collection of objects and thus in order to specify zero objects, we should explicitly set empty list. And these two forms cannot be mixed.
With that said, terraform supports an argument syntax too (even though they recommend using block syntax for simple cases for readability):
example = [
for name in var.names: {
foo = name
}
]
Just in case anyone lands here looking for how to return a blank dynamic configuration block (instead of an input parameter like the OP), you can make the iteration list used by the for_each to be blank, and use conditionals in the content to set everything to null.
For example, if you have a dynamic inline_policy for AWS iam_role and you pass your inline json with a variable name policy_documents, you can first combine them into a single json document, then set a local variable to either a blank or what was passed in:
data "aws_iam_policy_document" "combined_policy" {
source_policy_documents = var.policy_documents
}
locals {
policy_documents = length(var.policy_documents) == 0 ? [""] : [data.aws_iam_policy_document.combined_policy.json]
}
Now you can use local.policy_documents as the iterator for the dynamic block and the blank ensures the dynamic block will always generate something. To produce a blank inline_policy, you can test if the iterator is blank and set everything to null.
resource "aws_iam_role" "default" {
... #(other parameters ommitted)
dynamic "inline_policy" {
for_each = local.policy_documents
content {
name = length(inline_policy.value) == 0 ? null : "custom_inline_policy"
policy = length(inline_policy.value) == 0 ? null : inline_policy.value
}
}
}
In the case where no inline policy is passed in, the dynamic block will generate a blank inline_policy {}:
+ resource "aws_iam_role" "default" {
+ arn = (known after apply)
+ assume_role_policy = jsonencode(
{
+ Statement = [
+ {
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ Service = "ec2.amazonaws.com"
}
+ Sid = ""
},
]
+ Version = "2012-10-17"
}
)
+ create_date = (known after apply)
+ id = (known after apply)
+ inline_policy {}
}
I discovered how important a blank inline_policy is by accident when someone manually added an inline policy in the console, but the dynamic inline_policy did not notice it when I ran a terraform plan. If there was no inline_policy to start with, then the dynamic block never runs and so terraform doesn't check it. If you force the dynamic block to run even if the inline_policy is blank, terraform will ensure it is blank and delete the manually added inline policy. (Look here and here for terraform documentation and here for a good discussion at hashicorp about blank dyanamic blocks.)

Resources