Use count instead of for_each for terraform resource - terraform

When deploying resources, the template terraform gave uses for_each. This poses as a problem as it will give
Error: Invalid for_each argument
│
│ on /home/baiyuc/workspaces/billow/src/GoAmzn-LambdaStackTools/configurations/terraform/sync.tf line 410, in resource "aws_route53_record" "subdomain_cert_validation":
│ 410: for_each = {
│ 411: for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
│ 412: name = dvo.resource_record_name
│ 413: record = dvo.resource_record_value
│ 414: type = dvo.resource_record_type
│ 415: }
│ 416: }
│ ├────────────────
│ │ aws_acm_certificate.cert.domain_validation_options is a set of object, known only after apply
The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.
Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge.
error when using terraform import.
I found a potential solution that suggests using count in this type of scenarios, but it didn't go into details. Anyone can give any details on how to do so?
The code of interest is for resource "aws_route53_record" "subdomain_cert_validation":
data "aws_route53_zone" "root_domain" {
name = "${var.root_domain}."
private_zone = false
}
resource "aws_acm_certificate" "cert" {
depends_on = [aws_route53_record.sub-zone]
domain_name = var.domain
validation_method = "DNS"
}
resource "aws_route53_zone" "core-domain" {
name = var.domain
count = var.root_domain == var.domain ? 0 : 1 # If the two are the same, do not create this resource.
tags = {
Environment = var.stack_tag
}
}
resource "aws_route53_record" "sub-zone" {
depends_on = [aws_route53_zone.core-domain]
zone_id = data.aws_route53_zone.root_domain.zone_id
name = var.domain
type = "NS"
ttl = "30"
count = var.root_domain == var.domain ? 0 : 1 # If the two are the same, do not create this resource.
records = var.root_domain == var.domain ? [] : [
aws_route53_zone.core-domain[0].name_servers[0],
aws_route53_zone.core-domain[0].name_servers[1],
aws_route53_zone.core-domain[0].name_servers[2],
aws_route53_zone.core-domain[0].name_servers[3],
]
}
resource "aws_route53_record" "subdomain_cert_validation" {
depends_on = [aws_acm_certificate.cert]
for_each = {
for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
type = each.value.type
ttl = 600
zone_id = var.root_domain == var.domain ? data.aws_route53_zone.root_domain.zone_id : aws_route53_zone.core-domain[0].zone_id
}
resource "aws_acm_certificate_validation" "core" {
certificate_arn = aws_acm_certificate.cert.arn
validation_record_fqdns = [for record in aws_route53_record.subdomain_cert_validation : record.fqdn]
}

this issue is pretty common when using iteration and is caused by trying to use keys that will be dynamically generated at apply time. You need to make sure that your keys are statically defined so they're known at apply time, the value of the map can then be dynamic. Some good reference, for this issue and solutions are here
for_each example and solution
for_each example and solution

Related

Why isn't terraform interpolation working?

So my goal is to have write a terraform code to deploy 3 resource groups in AZ dev, uat and prod with each having the following resources.
SQL Database
Key Vault
variable.tf
variable "resource_group_name" {
description = "deafault resource group"
type = list (string)
default = ["Test-dev","Test-uat","Test-prod"]
}
variable "storage_account_name" {
description = "name for storage account"
default = "test-storageact"
}
main.tf
resource "azurerm_resource_group" "resgrp" {
for_each = toset(var.resource_group_name)
name = "${var.resource_group_name}-rg-${each.value}"
location = var.location
tags = {
"Environment" = "${var.env}-${each.value}"
}
}
resource "azurerm_storage_account" "storageact" {
for_each = toset(var.resource_group_name)
name = "${var.storage_account_name}-${each.value}"
resource_group_name = azurerm_resource_group.resgrp["${each.value}"].location
location = azurerm_resource_group.resgrp["${each.value}"].name
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
"Environment" = "${var.env}-${each.value}"
}
}
error message
│ Error: Invalid template interpolation value
│
│ on main.tf line 3, in resource "azurerm_resource_group" "resgrp":
│ 3: name = "${var.resource_group_name}-rg-${each.value}"
│ ├────────────────
│ │ var.resource_group_name is list of string with 3 elements
│
│ on main.tf line 6, in resource "azurerm_resource_group" "resgrp":
│ 6: "Environment" = "${var.env}-${each.value}"
│ ├────────────────
│ │ var.env is map of string with 3 elements
│
│ Cannot include the given value in a string template: string required.
╵
Operation failed: failed running terraform plan (exit 1)
please any help will be greatly appreciated.
i tried using type = map(string) in the variable but still gave me an error.
There are a few problems with the code.
var.resource_group_name is a list containing 3 string elements (by default), and thus the error: var.resource_group_name is list of string with 3 elements explains the problem, and is illustrated below.
The var.env is a map consisting of 3 elements and also being used as a string and fails for the same reason.
resource "azurerm_resource_group" "resgrp" {
for_each = toset(var.resource_group_name)
# var.resource_group_name is actually a list, but it's being
# used as a string here, which will fail.
name = "${var.resource_group_name}-rg-${each.value}"
location = var.location
tags = {
"Environment" = "${var.env}-${each.value}"
}
}
You probably instead want:
resource "azurerm_resource_group" "resgrp" {
for_each = toset(var.resource_group_name)
name = "${each.value}-rg"
location = var.location
tags = {
"Environment" = "${each.value}"
}
}
Additionally, as an alternative to the approach of putting multiple environments in a single resource construct, consider using modules to create reusable infrastructure for your resources and then calling each module for the environment that you're using, this is a best practice when implementing duplicate or near-duplicate infrastructure across multiple environments and allows you some flexibility with naming conventions and other parameters that would differ based upon the environment.
Rough example:
module "test-dev" {
source = "../modules/infrastructure"
environment = "Test-dev"
vm_count = 1
}
module "test-uat" {
source = "../modules/infrastructure"
environment = "Test-dev"
vm_count = 3
}
module "test-prod" {
source = "../modules/infrastructure"
environment = "Test-prod"
account_tier = "Premium"
vm_count = 6
}

