Importing existing AWS Route53 records into Terraform is a common migration task for teams moving from console-driven DNS management to Infrastructure as Code. Route53 zones often contain dozens to hundreds of records, and the manual workflow of writing a resource block then running terraform import for each record quickly becomes unbearable. A scripted approach based on the AWS CLI export and programmatic generation of dummy Terraform configuration removes the repetitive work while preserving the correct import identifiers that Route53 expects.
The problem is compounded by the way Terraform requires a resource declaration to exist before an import can succeed. Because of this, prior to running terraform import it is necessary to write manually a resource configuration block for the resource, to which the imported object will be mapped. When working with custom Terraform modules and a whole bunch of records to be imported, scripting the entire workflow becomes the practical solution.
Exporting the Hosted Zone with AWS CLI
The first step is to obtain a machine-readable snapshot of the current DNS state. The AWS CLI command aws route53 list-resource-record-sets dumps all record sets for a hosted zone into JSON.
aws route53 list-resource-record-sets --hosted-zone-id XXX > data/company-tld.json
The output is a JSON document containing an array of ResourceRecordSets. Each entry includes the record name, type, TTL, and values. Versioning this JSON file in a git repo allows easy diffing after the import process completes.
A small Python helper loads the export:
python
def load_records(zone_file=ZONE_FILE):
with open(zone_file) as record_file:
data = json.load(record_file)
return data
The records are then iterated with records.get("ResourceRecordSets") to extract Name and Type for each entry. The script can be configured to skip NS and SOA records because they are often managed more dynamically with allow_overwrite.
Preparing Dummy Terraform Configuration
Terraform CLI comes with an import command, however for import to work, you need to have a resource declaration in your Terraform file already. To overcome this restriction, a dummy.tf file is created programmatically and the configuration block for each record is written before the import runs.
python
def template_dummy_file(resource_name):
add_dummy_record = Template(
"""
resource "aws_route53_record" "$resource_name" {
# (resource arguments)
}
"""
)
dummy_file_path = path.join(TERRAFORM_DIR, "dummy.tf")
with open(dummy_file_path, "a") as f:
f.write(add_dummy_record.substitute(resource_name=resource_name))
The dummy block satisfies Terraform's requirement for a pre-existing resource. The actual arguments such as zone_id, name, type, ttl, and records can be filled later by the module or by a generation step that writes the full resource definitions to a file such as route53_cogvio_com_records.tf.
Importing Records into Terraform State
AWS Route53 module can import aws_route53_record as described. The import identifier for a Route53 record is composed of the hosted zone ID, the record name, and the record type.
python
def terraform_import(resource_name, resource_type):
import_command = f"terraform import aws_route53_record.{resource_name} {ZONE_ID}_{resource_name}_{resource_type}"
run(import_command, shell=True, check=True)
The command is executed as a subprocess in the host OS. For example, a manual import looks like:
terraform import aws_route53_record.cogvio_com_MX Z4KAPRWWNC7JR_cogvio_com_MX
After the import, Terraform loads the state and associates the existing AWS object with the declaration. The manual way requires first writing the resource definition itself:
hcl
resource "aws_route53_record" "cogvio_com_MX" {
zone_id = aws_route53_zone.cogvio_com.zone_id
name = "cogvio.com."
type = "MX"
ttl = 300
records = [
"1 aspmx.l.google.com",
"5 alt1.aspmx.l.google.com",
"5 alt2.aspmx.l.google.com",
"10 aspmx2.googlemail.com",
"10 aspmx3.googlemail.com",
]
}
The script generates semi-usable names for resources and the output can be reviewed and fixed manually. It is still less work than writing it manually from scratch.
Moving Resources into a Module
In case you are using a Module to manage AWS Route53 resources, you’ll need to move the declaration from resource to module configuration block. This is done with terraform state mv.
python
def terraform_move(resource_name, resource_type):
mv_command = f"terraform state mv aws_route53_record.{resource_name} 'module.{MODULE_NAME}.aws_route53_record.route53_record[\"{resource_name}-{resource_type}\"]'"
run(mv_command, shell=True, check=True)
The workflow for a full run is:
python
if __name__ == "__main__":
missing = check_env_vars()
if missing:
exit(f"Required env variable {missing} is missing.")
records = load_records()
for i in records.get("ResourceRecordSets"):
resource_name = i.get("Name")
resource_type = i.get("Type")
template_dummy_file(resource_name)
terraform_import(resource_name, resource_type)
terraform_move(resource_name, resource_type)
print(f"Imported {resource_name}")
This imports each record, then moves it into the module address expected by the current configuration.
Zone Import and Dynamic NS and SOA Records
You should first write the definition for the Route53 zone and import it. This would be a waste to automate since it’s only a single resource per domain.
terraform import aws_route53_zone.cogvio_com Z4KAPRWWNC7JR
hcl
resource "aws_route53_zone" "cogvio_com" {
name = "cogvio.com"
}
NS and SOA records are often skipped during automated generation because they are written with allow_overwrite to remain dynamic.
hcl
resource "aws_route53_record" "cogvio_com_nameservers" {
zone_id = aws_route53_zone.cogvio_com.zone_id
name = "${aws_route53_zone.cogvio_com.name}."
type = "NS"
ttl = 172800
records = [
"${aws_route53_zone.cogvio_com.name_servers[0]}.",
"${aws_route53_zone.cogvio_com.name_servers[1]}.",
"${aws_route53_zone.cogvio_com.name_servers[2]}.",
"${aws_route53_zone.cogvio_com.name_servers[3]}.",
]
allow_overwrite = true
}
hcl
resource "aws_route53_record" "cogvio_com_soa" {
zone_id = aws_route53_zone.cogvio_com.zone_id
name = "${aws_route53_zone.cogvio_com.name}."
type = "SOA"
ttl = 900
records = [
"${aws_route53_zone.cogvio_com.name_servers[0]}. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400",
]
allow_overwrite = true
}
Keeping NS and SOA under Terraform control with allow_overwrite avoids import drift when AWS updates name servers.
Module Options for Route53 Records
A Terraform module can create a Route53 record in AWS with a flexible way to configure various types of routing policies for DNS records.
The module supports:
- Creates a Route53 record with the specified zone ID, name, type, TTL, and records
- Supports optional attributes
- Supports different routing policies including geolocation, failover, latency, weighted, and CIDR-based routing
- Provides the option for alias records that can point to AWS resources using their AWS resource name
- Allows only one routing policy block to be supplied
- Adheres to security best practices by leveraging automated scanning with Checkov
Example minimum usage:
hcl
module "minimum_example" {
source = "boldlink/route53-records/aws"
version = "insert_latest_version"
zone_id = local.zone_id
name = var.name
type = var.type
ttl = var.ttl
records = var.records
}
Module Requirements
| Name | Version |
|---|---|
| terraform | >= 0.14.11 |
| aws | >= 4.65.0 |
Provider Versions
| Name | Version |
|---|---|
| aws | 5.23.1 |
Resources
| Name | Type |
|---|---|
| awsroute53record.main | resource |
Input Variables
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| alias | (Optional) An alias block. Conflicts with ttl & records | any | {} | no |
| allow_overwrite | Allow creation of this record in Terraform to overwrite an existing record, if any |
Workflow Comparison
| Step | Manual | Scripted |
|---|---|---|
| Export records | N/A | aws route53 list-resource-record-sets to JSON |
| Create resource block | Hand written per record | Programmatic dummy.tf generation |
| Import | terraform import per record |
Looped subprocess import |
| Move to module | terraform state mv per record |
Looped subprocess move |
| Review | Manual | Generate route53_*_records.tf for review |
The scripted approach is not pretty, but it is run once and never again for a given zone.
Practical Considerations
When you start with a cloud, you rarely get everything just right on the first try. Most projects begin with IaC after they’ve already been using AWS for some time which means you’ll have a bunch of resources that have been created using the AWS Console, and they have to be imported into Terraform.
Route53 is extra tricky because you can easily create a lot of resources. It can quickly become unbearable to manually import because, as with any Terraform resource, you have to first write the definition, and then you run the import command over and over for each resource individually.
A generated file can be reviewed before applying. The script expects the JSON with routes to be in the route53_cogvio_com.json file, and it generates the output into route53_cogvio_com_records.tf. The script also tries to generate semi-usable names for the resources. You should be able to modify it further if you have a convention in mind.
Conclusion
Importing existing Route53 records into Terraform at scale requires three coordinated actions: exporting the current state with the AWS CLI, ensuring a placeholder resource exists for each record so terraform import can succeed, and optionally moving the imported objects into the module address space used by the codebase. Automating dummy file creation and the import command eliminates repetitive manual editing while preserving the exact import identifiers Route53 expects. Skipping NS and SOA records for dynamic management and importing the hosted zone first provides a stable foundation. The resulting state can then be managed entirely with Terraform, and future changes are applied declaratively without re-creating DNS entries.