Terraform - Iterate over a list generated from a for_each on a data block - iteration

UPDATE -> To all folks with this particular problem, I found the solution, it is at the end of this question.
UPDATE 2 -> The last solution that I presented here was WRONG, see the update at the end.
I am retrieving a list of cidr_blocks from a data block to use as a value on a aws_ec2_transit_gateway_route, but so far I have been unable to iterate through that list to get individual values and set it on the appropriate place.
The important piece of my data_block.tf looks like this:
data "aws_vpc" "account_vpc" {
provider = aws.dev
count = "${length(data.aws_vpcs.account_vpcs.ids)}"
id = element(tolist(data.aws_vpcs.account_vpcs.ids), 0)
}
data "aws_subnet_ids" "account_subnets" {
provider = aws.dev
vpc_id = element(tolist(data.aws_vpcs.account_vpcs.ids), 0)
}
data "aws_subnet" "cidrblocks" {
provider = aws.dev
for_each = data.aws_subnet_ids.account_subnets.ids
id = each.value
}
And the part where I intend to use it is this one, tgw_rt.tf:
resource "aws_ec2_transit_gateway_route" "shared-routes" {
provider = aws.shared
#count = length(data.aws_subnet.cidrblocks.cidr_block)
#destination_cidr_block = lookup(data.aws_subnet.cidrblocks.cidr_block[count.index], element(keys(data.aws_subnet.cidrblocks.cidr_block[count.index]),0), "127.0.0.1/32")
#destination_cidr_block = data.aws_subnet.cidrblocks.cidr_block[count.index]
#destination_cidr_block = [data.aws_subnet.cidrblocks.*.cidr_block]
destination_cidr_block = [for s in data.aws_subnet.cidrblocks : s.cidr_block]
/* for_each = [for s in data.aws_subnet.cidrblocks: {
destination_cidr_block = s.cidr_block
}] */
#destination_cidr_block = [for s in data.aws_subnet.cidrblocks : s.cidr_block]
#destination_cidr_block = data.aws_subnet.cidrblocks.cidr_block[count.index]
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.fromshared.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.shared.id
}
The part in comments is what I have tried so far and nothing worked.
The error that is happening currently when using that uncommented part is
Error: Incorrect attribute value type
on modules/tgw/tgw_rt.tf line 20, in resource "aws_ec2_transit_gateway_route" "shared-routes":
20: destination_cidr_block = [for s in data.aws_subnet.cidrblocks : s.cidr_block]
|----------------
| data.aws_subnet.cidrblocks is object with 3 attributes
Inappropriate value for attribute "destination_cidr_block": string required.
I would really appreciate it if one of the terraform gods present here could shed some light on this problem.
SOLUTION - THIS IS WRONG Since it was complaining about it being an object with 3 attributes (3 Cidr blocks), to iterate i had to use this:
destination_cidr_block = element([for s in data.aws_subnet.cidrblocks : s.cidr_block], 0)
CORRECT SOLUTION The solution was to add a small part to #kyle suggestion, I had to use an object to represent the data and convert it to a map, you rock #kyle:
for_each = {for s in data.aws_subnet.cidrblocks: s.cidr_block => s}
destination_cidr_block = each.value.cidr_block
Thank you all in Advance

I haven't used data.aws_subnet, but I think you were close with your for_each attempt-
resource "aws_ec2_transit_gateway_route" "shared-routes" {
...
for_each = [for s in data.aws_subnet.cidrblocks: s.cidr_block]
destination_cidr_block = each.value
...
}

Related

How can we dynamically generate a list of map in terraform?

I have a list of rules which i want to generate at runtime as it depends on availability_domains where availability_domains is a list
availability_domains = [XX,YY,ZZ]
locals {
rules = [{
ad = XX
name = "service-XX",
hostclass = "hostClassName",
instance_shape = "VM.Standard2.1"
...
},{
ad = YY
name = "service-YY",
hostclass = "hostClassName",
instance_shape = "VM.Standard2.1"
...
}, ...]
}
Here, all the values apart from ad and name are constant. And I need rule for each availability_domains.
I read about null_resource where triggers can be used to generate this but i don't want to use a hack here.
Is there any other way to generate this list of map?
Thanks for help.
First, you need to fix the availability_domains list to be a list of strings.
availability_domains = ["XX","YY","ZZ"]
Assuming availability_domains is a local you just run a forloop on it.
locals {
availability_domains = ["XX","YY","ZZ"]
all_rules = {"rules" = [for val in local.availability_domains : { "ad" : val, "name" : "service-${val}" , "hostclass" : "hostClassName", "instance_shape" : "VM.Standard2.1"}] }
}
or if you dont want the top level name to the array then this should work as well
locals {
availability_domains = ["XX","YY","ZZ"]
rules = [for val in local.availability_domains : { "ad" : val, "name" : "service-${val}" , "hostclass" : "hostClassName", "instance_shape" : "VM.Standard2.1"}]
}

