I have a route53 module which has a record creating resource as part of it (of course):
resource "aws_route53_record" "record" {
for_each = var.module_enabled ? local.records : {}
zone_id = each.value.zone_id
type = each.value.type
name = each.value.name
allow_overwrite = each.value.allow_overwrite
health_check_id = each.value.health_check_id
set_identifier = each.value.set_identifier
# set default TTL when ttl missing and is not ALIAS record
ttl = each.value.ttl == null && each.value.alias.name == null ? var.default_ttl : each.value.ttl
[...]
}
where the local.records in the for_each loop looks like this:
+ local_records = {
+ "a-subdomain.domain.tld" = {
+ alias = {
+ evaluate_target_health = true
+ name = (known after apply)
+ zone_id = (known after apply)
}
}
+ "cname-subdomain-api.domain.tld" = {
+ alias = {
+ evaluate_target_health = null
+ name = null
+ zone_id = null
}
}
[ more CNAMEs here ]
+ }
and the module call example:
module "dns_records" {
source = "./modules/route53"
count = length(module.gla) == 1 ? 1 : 0
zone_id = try(data.aws_route53_zone.selected.id, null)
allow_overwrite = true
records = flatten(concat([
{
name = "${lower(var.vpc["tag"])}.${var.my_domain["name"]}"
type = "A"
alias = {
name = module.gla[count.index].dns_name #<== another module dependency
zone_id = module.gla[count.index].zone_id #<== another module dependency
evaluate_target_health = true
}
},
{
name = "${lower(var.vpc["tag"])}-api.${var.my_domain["name"]}"
type = "CNAME"
ttl = 60
records = ["${lower(var.vpc["tag"])}.${var.my_domain["name"]}"]
},
],
[ for i, v in var.my_subdomain : [{
name = "${split("-", lower(var.vpc["tag"]))[0]}-${lower(v)}.${var.my_domain["name"]}"
type = "CNAME"
ttl = 60
records = ["${lower(var.vpc["tag"])}.${var.my_domain["name"]}"]
},
{
name = "${split("-", lower(var.vpc["tag"]))[0]}-${lower(v)}-api.${var.my_domain["name"]}"
type = "CNAME"
ttl = 60
records = ["${lower(var.vpc["tag"])}.${var.my_domain["name"]}"]
}
]]
))
}
Now, this worked fine in 1.5.7 but not in latest 1.6 and 1.7 where the Route53 records fail to get created with the error bellow in the plan stage.
Actual Behavior
The error below is related to creating the ALIAS record, if I remove this from the list:
{
name = "${lower(var.vpc["tag"])}.${var.my_domain["name"]}"
type = "A"
alias = {
name = module.gla[count.index].dns_name #<== another module dependency
zone_id = module.gla[count.index].zone_id #<== another module dependency
evaluate_target_health = true
}
},
the error is gone. This indicates an issue with how this expression:
is being evaluated when the value is not known. My impression is that the effect here is as if this logic each.value.alias.name == null used to evaluate to null when name is known after apply but not anymore.
Relevant Error/Panic Output Snippet
╷
│ Error: Missing required argument
│
│ with module.dns_records[0].aws_route53_record.record["a-subdomain.domain.tld"],
│ on ./modules/route53/main.tf line 130, in resource "aws_route53_record" "record":
│ 130: ttl = each.value.ttl == null && each.value.alias.name == null ? var.default_ttl : each.value.ttl
│
│ "ttl": all of `records,ttl` must be specified
╵
Terraform Configuration Files
The modules/route53/main.tf file:
/* ROUTE53 ZONES AND RECORDS */
data "aws_caller_identity" "current" {
}
locals {
common_tags = {
Environment = try(lower(var.tag), null)
Type = "dns"
}
dns_tags = merge(
try(var.common_tags, null),
local.common_tags,
try(tomap({
"Name" = "enc-${var.common_tags["enc-environment"]}-${var.common_tags["enc-customer"]}"
}), null)
)
}
locals {
zones = var.name == null ? [] : try(tolist(var.name), [tostring(var.name)], [])
skip_zone_creation = length(local.zones) == 0
run_in_vpc = length(var.vpc_ids) > 0
skip_delegation_set_creation = !var.module_enabled || local.skip_zone_creation || local.run_in_vpc ? true : var.skip_delegation_set_creation
delegation_set_id = var.delegation_set_id != null ? var.delegation_set_id : try(
aws_route53_delegation_set.delegation_set[0].id, null
)
}
resource "aws_route53_delegation_set" "delegation_set" {
count = local.skip_delegation_set_creation ? 0 : 1
reference_name = var.reference_name
depends_on = [var.module_depends_on]
}
resource "aws_route53_zone" "zone" {
for_each = var.module_enabled ? toset(local.zones) : []
name = each.value
comment = var.comment
force_destroy = var.force_destroy
delegation_set_id = local.delegation_set_id
dynamic "vpc" {
for_each = { for id in var.vpc_ids : id => id }
content {
vpc_id = vpc.value
}
}
tags = local.dns_tags
}
locals {
records_expanded = {
for i, record in var.records : join("-", compact([
lower(record.type),
try(lower(record.set_identifier), ""),
try(lower(record.failover), ""),
try(lower(record.name), ""),
])) => {
type = record.type
name = try(record.name, "")
ttl = try(record.ttl, null)
alias = {
name = try(record.alias.name, null)
zone_id = try(record.alias.zone_id, null)
evaluate_target_health = try(record.alias.evaluate_target_health, null)
}
allow_overwrite = try(record.allow_overwrite, var.allow_overwrite)
health_check_id = try(record.health_check_id, null)
idx = i
set_identifier = try(record.set_identifier, null)
weight = try(record.weight, null)
failover = try(record.failover, null)
}
}
records_by_name = {
for product in setproduct(local.zones, keys(local.records_expanded)) : "${product[1]}-${product[0]}" => {
zone_id = try(aws_route53_zone.zone[product[0]].id, null)
type = local.records_expanded[product[1]].type
name = local.records_expanded[product[1]].name
ttl = local.records_expanded[product[1]].ttl
alias = local.records_expanded[product[1]].alias
allow_overwrite = local.records_expanded[product[1]].allow_overwrite
health_check_id = local.records_expanded[product[1]].health_check_id
idx = local.records_expanded[product[1]].idx
set_identifier = local.records_expanded[product[1]].set_identifier
weight = local.records_expanded[product[1]].weight
failover = local.records_expanded[product[1]].failover
}
}
records_by_zone_id = {
for id, record in local.records_expanded : id => {
zone_id = var.zone_id
type = record.type
name = record.name
ttl = record.ttl
alias = record.alias
allow_overwrite = record.allow_overwrite
health_check_id = record.health_check_id
idx = record.idx
set_identifier = record.set_identifier
weight = record.weight
failover = record.failover
}
}
records = local.skip_zone_creation ? local.records_by_zone_id : local.records_by_name
}
resource "aws_route53_record" "record" {
for_each = var.module_enabled ? local.records : {}
zone_id = each.value.zone_id
type = each.value.type
name = each.value.name
allow_overwrite = each.value.allow_overwrite
health_check_id = each.value.health_check_id
set_identifier = each.value.set_identifier
# only set default TTL when not set and not alias record
ttl = each.value.ttl == null && each.value.alias.name == null ? var.default_ttl : each.value.ttl
#ttl = each.value.ttl == null ? var.default_ttl : each.value.ttl
#ttl = can(each.value.ttl) && can(each.value.alias.name) ? var.default_ttl : each.value.ttl
#ttl = lookup(each.value, "ttl", null) == null && lookup(each.value.alias, "name", null) == null ? var.default_ttl : each.value.ttl
#ttl = try(each.value.ttl, null) == null && try(each.value.alias.name, null) == null ? var.default_ttl : each.value.ttl
# split TXT records at 255 chars to support >255 char records
records = can(var.records[each.value.idx].records) ? [for r in var.records[each.value.idx].records :
each.value.type == "TXT" && length(regexall("(\\\"\\\")", r)) == 0 ?
join("\"\"", compact(split("{SPLITHERE}", replace(r, "/(.{255})/", "$1{SPLITHERE}")))) : r
] : null
dynamic "weighted_routing_policy" {
for_each = each.value.weight == null ? [] : [each.value.weight]
content {
weight = weighted_routing_policy.value
}
}
dynamic "failover_routing_policy" {
for_each = each.value.failover == null ? [] : [each.value.failover]
content {
type = failover_routing_policy.value
}
}
dynamic "alias" {
for_each = each.value.alias.name == null ? [] : [each.value.alias]
content {
name = alias.value.name
zone_id = alias.value.zone_id
evaluate_target_health = alias.value.evaluate_target_health
}
}
}
The modules/route53/variables.tf file:
variable "name" {
description = "(Required) The name of the hosted zone. To create multiple zones at once, pass a list of names [\"zone1\", \"zone2\"]."
type = any
default = null
}
variable "tag" {
description = "(Optional) VPC tag."
type = string
default = ""
}
variable "common_tags" {
description = "(Optional) A map of tags to apply to all created resources that support tags."
type = map(string)
default = {}
}
variable "allow_overwrite" {
description = "(Optional) Default allow_overwrite value valid for all record sets."
type = bool
default = false
}
variable "comment" {
description = "(Optional) A comment for the hosted zone."
type = string
default = "Managed by Terraform"
}
variable "default_ttl" {
description = "(Optional) The default TTL (Time to Live) in seconds that will be used for all records that support the ttl parameter. Will be overwritten by the records ttl parameter if set."
type = number
default = 3600
}
variable "delegation_set_id" {
description = "(Optional) The ID of the reusable delegation set whose NS records you want to assign to the hosted zone."
type = string
default = null
}
variable "force_destroy" {
description = "(Optional) Whether to force destroy all records (possibly managed outside of Terraform) in the zone when destroying the zone."
type = bool
default = false
}
variable "records" {
description = "(Optional) A list of records to create in the Hosted Zone."
type = any
default = []
}
variable "reference_name" {
description = "(Optional) The reference name used in Caller Reference (helpful for identifying single delegation set amongst others)."
type = string
default = null
}
variable "skip_delegation_set_creation" {
description = "(Optional) Whether or not to create a delegation set and associate with the created zone."
type = bool
default = false
}
variable "vpc_ids" {
description = "(Optional) A list of IDs of VPCs to associate with a private hosted zone. Conflicts with the delegation_set_id."
type = list(string)
default = []
}
variable "zone_id" {
description = "(Optional) A zone ID to create the records in"
type = string
default = null
}
variable "module_enabled" {
type = bool
description = "(Optional) Whether to create resources within the module or not. Default is true."
default = true
}
variable "module_depends_on" {
type = any
description = "(Optional) A list of external resources the module depends_on. Default is []."
default = []
}
The modules/route53/outputs.tf file:
output "zone" {
description = "The created Hosted Zone(s)."
value = try(aws_route53_zone.zone, {})
}
output "records" {
description = "A list of all created records."
value = try(aws_route53_record.record, {})
}
output "delegation_set" {
description = "The outputs of the created delegation set."
value = try(aws_route53_delegation_set.delegation_set[0], {})
}
# debugging purposes
output "local_records_expanded" {
value = local.records_expanded
}
output "local_records_by_name" {
value = local.records_by_name
}
output "local_records_by_zone_id" {
value = local.records_by_zone_id
}
output "local_records" {
value = local.records
}
Steps to Reproduce
You can use the module call given above to reproduce the issue:
data "aws_route53_zone" "selected" {
name = "${var.my_domain["name"]}."
private_zone = false
}
module "dns_records" {
source = "./modules/route53"
count = length(module.gla) == 1 ? 1 : 0
zone_id = try(data.aws_route53_zone.selected.id, null)
allow_overwrite = true
records = flatten(concat([
{
name = "${lower(var.vpc["tag"])}.${var.my_domain["name"]}"
type = "A"
alias = {
name = module.gla[count.index].dns_name #<== another module dependency
zone_id = module.gla[count.index].zone_id #<== another module dependency
evaluate_target_health = true
}
},
{
name = "${lower(var.vpc["tag"])}-api.${var.my_domain["name"]}"
type = "CNAME"
ttl = 60
records = ["${lower(var.vpc["tag"])}.${var.my_domain["name"]}"]
},
],
[ for i, v in var.my_subdomain : [{
name = "${split("-", lower(var.vpc["tag"]))[0]}-${lower(v)}.${var.my_domain["name"]}"
type = "CNAME"
ttl = 60
records = ["${lower(var.vpc["tag"])}.${var.my_domain["name"]}"]
},
{
name = "${split("-", lower(var.vpc["tag"]))[0]}-${lower(v)}-api.${var.my_domain["name"]}"
type = "CNAME"
ttl = 60
records = ["${lower(var.vpc["tag"])}.${var.my_domain["name"]}"]
}
]]
))
}
with variables file variables.tf looking something like this:
variable "vpc" {
type = map(string)
default = {
tag = "myenv-au-test"
}
}
variable "my_domain" {
type = map(string)
default = {
name = "mydomain.com"
}
}
variable "my_subdomain" {
type = list(string)
default = ["subdomain1", "subdomain2"]
}
Where you will need a valid Route53 Zone for the data.aws_route53_zone and another module to simulate the module.gla's module.gla[count.index].dns_name and module.gla[count.index].zone_id outputs that the module.dns_records depends on and are unknown at the plan stage.
Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
Volunteering to Work on This Issue
If you are interested in working on this issue, please leave a comment.
If this would be your first contribution, please review the contribution guide.
Terraform Core Version
1.6.6, 1.7.4
AWS Provider Version
4.67, 5.38, 5.39.1
Affected Resource(s)
aws_route53_record
Expected Behavior
I have a route53 module which has a record creating resource as part of it (of course):
where the
local.records
in the for_each loop looks like this:and the module call example:
Now, this worked fine in
1.5.7
but not in latest1.6
and1.7
where the Route53 records fail to get created with the error bellow in theplan
stage.Actual Behavior
The error below is related to creating the
ALIAS
record, if I remove this from the list:the error is gone. This indicates an issue with how this expression:
is being evaluated when the value is not known. My impression is that the effect here is as if this logic
each.value.alias.name == null
used to evaluate tonull
when name is known after apply but not anymore.Relevant Error/Panic Output Snippet
Terraform Configuration Files
The
modules/route53/main.tf
file:The
modules/route53/variables.tf
file:The
modules/route53/outputs.tf
file:Steps to Reproduce
You can use the module call given above to reproduce the issue:
with variables file
variables.tf
looking something like this:Where you will need a valid Route53 Zone for the
data.aws_route53_zone
and another module to simulate themodule.gla
'smodule.gla[count.index].dns_name
andmodule.gla[count.index].zone_id
outputs that themodule.dns_records
depends on and are unknown at the plan stage.Debug Output
No response
Panic Output
No response
Important Factoids
No response
References
The link to Discuss https://discuss.hashicorp.com/t/terraform-plan-errors-for-known-after-apply-values/63457 where @apparentlymart and @jbardin generously offered their help.
Would you like to implement a fix?
None