Terraform fails with "Can't access attributes on a list of objects." - Cannot access fields on resources created using
for_each = toset(local.domains[*].domain_name)?
local.domains[*].domain_name is a list of domains, like www.example.com, that I need to convert to a set for use with for_each.
How can I iterate over the aws_apigatewayv2_domain_name resources individually to create aws_route53_record resources?
My current attempt results in the following error, which I don't understand (?):
╷
│ Error: Unsupported attribute
│
│ on ../modules/http-api-custom-domain-name/main.tf line 64, in resource "aws_route53_record" "api_gateway":
│ 64: name = aws_apigatewayv2_domain_name.api_gateway[each.value].domain_name_configuration.target_domain_name
│ ├────────────────
│ │ aws_apigatewayv2_domain_name.api_gateway is object with 1 attribute "bff-dev-partner.service-dev.dpt.lego.com"
│ │ each.value is "bff-dev-partner.service-dev.dpt.lego.com"
│
│ Can't access attributes on a list of objects. Did you mean to access attribute "target_domain_name" for a specific element of the list, or
│ across all elements of the list?
Instead of indexing aws_apigatewayv2_domain_name resources, I would rather iterate over them directly. Is this possible?
How can I fix the code, so that aws_route53_record and aws_api_gateway_base_path_mapping resources are created for each aws_apigatewayv2_domain_name?
Code:
resource "aws_apigatewayv2_domain_name" "api_gateway" {
for_each = toset(local.domains[*].domain_name)
domain_name = each.value
domain_name_configuration {
certificate_arn = var.certificate_arn == null ? aws_acm_certificate.cert[0].arn : var.certificate_arn
endpoint_type = "REGIONAL"
security_policy = "TLS_1_2"
}
}
resource "aws_route53_record" "api_gateway" {
for_each = toset(local.domains[*].domain_name)
name = aws_apigatewayv2_domain_name.api_gateway[each.value].domain_name_configuration.domain_name
type = "A"
zone_id = local.domain_to_hosted_zone[each.value]
alias {
name = aws_apigatewayv2_domain_name.api_gateway[each.value].domain_name_configuration.target_domain_name
zone_id = aws_apigatewayv2_domain_name.api_gateway[each.value].domain_name_configuration.hosted_zone_id
evaluate_target_health = false
}
}
resource "aws_api_gateway_base_path_mapping" "api_gateway" {
for_each = aws_apigatewayv2_domain_name.api_gateway[*].domain_name
api_id = var.api_gateway_id
stage_name = var.api_gateway_stage
domain_name = each.value
}
Errors:
│ Error: Unsupported attribute
│
│ on ../modules/http-api-custom-domain-name/main.tf line 59, in resource "aws_route53_record" "api_gateway":
│ 59: name = aws_apigatewayv2_domain_name.api_gateway[each.value].domain_name_configuration.domain_name
│ ├────────────────
│ │ aws_apigatewayv2_domain_name.api_gateway is object with 1 attribute "bff-dev-partner.service-dev.dpt.lego.com"
│ │ each.value is "bff-dev-partner.service-dev.dpt.lego.com"
│
│ Can't access attributes on a list of objects. Did you mean to access an attribute for a specific element of the list, or across all
│ elements of the list?
╵
╷
│ Error: Unsupported attribute
│
│ on ../modules/http-api-custom-domain-name/main.tf line 64, in resource "aws_route53_record" "api_gateway":
│ 64: name = aws_apigatewayv2_domain_name.api_gateway[each.value].domain_name_configuration.target_domain_name
│ ├────────────────
│ │ aws_apigatewayv2_domain_name.api_gateway is object with 1 attribute "bff-dev-partner.service-dev.dpt.lego.com"
│ │ each.value is "bff-dev-partner.service-dev.dpt.lego.com"
│
│ Can't access attributes on a list of objects. Did you mean to access attribute "target_domain_name" for a specific element of the list, or
│ across all elements of the list?
╵
╷
│ Error: Unsupported attribute
│
│ on ../modules/http-api-custom-domain-name/main.tf line 65, in resource "aws_route53_record" "api_gateway":
│ 65: zone_id = aws_apigatewayv2_domain_name.api_gateway[each.value].domain_name_configuration.hosted_zone_id
│ ├────────────────
│ │ aws_apigatewayv2_domain_name.api_gateway is object with 1 attribute "bff-dev-partner.service-dev.dpt.lego.com"
│ │ each.value is "bff-dev-partner.service-dev.dpt.lego.com"
│
│ Can't access attributes on a list of objects. Did you mean to access attribute "hosted_zone_id" for a specific element of the list, or
│ across all elements of the list?
╵
╷
│ Error: Unsupported attribute
│
│ on ../modules/http-api-custom-domain-name/main.tf line 71, in resource "aws_api_gateway_base_path_mapping" "api_gateway":
│ 71: for_each = aws_apigatewayv2_domain_name.api_gateway[*].domain_name
│
│ This object does not have an attribute named "domain_name".
When you use
for_eachto create multiple resources, Terraform treats these as a map of objects, not a list.In your
aws_route53_recordandaws_api_gateway_base_path_mappingresources, you are trying to access attributes using a list syntax, but you should be using the map key (which iseach.valuein your case).That means the syntax to access an attribute of an individual resource needs to be adjusted:
With this modification, you should no longer encounter the "
Unsupported attribute" error. Eachaws_route53_recordandaws_api_gateway_base_path_mappingresource should be correctly associated with eachaws_apigatewayv2_domain_nameresource.However, as noted by jordanm in the comments, it depends on how
local.domainsis defined:If
local.domainsis a simple list of domain name strings, like["www.example.com", "api.example.com"], the solution above would work as is. That is becausetoset(local.domains[*].domain_name)would correctly convert this list into a set of strings, whichfor_eachcan iterate over.If
local.domainsis a list of maps or objects, such as[{"domain_name": "www.example.com"}, {"domain_name": "api.example.com"}], then the way you extract domain names forfor_eachwould need to change. Instead oftoset(local.domains[*].domain_name), you would need to use a more complex expression to extract the domain names from each map/object in the list.If
local.domainshas a more complex or nested structure, the solution would need to be tailored to correctly navigate and extract the required information. For example, iflocal.domainsincludes additional nested attributes or varying levels of details, the method of extracting domain names forfor_eachwould need to accommodate this complexity.To detail the second case, when
local.domainsis a list of maps or objects, where each element contains adomain_namekey, your Terraform configuration would need to extract these domain names differently for use withfor_each.Assuming
local.domainsis defined like this:Your resources would be defined as follows:
aws_apigatewayv2_domain_name: Use a
for_eachto iterate over a set of domain names extracted fromlocal.domains.aws_route53_record: Referencing
each.keyandeach.valuebased on thefor_eachused in theaws_apigatewayv2_domain_nameresource.aws_api_gateway_base_path_mapping: Using
each.keywhich represents the domain name in this setup.The
for_eachinaws_apigatewayv2_domain_nameis changed to create a map where keys and values are both domain names, extracted from thelocal.domainslist of maps. That structure can then be used consistently in the other resources.The confusion arises from the syntax and behavior of Terraform's expressions, specifically when dealing with lists of objects and the splat expression
[*].If
local.domainsis a list of objects, like so:The expression
local.domains[*].domain_nameattempts to use the splat expression to extract thedomain_nameattribute from each object in the list. However, this expression does not directly yield a simple list of strings; instead, it results in a complex list of lists due to how Terraform interprets the splat operator with objects.The correct way to extract a list of strings (domain names in this case) from a list of objects in Terraform is to use a
forexpression:That
forexpression iterates over each object inlocal.domainsand extracts thedomain_nameattribute, resulting in a flat list of strings.Once you have a flat list of domain names, you can convert it to a set for use with
for_each:That expression creates a set of domain names from
local.domains, which can be used withfor_eachin your resource definitions.So, when you tried to use
toset(local.domains[*].domain_name), it did not work as expected becauselocal.domains[*].domain_namedid not produce a simple list of strings, but rather a more complex structure that is not directly compatible withtoset(). Theforexpression resolves this issue by correctly flattening the structure into a simple list of strings.Yes, your understanding is correct! The expression
for_each = { for d in local.domains : d.domain_name => d.domain_name }does indeed create a map where each key is mapped to the same value. That might seem a bit odd at first glance, but it is a useful technique in Terraform for a couple of reasons:Uniqueness of keys: In Terraform, when using
for_each, the keys of the map need to be unique. By using the domain name as both the key and the value, you make sure each entry in the map is unique, which is a requirement forfor_eachto work correctly. That is particularly useful when the value (the domain name in this case) is already unique and encapsulates all the information needed for the iteration.Ease of reference: By setting the value to be the same as the key, you can easily reference the domain name in your resource configurations. Within the resource block,
each.keyandeach.valuewill both give you the domain name, making your code more intuitive and easier to understand.Flexibility for expansion: That pattern also leaves room for easily expanding the configuration in the future. If you later need to include more information for each domain, you can adjust the map to include additional values while keeping the domain name as the key.
While it might seem redundant to map a value to itself, this approach is often used in Terraform to satisfy the unique key constraint of
for_eachand to make the code more flexible and maintainable.If
local.domainsis a list of strings, the expressiontoset(local.domains[*].domain_name)would indeed not work as expected: whenlocal.domainsis a simple list of strings, like["www.example.com", "api.example.com"], each element of the list is just a string, not an object or map. So, it does not have any attributes or named fields likedomain_nameto access.The
[*].domain_namepart is a use of the splat operator, which is intended for use with lists of complex types (like objects or maps) to access a named attribute on each element. When applied to a list of simple strings, it does not make sense because there is nodomain_nameattribute on a string.In the scenario where
local.domainsis a list of strings, converting it to a set would be straightforward with justtoset(local.domains):In this case,
local.domainsitself is directly a list of domain names (strings), andtoset()converts this list into a set for use withfor_each. There is no need to use the splat operator since you are not accessing an attribute of a complex type, but rather directly using the string values in the list.Yes, in the context of the
aws_apigatewayv2_domain_nameresource in Terraform,domain_nameandtarget_domain_namerefer to two different aspects of the API Gateway custom domain setupdomain_name: That is the custom domain name that you are setting up for your API Gateway. It is the domain that clients will use to access your API. For example, if you have a custom domain likeapi.mycompany.com, this is what you would specify as thedomain_name. That is essentially the public-facing URL that you want to map to your API Gateway.target_domain_name: That attribute is part of thedomain_name_configurationblock within theaws_apigatewayv2_domain_nameresource. It refers to the hostname that API Gateway assigns to your deployed API. That is the actual endpoint that API Gateway creates and maintains, and it is different from the custom domain name you provide. Thetarget_domain_nameis used internally by AWS to route requests to your API Gateway.When setting up a custom domain in API Gateway, you typically point your custom domain (
domain_name) to the AWS-generated target domain (target_domain_name) using a DNS record (like a CNAME or an Alias record in Route 53). That setup makes sure when users hit your custom domain, the request is routed correctly to your API Gateway's target domain, and from there, to your API's deployment stage.In short:
domain_nameis the custom domain you want to use for your API, whiletarget_domain_nameis the AWS-generated endpoint to which your custom domain should point.The error message indicating that
each.value.domain_name_configuration is a list of objects with 1 elementsuggests that there is a mismatch between how the Terraform resourceaws_apigatewayv2_domain_nameis structured and how you are trying to access its attributes in theaws_route53_recordand other related resources.When you define a resource with
for_each, each instance of that resource is a map where the key is defined by thefor_eachexpression and the value is the resource itself. Ifaws_apigatewayv2_domain_name.api_gatewayis defined withfor_each,aws_apigatewayv2_domain_name.api_gateway[each.key]oraws_apigatewayv2_domain_name.api_gateway[each.value]will refer to individual instances of theaws_apigatewayv2_domain_nameresource.If your
aws_route53_recordresource configuration is:Then make sure you are accessing the attributes of the
aws_apigatewayv2_domain_nameresource correctly. The resource might have a different structure than anticipated.Review the Terraform documentation for
aws_apigatewayv2_domain_nameto confirm the structure of the resource and how its attributes should be accessed. Thedomain_name_configurationmight be a list, and if so, you would need to access its first element, likeeach.value.domain_name_configuration[0].target_domain_name.Use
terraform state showto inspect the state of an individualaws_apigatewayv2_domain_nameresource. That can give you a clear view of its structure and help you understand how to access its attributes.Make sure your Terraform code is compatible with the version of the AWS provider you are using. Sometimes, attributes or their structures can change between versions.
And double-check the syntax and make sure there are no typos or incorrect references in your Terraform configuration.
As you mentioned,
terraform consoleis a useful tool for experimenting with Terraform expressions. But if you want alternative, you might consider:Output variables to print values during
terraform applyorterraform plan. This is a simple yet effective way to see the values of variables, locals, or any expression. Define an output in your configuration like this:After running
terraform applyorterraform plan, Terraform will display the values of these outputs.Debug logging that can be enabled by setting the
TF_LOGenvironment variable. This can provide a lot of insight into what Terraform is doing, but the logs can be quite verbose. Use it like this:Keep in mind that debug logs can contain sensitive information, so be cautious about where and how you share these logs.
Refactoring for clarity, breaking down complex expressions into simpler, more atomic parts can help in understanding and debugging. This might involve using more locals to hold intermediate values so you can output and inspect them easily.