Thanks for sharing those examples, @mihaime! That helps a lot to understand what you’re intending to achieve and what challenges you’ve run into.
Since this configuration is built from a number of third-party modules the options here are somewhat constrained by some design decisions of those modules. I won’t dwell on this a lot because changing those third-party modules is not really in scope for what you are working on anyway, but it’s true that if you are using modules that require a lot of different inputs then you can’t really avoid assigning values to all of them.
One thing I’m noticing reading over all of these module
blocks is that although there are several repeated blocks with similar structure, there seems to be very little overlap in actual content between them. For example: module "aws_trans"
, module "aws_client_spoke"
, and module "aws_server_spoke"
all have an argument called region
, but they all take different values for that argument because the purpose of this design is (presumably) to create connectivity between regions.
With that said then, it doesn’t seem like there’s a lot of content redundancy across these blocks. The differences in settings between these need to be represented somewhere, and so what you have is the most direct and therefore least redundant way to represent those: directly inside the module
blocks. It’s important to recognize the difference between something being long because there are really lots of different things to declare and long because the declarations are done in an inefficient way.
The main opportunity I can see for simplification here is that you seem to have a general pattern in how the different modules all contribute DNS records to a route53 zone: each module instance contributes one A
record to a route53 zone where all of the other settings are always the same. If we can gather that information all together into a simpler data structure then that could all potentially be reduced to a single resource "aws_route53_record"
block with one instance for each recordset.
To achieve that we need to think about how to transform the various source module data into the shape needed to represent the DNS recordsets. That ultimately means a collection that has one element for each recordset, where the elements each describe the name
, zone_id
, and the IP address to write into records
.
The zone IDs seem to vary only by the target platform (AWS vs Azure) so I think my first step would be to build two separate collections of records per platform so that we don’t need to worry yet about the zone IDs and fully-qualified domain names:
locals {
aws_dns_records = merge(
{ for i, m in module.aws_client : "aws-client${i}" => m.public_ip },
{ for i, m in module.aws_client : "aws-client${i}-priv" => m.private_ip },
{ for i, m in module.aws_server : "aws-server${i}" => m.public_ip },
{ for i, m in module.aws_server : "aws-server${i}-priv" => m.private_ip },
)
azure_dns_records = merge(
{ for i, m in module.azureclient : "azure-client${i}" => m.public_ip.ip_address },
{ for i, m in module.azureclient : "azure-client${i}-priv" => m.nic.private_ip_address },
{ for i, m in module.azureserver : "azure-server${i}" => m.public_ip.ip_address },
{ for i, m in module.azureserver : "azure-server${i}-priv" => m.nic.private_ip_address },
)
}
The general goal here is to discard the parts of these module instances that are not relevant to the DNS records and focus only normalizing away the little differences in how they are structured between the modules. The end result of both of these should be a flat map from hostname to IP address, because it’s constructing one mapping for each module (using for
expressions) and then merge
-ing them all together into a single map. The hostname structures are different for each so there should be no conflicts during the merge.
With both of those now in a regular structure, we can follow a similar pattern to merge those together to produce a single local value with all of the DNS records in it:
locals {
dns_records = tomap(merge(
{
for n, ip in local.aws_dns_records :
"${n}.${data.aws_route53_zone.awslinuxdns.name}" => {
zone_id = data.aws_route53_zone.awslinuxdns.zone_id
ip_address = ip
}
},
{
for n, ip in local.azure_dns_records :
"${n}.${data.aws_route53_zone.azurelinuxdns.name}" => {
zone_id = data.aws_route53_zone.azurelinuxdns.zone_id
ip_address = ip
}
},
))
}
Again here the goal is to isolate the ways in which these two cases differ – the domain name and the zone ID – and normalize that away so that all of them have the same structure and meaning. The result of this is a map from fully qualified domain names to objects that contain both the target zone ID and the IP address.
This final data structure now fully represents all of the systematic differences between your DNS records, and does so in a way that’s compatible with resource for_each
, and so it should be possible to declare them all together with a single resource block:
resource "aws_route53_record" "servers" {
for_each = local.dns_records
zone_id = each.value.zone_id
name = each.key
type = "A"
ttl = "1"
records = [each.value.ip_address]
}
Due to the design of resource for_each
, Terraform will identify each of the instances of this resource by the keys in the map, which are the fully-qualified domain names. If we assume that your AWS DNS zone is called example.com
and your Azure one is called example.net
then you’d end up with resource instances with addresses like this:
aws_route53_record.servers["aws-client0.example.com"]
aws_route53_record.servers["aws-client1.example.com"]
aws_route53_record.servers["aws-client0-priv.example.com"]
aws_route53_record.servers["aws-client1-priv.example.com"]
aws_route53_record.servers["azure-client0.example.net"]
aws_route53_record.servers["azure-client1.example.net"]
aws_route53_record.servers["azure-client0-priv.example.net"]
aws_route53_record.servers["azure-client1-priv.example.net"]
I hope that this has at least given some ideas about how you can use transformation expressions to pull out the common elements of various separately-declared objects so that you can use them systematically. I don’t see any other opportunities for designs like this in the module you’ve shared here, but this approach of starting with a bunch of things that all are all separate due to essential differences and then using expressions to normalize them into a simpler, common structure to use elsewhere is a common technique for reducing redundancy and repetition in Terraform modules.