For loop with If condition in Terraform

I'm writing a module to create multiple S3 Buckets with all the related resources. Currently, I'm a little bit stuck on the server side encryption as I need to parametrize the KMS key id for a key that is not still created.
The variables passed to the module are:
A list of S3 buckets
A map with the KMS created
The structure of the S3 buckets is
type = list(object({
bucket = string
acl = string
versioning = string
kms_description = string
logging = bool
loggingBucket = optional(string)
logPath = optional(string)
}))
}
The structure of the KMS map is similar to
kms_resources = {
0 = {
kms_arn = (known after apply)
kms_description = "my-kms"
kms_id = (known after apply)
}
}
This variable is an output from a previous module that creates all the KMS. The output is created this way
output "kms_resources" {
value = {
for kms, details in aws_kms_key.my-kms :
kms => ({
"kms_description" = details.description
"kms_id" = details.key_id
"kms_arn" = details.arn
})
}
}
As you can see the idea is that, on the S3 variable, the user can select his own KMS key, but I'm struggling to retrieve the value. At this moment, resource looks like this
resource "aws_s3_bucket_server_side_encryption_configuration" "my-s3-buckets" {
count = length(var.s3_buckets)
bucket = var.s3_buckets[count.index].bucket
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = [
for k in var.kms_keys : k.kms_id
if k.kms_description == var.s3_buckets[count.index].kms_description
]
sse_algorithm = "aws:kms"
}
}
}
I thought it was gonna work but once terraform is giving me Inappropriate value for attribute "kms_master_key_id": string required.. I would also like that if the value does not exist the kms_master_key_id is set by default to aws/s3
The problem seems to be that you try to give a list as kms_master_key_id instead of just a string. The fact that you alternatively want to use "aws/s3" actually makes the fix quite easy:
kms_master_key_id = concat([
for k in var.kms_keys : k.kms_id
if k.kms_description == var.s3_buckets[count.index].kms_description
], ["aws/s3"])[0]
That way you first get a list with keys / that key that matches the description and then append the default s3 key. Afterwards you simply pick the first element of the list, either the first actual key or if none matched you pick the default key.

Terraform for_each loop. Invalid index

