Terraform Import for AWS Route53 Records: Automating Bulk Imports and State Migration

Importing existing AWS Route53 records into Terraform state is one of the most common friction points when moving from console-driven DNS management to Infrastructure as Code. Route53 zones accumulate records over time, often with a mix of A, MX, CNAME, TXT and routing-policy records. Manually writing a resource block for each record and running terraform import individually is tedious and error prone. The reference patterns for handling this at scale involve understanding the import ID format, pre-generating resource configuration, and automating the import and state move workflow.

Manual Import Workflow

The manual way starts with a definition that mirrors the existing record.

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", ] }

Once the definition exists, Terraform can be told to bind that configuration to an existing remote object.

terraform import aws_route53_record.cogvio_com_MX Z4KAPRWWNC7JR_cogvio_com_MX

Terraform loads the state for that address and from that point forward manages the record. The same principle applies to every record type, but the import ID differs.

A frequent complaint with this approach is volume. A hosted zone with a lot of records requires writing the definition first and then running the import command over and over for each resource individually. For a handful of records the manual method works well. For a whole bunch of records the workflow becomes unbearable without scripting.

Import ID Format and Address Conventions

The import command syntax is:

terraform import [options] ADDRESS ID

ADDRESS must be a valid resource address. Because any resource address is valid, the import command can import resources into modules as well as directly into the root of your state.

ID is dependent on the resource type being imported. For example, for AWS EC2 instances it is the instance ID, but for AWS Route53 zones it is the zone ID. Please reference the provider documentation for details on the ID format.

For Route53 records the import ID format is:

{zone_id}_{name}_{type}

with {set_identifier} appended for routing-policy records.

Examples:

terraform import 'aws_route53_record.web' Z1234567890_example.com_A terraform import 'aws_route53_record.primary' Z1234567890_app.example.com_A_primary

The warning from the import command reference remains relevant: Terraform expects that each remote object it is managing will be bound to only one resource address, which is normally guaranteed by Terraform itself having created all objects. If you import existing objects into Terraform, be careful to import each remote object to only one Terraform resource address. If you import the same object multiple times, Terraform may exhibit unwanted behavior.

Resource Address Example Import ID Example Notes
awsroute53record awsroute53record.cogviocomMX Z4KAPRWWNC7JRcogviocom_MX Standard record
awsroute53record awsroute53record.web Z1234567890example.comA Simple A record
awsroute53record awsroute53record.primary Z1234567890app.example.comA_primary Routing policy with set identifier

Scripted Bulk Import Approach

When working with custom Terraform modules and a whole bunch of records to be imported, scripting the entire workflow saves time and reduces errors. The task consists of three parts.

  1. Import all existing records in a hosted zone using AWS CLI

aws route53 list-resource-record-sets --hosted-zone-id XXX > data/company-tld.json

The file is loaded into a dict for processing.

def load_records(zone_file=ZONE_FILE): with open(zone_file) as record_file: data = json.load(record_file) return data

  1. Import the record in Terraform state

To do this, Terraform CLI comes with an import command. However for import to work, you need to have a resource declaration in your Terraform file already.

From the official documentation:

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.

To overcome this restriction, a dummy configuration is written programmatically for each record.

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))

AWS Route53 module can import awsroute53record as described. The command is run as a subprocess.

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)

  1. Optional move resources in a module

In case you are using a Module to manage AWS Route53 resources, you will need to move the declaration from resource to module configuration block.

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 full loop iterates over the records returned by the AWS CLI.

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 tiny Python script helps transition AWS Route53 records neatly and effortlessly.

Using Import Blocks and Generate Config

Terraform also supports import blocks to declare imports declaratively.

// import.tf import { to = aws_route53_record.example id = "{hostedZoneId}_{recordName}_{Type}" }

Run tf command to output config to generated_resources.tf

terraform plan -generate-config-out=generated_resources.tf

This approach pairs with the import command reference which states that terraform import command imports existing resources into Terraform. Refer to Import for additional information. Hands-on: Try the Import Terraform Configuration tutorial.

The import block method is useful when you want to generate configuration from existing state rather than writing dummy blocks by hand.

Moving Resources into Modules

After import, resources often need to live inside a module. The state move command relocates the resource address.

terraform state mv aws_route53_record.<resource_name> 'module.<MODULE_NAME>.aws_route53_record.route53_record["<resource_name>-<resource_type>"]'

This ensures the imported record is managed under the module path and subsequent plans do not show drift.

A typical workflow is:

  • List records with AWS CLI
  • Generate dummy resource blocks
  • Import each record with the correct ID
  • Move each resource into the module address

The sequence keeps state consistent and avoids duplicate management.

Best Practices and Pitfalls

  • Use variables for zone IDs. Don't hardcode zone IDs. Hardcoding makes the import scripts brittle across environments.
  • Ensure each remote object is imported to only one Terraform resource address. Importing the same object multiple times causes unwanted behavior.
  • Write a resource configuration block for the resource before running terraform import. The import command requires a target address to exist in configuration.
  • For routing-policy records, append the set identifier to the import ID. The format becomes {zoneid}{name}{type}{set_identifier}.
  • When importing a whole hosted zone, prefer scripting over manual repetition. The three part pattern of list, template, import, and optionally move scales well.
  • Validate the JSON output from aws route53 list-resource-record-sets before feeding it into the script. Record names may need sanitization for valid Terraform identifiers.
  • After import, run terraform plan to verify no diff is generated. A correct import should result in an empty plan for that resource.

Conclusion

Importing existing AWS Route53 records into Terraform is straightforward for a few records and becomes a systematic automation problem at scale. The import ID format {zoneid}{name}_{type} with optional set identifier for routing-policy records is the key to binding remote DNS objects to Terraform addresses. Manual import works for small sets, while scripted bulk import with AWS CLI listing, programmatic dummy resource generation, and terraform import plus terraform state mv provides a repeatable path for large zones and module-based architectures. Import blocks and generate-config offer a declarative alternative for config generation. Keeping zone IDs in variables, avoiding duplicate imports, and verifying with plan after import ensures a safe transition from console-managed DNS to IaC managed state.

Sources

  1. mrkaran.dev
  2. filip-prochazka.com
  3. oneuptime.com
  4. dev.to
  5. developer.hashicorp.com

Related Posts