Terraform for_each The given key does not identify an element in this collection value

I am working on a project where I am building a "stateful" qa environment that incorporates different Win versions that correspond to different sql versions. For example, for Server 2016 I will have 3 servers with each having a different version of sql. Same thing for 2019, and 2022. I have reached the point where it will read the values correctly but then it is giving me this error:
│ Error: Invalid index
│
│ on main.tf line 60, in resource "vsphere_virtual_machine" "vm":
│ 60: guest_id = data.vsphere_virtual_machine.template[each.value.template].guest_id
│ ├────────────────
│ │ data.vsphere_virtual_machine.template is object with 2 attributes
│ │ each.value.template is "Templates/QA_2016"
│
│ The given key does not identify an element in this collection value.
Here is the code:
`
provider "vsphere" {
vim_keep_alive = 30
user = var.vsphere_user
password = var.vsphere_password
vsphere_server = var.vsphere_server
# If you have a self-signed cert
allow_unverified_ssl = true
}
#### data block see local vars
data "vsphere_datacenter" "dc" {
name = local.dc
}
data "vsphere_compute_cluster" "compute_cluster" {
name = local.cluster
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_datastore" "datastore" {
name = local.datastore
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_network" "network" {
for_each = var.vms
name = each.value.network
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_virtual_machine" "template" {
for_each = var.vms
name = each.value.template
datacenter_id = data.vsphere_datacenter.dc.id
}
##### Resource Block
resource "vsphere_virtual_machine" "vm" {
for_each = var.vms
datastore_id = data.vsphere_datastore.datastore.id
guest_id = data.vsphere_virtual_machine.template[each.value.template].guest_id
resource_pool_id = data.vsphere_compute_cluster.compute_cluster.id
# host_system_id = "${data.vsphere_datacenter.dc.id}"
firmware = data.vsphere_virtual_machine.template[each.value.template].firmware
num_cpus = local.cpu_count
memory = local.memory
scsi_type = data.vsphere_virtual_machine.template[each.value.template].scsi_type
wait_for_guest_net_timeout = -1
name = each.value.name
`
Here is the vars file:
`
locals {
dc = "DC"
cluster = "The Cluster"
datastore = "Storage_thing"
cpu_count = "4"
memory = "16384"
disk_label = "disk0"
disk_size = "250"
disk_thin = "true"
domain = "my.domain"
dns = ["xx.xx.xx.xx", "xx.xx.xx.xx"]
password = "NotMyPass"
auto_logon = true
auto_logon_count = 1
firmware = "efi"
}
#### Name your vm's here - Terraform will provision what is provided here - Add or comment out VM's as needed
variable "vms" {
type = map(any)
default = {
wqawin16sql14 = {
name = "wqawin16sql14"
network = "vm_network"
template = "Templates/QA_2016"
},
wqawin16sql17 = {
name = "wqawin16sql17"
network = "vm_network"
template = "Templates/QA_2016"
},
}
}
`
Terraform is reporting this error because your data "vsphere_virtual_machine" "template" block has for_each = var.vms and so the instance keys of that resource are the keys from your map value: "wqawin16sql14" and "wqawin16sql17".
That fails because you're trying to look up an instance using the value of the template attribute, which is "Templates/QA_2016" and therefore doesn't match any of the instance keys.
It seems like your goal here is to find one virtual machine for each distinct value of the template attributes in your input variable, and then use the guest_id of each of those VMs to populate the guest_id of the corresponding instance of resource "vsphere_virtual_machine" "vm".
If so, you'll need to make the for_each for your data resource be a collection where each element represents a template, rather than having each element represent a virtual machine to manage. One way to achieve that would be to calculate the set of all template values across all of your elements of var.vm, like this:
data "vsphere_virtual_machine" "template" {
for_each = toset(var.vms[*].template)
name = each.value
datacenter_id = data.vsphere_datacenter.dc.id
}
Notice that name is now set to just each.value because for_each is now just a set of template names, like toset(["Templates/QA_2016"]), so the values of this collection are just strings rather than objects with attributes.
With this change you should then have only one instance of this data resource whose address will be data.vmware_virtual_machine.template["Templates/QA_2016"]. This instance key now does match the template attribute in both of your VM objects, and so the dynamic lookup of the guest ID based on the template attribute of each VM object should succeed.