I am facing a problem with for_each looping in terraform.
I have a azure resource for managed_keys as follow:
resource "azurerm_storage_account_customer_managed_key" "storage-managed-key" {
for_each = toset(var.key-name)
key_name = "Key-Client-${each.value}"
key_vault_id = azurerm_key_vault.tenantsnbshared.id
key_version = azurerm_key_vault_key.client-key[each.value].version
storage_account_id = azurerm_storage_account.storage-foreach[each.value].identity.0.principal_id
depends_on = [azurerm_key_vault_access_policy.storage]
}
I have a variable named key-name and a storage-account storage-foreach, both of them have a list(string) with some values.
My aim is to be able to loop through those 2 variables and encrypt the storage account with the respective key.
but if I run my code, I get this error:
Error: Invalid index
on main.tf line 173, in resource "azurerm_storage_account_customer_managed_key" "storage-managed-key":
173: storage_account_id = azurerm_storage_account.storage-foreach[each.value].identity.0.principal_id
|----------------
| azurerm_storage_account.storage-foreach is object with 4 attributes
| each.value is "key-name"
The given key does not identify an element in this collection value.
EDIT:
resource "azurerm_storage_account" "storage-foreach" {
for_each = toset(var.storage-foreach)
access_tier = "Hot"
account_kind = "StorageV2"
account_replication_type = "LRS"
account_tier = "Standard"
location = var.location
name = each.value
resource_group_name = azurerm_resource_group.tenant-testing-hamza.name
identity {
type = "SystemAssigned"
}
lifecycle {
prevent_destroy = false
}
}
Key vault access policies:
resource "azurerm_key_vault_access_policy" "storage" {
for_each = var.storage-foreach
key_vault_id = azurerm_key_vault.tenantsnbshared.id
tenant_id = "<tenant-id>"
object_id = azurerm_storage_account.storage-foreach[each.value].identity.0.principal_id
key_permissions = ["get", "Create", "List", "Restore", "Recover", "Unwrapkey", "Wrapkey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "Delete"]
secret_permissions = ["get", "set", "list", "delete", "recover"]
depends_on = [azurerm_key_vault.tenantsnbshared]
}
variable "storage-foreach" {
type = map(string)
default = { "<name1>" = "storage1", "<name2>" = "storage2", "<name3>" = "storage3", "<name4>" = "storage4"}
}
variable "key-name" {
type = map(string)
default = {"<name1>" = "key1", "<name2>" = "<key2>", "name3" = "<key3>", "<name4>" = "key4"}
}
this error get repeated for each element I have in my key-name variable.
I tried the some thing but using a count instead of a for_each and it works just fine, but the problem I had with that, was if I wanted to delete the first storage account and the first key, it automatically destroy all the element coming after to then recreate them, and is not something I wanted to do.
Is there anyone who can help me to understand this error and how to fix it please?
I'm assuming the lists with keys and storage accounts are of the same length, and that you want to encrypt storage account number i with key number i. You can either use "old style", index-based loops or zip the two lists into a single list of tuples, and then iterate over the zipped list.
Solution 1: using index-based iteration
This solution does not use for_each but the meta-argument count. This way of iterating over resources in Terraform predates the newer for_each style.
resource "azurerm_storage_account_customer_managed_key" "storage-managed-key" {
count = length(var.key-name)
key_name = azurerm_key_vault_key.client-key[var.key-name[count.index]].name
key_vault_id = azurerm_key_vault_key.client-key[var.key-name[count.index]].key_vault_id
key_version = azurerm_key_vault_key.client-key[var.key-name[count.index]].version
storage_account_id = azurerm_storage_account.storage-foreach[var.storage-foreach[count.index]].identity.0.principal_id
depends_on = [azurerm_key_vault_access_policy.storage]
}
I've taken the liberty to replace the explicit key name and key vault ID by references to the attributes of your key resource.
Solution 2: using combined structure
Here, the idea is to create a structure that you can iterate over, and of which the elements combine key name and the storage account names. There's multiple ways of doing this. The easiest way is probably to "misuse" a map and treat it as a list of tuples, as you can then simply use zipmap.
resource "azurerm_storage_account_customer_managed_key" "storage-managed-key" {
for_each = zipmap(var.storage-foreach, var.key-name)
key_name = azurerm_key_vault_key.client-key[each.value].name
key_vault_id = azurerm_key_vault_key.client-key[each.value].key_vault_id
key_version = azurerm_key_vault_key.client-key[each.value].version
storage_account_id = azurerm_storage_account.storage-foreach[each.key].identity.0.principal_id
depends_on = [azurerm_key_vault_access_policy.storage]
}
Note that I, perhaps confusingly, chose var.storage-foreach to be the keys of the object. Picking the keys as the map keys would make it impossible to use the same key to encrypt multiple storage accounts. Since storage-foreach is already used to index Terraform resources, I also already know these adhere to the Terraform naming restrictions.
What's the difference?
In solution 1, your azurerm_storage_account_customer_managed_key resource names are integer-based. Re-ordering elements in the lists may cause Terraform to destroyed and re-create resources. This is not the case for solution 2, which is why I usually prefer solution 2. However, in this case solution 1 may have the advantage of being more straight-forward.
A suggestion...
If possible, I would suggest to re-evaluate how you define your variables. It likely makes more sense to ask for list of objects combining key name and storage accounts in the first place; then you can basically use solution 2 without the call to zipmap. It's almost never a good idea to have an implicit dependency between two variables like this - these lists have to have the same length, and implicitly, the contents of the lists are connected by virtue of having the same index.

Terraform conditions in a module

I am trying to create some simple logic when calling applicationg gateway module.
When creating WAF v2 application gateway I want to specify more attributes that simple application gateway can't handle and they won't be described.
resource "azurerm_application_gateway" {
name = var.appgatewayname
resource_group_name = data.azurerm_resource_group.rg.name
location = data.azurerm_resource_group.rg.location
......................
waf_configuration {
enabled = "${length(var.waf_configuration) > 0 ? lookup(var.waf_configuration, "enabled", "") : null }"
firewall_mode = "${length(var.waf_configuration) > 0 ? lookup(var.waf_configuration, "firewall_mode", "") : null }"
............
Calling module:
module "GWdemo" {
source = "./...."
sku-name = "WAF_v2"
sku-tier = "WAF_v2"
sku-capacity = 1
waf-configuration = [
{
enabled = true
firewall_mode = "Detection"
}
Am I thinking right that if waf-configuration map is specified it should specify following settings is applied and if not than null?
When working with Terraform we often want to reframe problems involving a conditional test into problems involving a collection that may or may not contain elements, because the Terraform language features are oriented around transforming collections into configuration on an element-by-element basis.
In your case, you have a variable that is already a list, so it could work to just ensure that its default value is an empty list rather than null, and thus that you can just generate one waf_configuration block per element:
variable "waf_configuration" {
type = list(object({
enabled = bool
firewall_mode = string
}))
default = []
}
Then you can use a dynamic block to generate one waf_configuration block per element of that list:
dynamic "waf_configuration" {
for_each = var.waf_configuration
content {
enabled = waf_configuration.value.enabled
firewall_mode = waf_configuration.value.firewall_mode
}
}
Although it doesn't seem to apply to this particular example, another common pattern is a variable that can be set to enable something or left unset to disable it. For example, if your module was designed to take only a single optional WAF configuration, you might define the variable like this:
variable "waf_configuration" {
type = object({
enabled = bool
firewall_mode = string
})
default = null
}
As noted above, the best way to work with something like that in Terraform is to recast it as a list that might be empty. Because it's a common situation, there is a shorthand for it via splat expressions:
dynamic "waf_configuration" {
for_each = var.waf_configuration[*]
content {
enabled = waf_configuration.value.enabled
firewall_mode = waf_configuration.value.firewall_mode
}
}
When we apply the [*] operator to a value of a non-list/non-set type, Terraform will test to see if the value is null. If it is null then the result will be an empty list, while if it is not null then the result will be a single-element list containing that one value.
After converting to a list we can then use it in the for_each argument to dynamic in the usual way, accessing the attributes from that possible single element inside the content block. We don't need to repeat the conditionals for each argument because the content block contents are evaluated only when the list is non-empty.
I would encourage you to upgrade to Terraform v0.12.x and this should get a much easier to do. I would leverage the new dynamic block syntax to make the block optional based on whatever condition you need to use.
Here is a rough example, but should get you going in the correct direction.
dynamic "waf-configuration " {
for_each = length(var.waf_configuration) > 0 ? [] : [1]
content {
enabled = "${length(var.waf_configuration) > 0 ? lookup(var.waf_configuration, "enabled", "") : null }"
firewall_mode = "${length(var.waf_configuration) > 0 ? lookup(var.waf_configuration, "firewall_mode", "") : null }"
}
}

Can't get additionalSearchFields to work

jsonStoreInit = function(pSuccess, pFailure){
collections={};
collections['objects'] = {};
var options = {};
options.localKeyGen = false;
options.clear = false;
options.username = app.username;
options.password = app.password;
options.additionalSearchFields = {key: 'string'};
WL.JSONStore.init(collections, options)
.then(pSuccess)
.fail(pFailure);
};
putObject = function(pObject) {
var keyValue = pObject.getKey();
var object = {myObject : pObject.getKey()};
var options = {};
//options.additionalSearchFields = {key : keyValue};
WL.JSONStore.get("objects")
.add(object, options);
};
I'm on WL 6.0 FP 1
In the code sample above jsonStoreInit is what I use to init my store including the options.additionalSearchFields.
When I come to add the objects in the putObject funciton it works fine with the additionalSearchFields commented out, but when I uncomment it to add the additional fields I get an error
[wl.jsonstore] {"src":"store","err":21,"msg":"INVALID_ADD_INDEX_KEY","col":"objects","usr":"xxxx","doc":{},"res":{}}
When I look this error message up all I get is
21 INVALID_ADD_INDEX_KEY
Problem with additional search fields.
Which I had kinda figured ... can anyone provide any help on this ...
I don't need to you fix my code but if you could point me to a working example that would be excellent.
Many thanks, ownimage
The person that asked the question solved it, but I'm leaving this answer in case someone is wondering how to pass data that uses additionalSearchFields.
Example:
var data = {hello: 'world'};
WL.JSONStore.get('collection').add(data, {additionalSearchFields: {key: 'value'}})
The example assumes the collection was created with a search field for hello as string and an additional search field for key as string. It also assumes there's a collection initialized called collection.