I'm trying to deploy a list of Azure Front Doors and their custom https configuration resources. I have a list of Azure Front Door resources which are deployed like this pseudo code. They work correctly (although without custom https configuration)
resource "azurerm_frontdoor" "front_door" {
count = length(local.frontdoors)
... config
}
I then try and add some terraform to create the custom https configuration as described here and useterraform azure frontdoor custom https config docs the following fragment:
resource "azurerm_frontdoor_custom_https_configuration" "custom_https_configuration" {
count = length(local.frontdoors)
for_each = { for frontend in azurerm_frontdoor.front_door[count.index].frontend_endpoint : frontend.id => frontend_id }
frontend_endpoint_id = each.value.frontend_id
custom_https_provisioning_enabled = each.key != "front_door" ? local.frontend_https_configurations[each.key].custom_https_provisioning_enabled : false
dynamic "custom_https_configuration" {
for_each = (each.key != "front_door" ? local.frontend_https_configurations[each.key].custom_https_provisioning_enabled : false) ? [1] : []
content {
certificate_source = "AzureKeyVault"
azure_key_vault_certificate_secret_name = XXXX
azure_key_vault_certificate_secret_version = XXXX
azure_key_vault_certificate_vault_id = XXXX
}
}
}
I'm getting this syntax error:
Error: Invalid combination of "count" and "for_each"
if i try and remove the count, and use the for_each structure instead:
resource "azurerm_frontdoor_custom_https_configuration" "custom_https_configuration" {
for_each = {
for frontdoor in azurerm_frontdoor.front_door :
[
for key, value in frontdoor.frontend_endpoint: value.frontend.id => frontend_id
]
}
frontend_endpoint_id = each.value.frontend_id
custom_https_provisioning_enabled = each.key != "front_door" ? local.frontend_https_configurations[each.key].custom_https_provisioning_enabled : false
dynamic "custom_https_configuration" {
for_each = (each.key != "front_door" ? local.frontend_https_configurations[each.key].custom_https_provisioning_enabled : false) ? [1] : []
content {
certificate_source = "AzureKeyVault"
azure_key_vault_certificate_secret_name = XXXX
azure_key_vault_certificate_secret_version = XXXX
azure_key_vault_certificate_vault_id = XXXX
}
}
}
I get this error instead:
Error: Invalid 'for' expression
on main.tf line 25, in resource "azurerm_frontdoor_custom_https_configuration" "custom_https_configuration":
173: for_each = {
174: for frontdoor in azurerm_frontdoor.front_door :
175: [
176: for key, value in frontdoor.frontend_endpoint: value.frontend.id => frontend_id
177: ]
178: }
Key expression is required when building an object.
How can i have a nested loop so that i can successfully deploy a f
When you need to nest for loops, you need to use the flatten function (for lists) or the merge function in combination with the ... list expansion operator (for maps).
Basically, like so:
// To make a list
for_each = flatten([
for idx1, val1 in var.list1:
[
for idx2, val2 in val2.list_field:
// Here is where you construct whatever value/object for each element
]
])
// To make a list
for_each = merge([
for key1, val1 in var.map1:
{
for key2, val2 in val1.map_field:
// Some key/value pair, such as:
"${key1}-${key2}" => val2
}
]...)
You also have your map comprehension key/value reversed. Try this:
for_each = merge([
for idx, frontdoor in azurerm_frontdoor.front_door :
{
for key, value in frontdoor.frontend_endpoints:
"${idx}-${key}" => {
endpoint_key = key
endpoint_id = value
}
}
]...)
And now within your resource, you can use each.value.endpoint_key and each.value.endpoint_id.
Related
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 }
}
I am defining network interface inside the VMSS AZURE Terraform
Well what i want is disable or enable the load balancer backend address pool id if $app != "api"
In other words if app is api then only add that parameter and attach pool id.
Also, How to enable or disable entire resource lets say i want to diable or enable the network interface block in whole.
Thanks in advance for helping.
load_balancer_backend_address_pool_ids = [var.backend_address_pool_id] #enable only when var.app is api.
network_interface {
name = "${var.app}-vmss-nic"
primary = true
ip_configuration {
name = "internal"
primary = true
subnet_id = var.pvt_subnet_1_id
load_balancer_backend_address_pool_ids = [var.backend_address_pool_id]
}
}
This can be done by using the ternary operator [1] and a combination of the dynamic block [2] with for_each meta-argument [3] for the network interface. To achieve this you would do the following:
dynamic "network_interface" {
for_each = var.app == "api" ? [1] : [0]
content {
name = "${var.app}-vmss-nic"
primary = true
ip_configuration {
name = "internal"
primary = true
subnet_id = var.pvt_subnet_1_id
load_balancer_backend_address_pool_ids = [var.backend_address_pool_id]
}
}
}
[1] https://developer.hashicorp.com/terraform/language/expressions/conditionals
[2] https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks
[3] https://developer.hashicorp.com/terraform/language/meta-arguments/for_each
you would need to test this but you could possible do something like
load_balancer_backend_address_pool_ids = var.app == "api" ? [var.backend_address_pool_id] : null
or incase the provider doesnt support null and just expects an empty list
load_balancer_backend_address_pool_ids = var.app == "api" ? [var.backend_address_pool_id] : []
When I have this kind of situation, I Always use this structure:
dynamic "azure_devops_repo" {
for_each = var.azure_devops_repo == null ? {} : var.azure_devops_repo
iterator = devops
content {
account_name = devops.value.account_name
branch_name = devops.value.branch_name
project_name = devops.value.project_name
repository_name = devops.value.repository_name
root_folder = devops.value.root_folder
tenant_id = lookup(devops.value, "tenant_id", null)
}
}
As you see this is like an optional block!
So i have a terraform variable type list(string) that is called zones and contains
zones = [
"example.com",
"example2.com",
"example3.com",
...
]
and i m using data cloudflare_zones resource to fetch all zones info
data "cloudflare_zones" "zones" {
for_each = toset(var.zones)
filter {
name = each.value
}
}
Output for each of the zones
data.cloudflare_zones.zones["example.com"]
{
"filter" = tolist([
{
"account_id" = ""
"lookup_type" = "exact"
"match" = ""
"name" = "example.com"
"paused" = false
"status" = ""
},
])
"id" = "9f7xxx3xxxx"
"zones" = tolist([
{
"id" = "e13xxxx"
"name" = "example.com"
},
])
}
To fetch the zone id you need to parse data.cloudflare_zones as below:
data.cloudflare_zones.zones["example.com"].zones[0].id
What i want to create then is a variable that will be an object with all the zones names as keys and zone ids ad values, so i can use them in other resources.
For Example:
zones_ids =
{
"example.com" = "xxxzone_idxxx",
"example2.com" = "xxxzone_id2xxx",
"example3.com" = "xxxzone_id3xxx",
...
}
I would like to achieve this inside locals block
locals {
...
}
That should be easy:
locals {
zones_ids = { for k,v in data.cloudflare_zones.zones: k => v.zones[0].id }
}
Or alternatively:
locals {
zones_ids = { for k,v in data.cloudflare_zones.zones: v.zones[0].name => v.zones[0].id }
}
The above answers helped me here but did not give me the final answer. For anyone looking to update A records for cloudflare with a list of domain names that gets the zone_ids for you. Here is how I did it:
locals {
domains = ["example1.com", "example2.com"]
}
data "cloudflare_zones" "zones" {
count = "${length(local.domains)}"
filter {
name = "${element(local.domains, count.index)}"
}
}
locals {
zones_ids = { for k,v in data.cloudflare_zones.zones: k => v.zones[0].id }
}
resource "cloudflare_record" "redir-A-record" {
for_each = local.zones_ids
zone_id = each.value
name = "#"
value = "24.1.1.1"
type = "A"
proxied = false
}
resource "cloudflare_record" "redir-A-record-www" {
for_each = local.zones_ids
zone_id = each.value
name = "www"
value = "24.1.1.1"
type = "A"
proxied = false
}
Getting output for these values did not seem to work based on the above answer. This could of just been my confusion but I wanted to print out the zone_id for each domain. I found since it is a tuple it requires the use of a number instead of a name so I was required to do the following to get the proper output:
# Get information for Domain 1
output "Domain_Information" {
value = data.cloudflare_zones.zones[0].zones[0].id
}
# Get information for Domain 2
output "Domain_Information2" {
value = data.cloudflare_zones.zones[1].zones[0].id
}
There is a way to loop this in output with Terraform but in my case I only had 2 domains and did not need to spend additional time on this.
Now when I want to spin up a server in AWS and have multiple domains point to 1 IP address this code works.
This line here posted by #Marko E was the solution to my issues for looping and saving the data that could be used later.:
locals {
zones_ids = { for k,v in data.cloudflare_zones.zones: k => v.zones[0].id }
}
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
}
How can I iterate over the JSON rendered data.aws_iam_policy_document documents within an aws_iam_policy?
data "aws_iam_policy_document" "role_1" {
statement {
sid = "CloudFront1"
actions = [
"cloudfront:ListDistributions",
"cloudfront:ListStreamingDistributions"
]
resources = ["*"]
}
}
data "aws_iam_policy_document" "role_2" {
statement {
sid = "CloudFront2"
actions = [
"cloudfront:CreateInvalidation",
"cloudfront:GetDistribution",
"cloudfront:GetInvalidation",
"cloudfront:ListInvalidations"
]
resources = ["*"]
}
}
variable "role_policy_docs" {
type = list(string)
description = "Policies associated with Role"
default = [
"data.aws_iam_policy_document.role_1.json",
"data.aws_iam_policy_document.role_2.json",
]
}
locals {
role_policy_docs = { for s in var.role_policy_docs: index(var.role_policy_docs, s) => s}
}
resource "aws_iam_policy" "role" {
for_each = local.role_policy_docs
name = format("RolePolicy-%02d", each.key)
description = "Custom Policies for Role"
policy = each.value
}
resource "aws_iam_role_policy_attachment" "role" {
for_each = { for p in aws_iam_policy.role : p.name => p.arn }
role = aws_iam_role.role.name
policy_arn = each.value
}
This example has been reduced down to the very basics. The policy documents are dynamically generated with the source_json and override_json conventions. I cannot simply combine the statements into a single policy document.
Terraform Error:
Error: "policy" contains an invalid JSON policy
on role.tf line 35, in resource "aws_iam_policy" "role":
35: policy = each.value
This:
variable "role_policy_docs" {
type = list(string)
description = "Policies associated with Role"
default = [
"data.aws_iam_policy_document.role_1.json",
"data.aws_iam_policy_document.role_2.json",
]
}
Is literally defining those default values as strings, so what you're getting is this:
+ role_policy_docs = {
+ 0 = "data.aws_iam_policy_document.role_1.json"
+ 1 = "data.aws_iam_policy_document.role_2.json"
}
If you tried removing the quotations around the data blocks, it will not be valid because you cannot use variables in default definitions. Instead, assign your policy documents to a new local, and use that local in your for loop instead:
locals {
role_policies = [
data.aws_iam_policy_document.role_1.json,
data.aws_iam_policy_document.role_2.json,
]
role_policy_docs = {
for s in local.role_policies :
index(local.role_policies, s) => s
}
}