Terraform - Can't access attributes on a primitive-typed value (string) when trying to add multiple disks

I'm a total newbie to Terraform and loving it so far. However, I'm a bit stuck with the below. I'm trying to add a disk to multiple machines using a dynamic block, but I'm getting the error 'Can't access attributes on a primitive-typed value (string)' whenever I run terraform plan. The config for my compute engine instance/disk looks like this:
resource "google_compute_instance" "vm_instance" {
count = 2
name = "test-instance${count.index + 1}"
machine_type = "e2-micro"
labels = {
"environment" = var.environment
}
boot_disk {
initialize_params {
image = var.image
}
}
network_interface {
# A default network is created for all GCP projects
network = "default"
}
lifecycle {
ignore_changes = [
resource_policies,
metadata,
attached_disk
]
}
}
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.value.name
type = "pd-ssd"
labels = {
environment = "dev"
}
physical_block_size_bytes = 4096
}
resource "google_compute_attached_disk" "default" {
for_each = google_compute_instance.vm_instance.*.id
disk = google_compute_disk.default[each.key].id
instance = each.key
}
It looks like the plan picks up two instances of the VM as expected, but Terraform is unable to access any of its attributes...
│ Error: Unsupported attribute
│
│ on main.tf line 50, in resource "google_compute_disk" "default":
│ 50: name = each.value.id
│ ├────────────────
│ │ each.value is "projects/blah/zones/europe-west2-a/instances/test-instance2"
Can anyone advise on where I'm going wrong, please? Thanks
If you use for_each over a set of string, you don't get access to properties, so this won't work.
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.value.name
# ...
}
If you want to use each of those id's, what you need is the set key.
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.key
# ...
}
Also, you're better off not using the Legacy Splat Expressions, and will find the code more intuitive I suspect formed something like this:
resource "google_compute_disk" "default" {
for_each = google_compute_instance.vm_instance
name = each.value.id
# ...
}

How to loop over a map and return value if it matches with a value from a list in Terraform

I have a map with variable names of a subnet and their ids
eg:
subnet_id = {
subnet-a="XXXX/subnet-a",
subnet-b="XXXX/subnet-b",
subnet-c="XXXX/subnet-c"
}
I have a list variable(subnet_variable_name) with the variable names of subnets (which is a value inside a map variable:network_rule)
eg:
network_rule = {
rule1 = {
subnet_variable_name = ["subnet-a","subnet-b"]
rule = []
}
}
How do I loop through subnet_id and return the values where the key matches with values in list subnet_variable_name ? I tried using the below for function but it is throwing an error.
resource "xxx" "xxx"{
for_each = var.network_rule
subnet_ids = {for value in each.value["subnet_variable_name"]: value => lookup(var.subnet_id, value, null)}
}
Error :
Error: Incorrect attribute value type
│ 7: subnet_ids = {for value in each.value["subnet_variable_name"]: value => lookup(var.subnet_id, value, null)}
│ ├────────────────
│ │ each.value["subnet_variable_name"] is list of string with 1 element
│ │ var.subnet_id is map of string with 2 elements
│
│ Inappropriate value for attribute "subnet_id": set of string required.
Edit: the result I want is for subnet_ids to be assigned ["XXXX/subnet-a","XXXX/subnet-b"]
You can use a condition with contains and keys functions:
resource "xxx" "xxx" {
for_each = var.network_rule
subnet_ids = [
for subnet in each.value.subnet_variable_name: subnet if contains(keys(var.subnet_id), subnet)
]
}
Update:
If you want the value of the subnet_id instead of the key, you can do the following:
resource "xxx" "xxx" {
for_each = var.network_rule
subnet_ids = [
for subnet in each.value.subnet_variable_name: var.subnet_id[subnet] if contains(keys(var.subnet_id), subnet)
]
}

Terraform expression for_each invalid index

HI Guys and happy new year to all
I got an issue to gatter the token generate by the bloc ressource below which have an iteration with an for_each loop.
my varibale map is :
variable "wvd_hostpool" {
description = "Please provide the required information to create a WVD hostpool."
type = map(any)
default = {
hp-azcan-weu-wvd-01 = {
"name" = "hp-azcan-weu-wvd-01"
"type" = "Personal"
"load_balancer_type" = "DepthFirst"
"personal_desktop_assignment_type" = "Automatic"
"maximum_sessions_allowed" = 16
"expiration_date" = "2022-02-10T18:46:43Z"
"friendly_name" = "Canary"
"description" = "Dedicated to canary deployments."
"location" = "westeurope"
"vm_count" = 1
"vm_size" = "Standard_F4s_v2"
"vm_prefix" = "AZWEUHP01TST"
"validate_environment" = "true"
},
hp-azprd-weu-wvd-01 = {
"name" = "hp-azprd-weu-wvd-01"
"type" = "Pooled"
"load_balancer_type" = "DepthFirst"
"personal_desktop_assignment_type" = "Automatic"
"maximum_sessions_allowed" = 16
"expiration_date" = "2022-02-10T18:46:43Z"
"friendly_name" = "desktop"
"description" = "Dedicated to medium workload type (Microsoft Word, CLIs, ...)."
"location" = "westeurope"
"vm_count" = 1
"vm_size" = "Standard_F4s_v2"
"vm_prefix" = "AZWEUHP01WKT"
"validate_environment" = "false"
},
the ressource bloc :
resource "azurerm_virtual_desktop_host_pool" "wvd_hostpool" {
for_each = var.wvd_hostpool
name = each.value.name
location = each.value.location
custom_rdp_properties = "audiocapturemode:i:1;audiomode:i:0;"
resource_group_name = data.azurerm_resource_group.avd_rg.name
validate_environment = each.value.validate_environment
type = each.value.type
load_balancer_type = each.value.load_balancer_type
friendly_name = each.value.friendly_name
description = each.value.description
personal_desktop_assignment_type = each.value.personal_desktop_assignment_type
maximum_sessions_allowed = each.value.maximum_sessions_allowed
registration_info {
expiration_date = each.value.expiration_date
}
}
I would get the value of the token generate under registration_info to save it to a key vault for reuse or export it to an output but has you can see I getting an error with invalid index. I speding 2 day without sucess at this could you help me please ?
resource "azurerm_key_vault_secret" "wvd_registration_info" {
for_each = var.wvd_hostpool
name = each.value.name
value = azurerm_virtual_desktop_host_pool.wvd_hostpool[each.value.name].registration_info.0.token
key_vault_id = azurerm_key_vault.wvd_key_vault.id
depends_on = [azurerm_role_assignment.wvd_sp]
}
the same result
Error: Invalid index
│
│ on security.tf line 115, in resource "azurerm_key_vault_secret" "wvd_registration_info":
│ 115: value = azurerm_virtual_desktop_host_pool.wvd_hostpool[each.value.name].registration_info[0].token
│ ├────────────────
│ │ azurerm_virtual_desktop_host_pool.wvd_hostpool is object with 3 attributes
│ │ each.value.name is "hp-azprd-weu-wvd-02"
│
│ The given key does not identify an element in this collection value: the collection has no elements
If you specify a map as a for_each attribute, Terraform will use its keys as identifiers for the resources which will be created. This means that if you want to reference a another resource created using a for_each, you have to use they keys from the map, or each.key in your example:
resource "azurerm_key_vault_secret" "wvd_registration_info" {
for_each = var.wvd_hostpool
name = each.value.name
value = azurerm_virtual_desktop_host_pool.wvd_hostpool[each.key].registration_info.token
key_vault_id = azurerm_key_vault.wvd_key_vault.id
depends_on = [azurerm_role_assignment.wvd_sp]
}

Resources