Skip to content

Instantly share code, notes, and snippets.

@thom-vend
Last active December 13, 2022 23:12
Show Gist options
  • Save thom-vend/231682561e4440b77ec4b2187a73b086 to your computer and use it in GitHub Desktop.
Save thom-vend/231682561e4440b77ec4b2187a73b086 to your computer and use it in GitHub Desktop.
Script to import in terraform route53 dns zone and record when terraformer isn't working as expected
#!/usr/bin/env bash
# WARNING: You probably want to look at https://github.com/GoogleCloudPlatform/terraformer
# But was't working great for me so I end-up searching for a more custom solution
# Script isn't perfect but as it was a one shot operation....
# Script work on my machine TM, using some gnu core utils like gsed, gwc etc
# refactor of https://www.daniloaz.com/en/how-to-quickly-import-all-records-from-a-route53-dns-zone-into-terraform/
set -x
set -euo pipefail
# This script retrieves all DNS records from AWS Route53 DNS zone and imports all of them to Terraform
# required positional args
if [[ -z "${1:-}" ]];then
echo "usage: $0 zone-name"
exit 1
fi
zone_name=$1
shift
# Options
run_import=0
clean_import=0
much_match=0
search_file=""
while [[ $# -gt 0 ]];
do
case "$1" in
--import)
run_import=1
shift
;;
--clean)
clean_import=1
shift
;;
--grep-zone)
# Check if the record is present in this file
if ! [[ -f "$2" ]];then
echo "Wrong usage of --grep option ; usage --grep-zone /path/to/zone/file"
exit 1
fi
search_file=$2
much_match=1
shift 2
esac
done
# Get zone slug from zone name
zone_slug=$(echo "${zone_name}" | tr '.' '-')
zone_slug=${zone_slug%-}
###########################################################
# Helpers
###########################################################
function is_record_targeted {
# 0=yes ; 1=no
local name=${1%.}
local type=$2
# route53 allowed type:
# A, AAAA, CAA, CNAME, DS, MX, NAPTR, NS, PTR, SOA, SPF, SRV and TXT.
if [[ $much_match -eq 0 ]] || [[ ! "$type" =~ ^A|AAAA|CAA|CNAME$ ]];then
return 0 # skip
fi
if [[ "$name" == "${zone_name%.}" ]];then
name="@"
fi
if grep "^${name}" "$search_file" || \
grep -q "^${name/.${zone_name%.}/}" "$search_file"
then
return 0
fi
return 1
}
function run_terraform_import {
tf_name=$1
aws_id=$2
log_id="${tf_name}_${aws_id}"
if ! grep -qF "${log_id}" .imported.log && [[ $run_import -eq 1 ]];then
if [[ $clean_import -eq 1 ]];then
terraform state rm "$tf_name" ||true
fi
terraform import "$tf_name" "$aws_id"
echo "${log_id}" >> .imported.log
fi
}
function generate_code_record {
dns_record=$1
is_alias=$2
zone_slug=$3
name="$(echo "${dns_record}" | jq -r '.Name'|gsed -r 's/\\052/\*/g')"
type="$(echo "${dns_record}" | jq -r '.Type')"
ttl="$(echo "${dns_record}" | jq -r '.TTL')"
setidentifier="$(echo "${dns_record}" | jq -r '.SetIdentifier')"
weight="$(echo "${dns_record}" | jq -r '.Weight')"
setidentifier_import="_$setidentifier"
if [[ "$setidentifier" == "null" ]];then
setidentifier=""
setidentifier_import=""
fi
name_slug="$(echo "${type}-${name}-${zone_slug}${setidentifier_import}" | gsed -E -e 's/[\._\ ]+/-/g' -e 's/(^-|-$)//g' -e 's/\*/wildcard/g')"
if [[ "$ttl" != "null" ]];then
ttl='"'"$ttl"'"'
fi
if ! is_record_targeted "$name" "$type"; then
echo "DEBUG: record skipped: $name $type"
return 0 # skip
fi
cat << EOF >> "dns-zone-${zone_slug}.tf"
resource "aws_route53_record" "${name_slug}" {
zone_id = aws_route53_zone.${zone_slug}.zone_id
name = "${name}"
type = "${type}"
ttl = ${ttl}
EOF
if [[ -n "$setidentifier" ]];then
echo " set_identifier = \"${setidentifier}\"" >> "dns-zone-${zone_slug}.tf"
fi
if [[ "$weight" != "null" ]];then
cat <<EOF >> "dns-zone-${zone_slug}.tf"
weighted_routing_policy {
weight = $weight
}
EOF
fi
if [[ "$is_alias" -eq 1 ]];then
alias_name="$(echo "${dns_record}" | jq -cr '.AliasTarget' | jq -r '.DNSName')"
alias_hosted_zone_id="$(echo "${dns_record}" | jq -cr '.AliasTarget' | jq -r '.HostedZoneId')"
evaluate_target_health="$(echo "${dns_record}" | jq -cr '.AliasTarget' | jq -r '.EvaluateTargetHealth')"
cat <<EOF >> "dns-zone-${zone_slug}.tf"
alias {
name = "${alias_name}"
zone_id = "${alias_hosted_zone_id}"
evaluate_target_health = ${evaluate_target_health}
}
EOF
else
# Handle TXT record magic quote from TF
echo ' records = [' >> "dns-zone-${zone_slug}.tf"
if [[ "$type" =~ ^TXT$ ]]; then
# handle quotes for terraform
# terraform magically quote TXT record so we don't have to
# But if we TXT container multiple section with double quote then we have to:
# add all the required quote and escape them for the TF code to be valid.
records="$(echo "${dns_record}" | jq -c -r '.ResourceRecords|.[].Value')"
while read -r rec
do
if [[ "$(tr -dc '"' <<<"$rec"|gwc -c)" -gt 2 ]];then
# sed: delete starting quote and end quote, escape all others
echo "\"$(gsed -r -e 's/^"//g' -e 's/"$//g' -e 's/"/\\"/g' <<<"${rec}")\"," >> "dns-zone-${zone_slug}.tf"
else
echo "${rec}," >> "dns-zone-${zone_slug}.tf"
fi
done <<<"${records}"
else
echo "${dns_record}" | jq -c '.ResourceRecords|.[].Value' |gsed -r 's/$/,/g' >> "dns-zone-${zone_slug}.tf"
fi
echo " ]" >> "dns-zone-${zone_slug}.tf"
fi
cat <<EOF >> "dns-zone-${zone_slug}.tf"
lifecycle {
ignore_changes = [weighted_routing_policy]
}
}
EOF
run_terraform_import "aws_route53_record.${name_slug}" "${zone_id}_${name}_${type}${setidentifier_import}"
}
###########################################################################3
# Get DNS zone current data from AWS
zone="$(aws route53 list-hosted-zones | jq '.HostedZones[] | select (.Name=="'${zone_name}'")')"
if [[ -z "$zone" ]];then
echo "Error: Zone not found"
exit 1
fi
zone_id="$(jq -r '.Id'<<<"$zone")"
if ! [[ "$zone_id" =~ ^/hostedzone/ ]];then
echo "Error: invalid zone id detected"
exit 1
fi
zone_id=${zone_id//\/hostedzone\/}
# Another method to get DNS zone data searching by zone name instead of zone ID
zone_comment="$(jq -r '.Comment' <<<"$zone")"
if [[ "${zone_comment}" == 'null' ]];then
zone_comment="${zone_name} zone"
fi
# Write aws_route53_zone resource to terraform file
cat << EOF > "dns-zone-${zone_slug}.tf"
resource "aws_route53_zone" "${zone_slug}" {
name = "${zone_name}"
comment = "${zone_comment}"
tags = {
"Cost Center" = "Platform"
}
}
EOF
run_terraform_import "aws_route53_zone.${zone_slug}" "${zone_id}"
# Import DNS zone and records from file to terraform
aws_cli_output=$(mktemp)
aws route53 list-resource-record-sets --hosted-zone-id "${zone_id}" |tee "$aws_cli_output"
# Retrieve all regular records (not alias) from DNS zone and write them down to terraform file
regular_records=$(mktemp)
alias_records=$(mktemp)
jq -c '.ResourceRecordSets[] | select(has("AliasTarget") | not)' "$aws_cli_output" | tee "$regular_records"
jq -c '.ResourceRecordSets[] | select(has("AliasTarget"))' "$aws_cli_output" | tee "$alias_records"
while read -r dns_record
do
generate_code_record "$dns_record" 0 "$zone_slug"
done < "$regular_records"
while read -r dns_record
do
generate_code_record "$dns_record" 1 "$zone_slug"
done < "$alias_records"
terraform fmt -diff=true -recursive
echo "done"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment