Hello,
We just observed a strange behaviour when tried to split the code into modules.
It is a simple, four steps ,task:
- Create certificate
- Create DNS zone
- Create certificate validation records
- Validate certificate
We know that some additional steps are required to make it works in real life scenario.
1. Code works as expected when is located in a single file
main-single.tf
# // Variables
variable "domain_name" {
type = string
default = "domain.com"
}
# // Request certificate
resource "aws_acm_certificate" "common" {
domain_name = var.domain_name
subject_alternative_names = ["*.${var.domain_name}"]
validation_method = "DNS"
options {
certificate_transparency_logging_preference = "DISABLED"
}
}
# // Validate certificate
resource "aws_acm_certificate_validation" "common" {
certificate_arn = aws_acm_certificate.common.arn
validation_record_fqdns = [for record in aws_route53_record.acm_certificate_validation : record.fqdn]
}
# // DNS zone
resource "aws_route53_zone" "common" {
name = var.domain_name
comment = "Terraform"
}
# // DNS records - ACM validation
resource "aws_route53_record" "acm_certificate_validation" {
for_each = {
for dvo in aws_acm_certificate.common.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]
ttl = 60
type = each.value.type
zone_id = aws_route53_zone.common.zone_id
}
2 . And it still works when we move just part of the code in one module
βββ main-one-module.tf
βββ modules
βββ acm
βββ main.tf
main-one-modules.tf
# // Variables
variable "domain_name" {
type = string
default = "domain.com"
}
# // ACM
module "acm" {
source = "./modules/acm"
domain_name = var.domain_name
certificate_validation_records = aws_route53_record.acm_certificate_validation
}
# // DNS zone
resource "aws_route53_zone" "common" {
name = var.domain_name
comment = "Terraform"
}
# // DNS records - ACM validation
resource "aws_route53_record" "acm_certificate_validation" {
for_each = {
for dvo in module.acm.certificate_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]
ttl = 60
type = each.value.type
zone_id = aws_route53_zone.common.zone_id
}
main-acm.tf
# // Variables
variable "domain_name" {
type = string
}
variable "certificate_validation_records" {
type = map(any)
}
# // Request certificate
resource "aws_acm_certificate" "common" {
domain_name = var.domain_name
subject_alternative_names = ["*.${var.domain_name}"]
validation_method = "DNS"
options {
certificate_transparency_logging_preference = "DISABLED"
}
}
# // Validate certificate
resource "aws_acm_certificate_validation" "common" {
certificate_arn = aws_acm_certificate.common.arn
validation_record_fqdns = [for record in var.certificate_validation_records : record.fqdn]
}
# // Output
output "certificate_domain_validation_options" {
value = aws_acm_certificate.common.domain_validation_options
}
3. But when we break it down into two modules, we got an error
βββ main-two-modules.tf
βββ modules
βββ acm
β βββ main-acm.tf
βββ route53
βββ main-route53.tf
main-two-modules.tf
# // Variables
variable "domain_name" {
type = string
default = "domain.com"
}
# // ACM
module "acm" {
source = "./modules/acm"
domain_name = var.domain_name
certificate_validation_records = module.route53.acm_certificate_validation
}
# // Route53
module "route53" {
source = "./modules/route53"
domain_name = var.domain_name
certificate_domain_validation_options = module.acm.certificate_domain_validation_options
}
main-acm.tf
# // Variables
variable "domain_name" {
type = string
}
variable "certificate_validation_records" {
type = map(any)
}
# // Request certificate
resource "aws_acm_certificate" "common" {
domain_name = var.domain_name
subject_alternative_names = ["*.${var.domain_name}"]
validation_method = "DNS"
options {
certificate_transparency_logging_preference = "DISABLED"
}
}
# // Validate certificate
resource "aws_acm_certificate_validation" "common" {
certificate_arn = aws_acm_certificate.common.arn
validation_record_fqdns = [for record in var.certificate_validation_records : record.fqdn]
}
# // Output
output "certificate_domain_validation_options" {
value = aws_acm_certificate.common.domain_validation_options
}
main-route53.tf
# // Variables
variable "domain_name" {
type = string
}
variable "certificate_domain_validation_options" {
type = list(any)
}
# // DNS zone
resource "aws_route53_zone" "common" {
name = var.domain_name
comment = "Terraform"
}
# // DNS records - ACM validation
resource "aws_route53_record" "acm_certificate_validation" {
for_each = {
for dvo in var.certificate_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]
ttl = 60
type = each.value.type
zone_id = aws_route53_zone.common.zone_id
}
# // Outputs
output "acm_certificate_validation" {
value = aws_route53_record.acm_certificate_validation
}
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.
What is the difference when Terraform determine resources it should create, when we use flat structure and modules?
Also, we found an open issue on GitHub, but not clear if it is related - for_each not working well when creating aws_route53_record #14447.