Skip to content

Instantly share code, notes, and snippets.

@kwilczynski
Last active September 14, 2024 09:20
Show Gist options
  • Save kwilczynski/1a197cbd093113c75560 to your computer and use it in GitHub Desktop.
Save kwilczynski/1a197cbd093113c75560 to your computer and use it in GitHub Desktop.
EC2 automatic DNS entry in route53 for Auto Scaling Group
TTL=300
HOSTED_ZONE_ID=
REVERSE_HOSTED_ZONE_ID=
INSTANCE_ID=
REGION=
#!/bin/sh
### BEGIN INIT INFO
# Provides: route53
# Required-Start: $local_fs $network
# Required-Stop: $local_fs $network
# Should-Start: $network
# Should-Stop: $network
# Default-Start: 2 3 5
# Default-Stop: 0 6
# Short-Description: Add and/or remove a DNS entry in Route53
### END INIT INFO
. /lib/lsb/init-functions
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
SCRIPT_NAME="/etc/init.d/$(basename -- $0)"
ROUTE53_BINARY="/usr/local/sbin/route53"
case "$1" in
start)
log_daemon_msg 'Adding a DNS entry into Route53 ...'
$ROUTE53_BINARY --add >/dev/null 2>&1
log_end_msg $?
;;
stop)
log_daemon_msg 'Removing a DNS entry from Route53 ...'
$ROUTE53_BINARY --check >/dev/null 2>&1
case $? in
0)
$ROUTE53_BINARY --remove >/dev/null 2>&1
log_end_msg $?
;;
1) log_end_msg 0 ;;
esac
;;
status)
log_daemon_msg 'Checking if a DNS entry exists in Route53 ...'
$ROUTE53_BINARY --check >/dev/null 2>&1
log_end_msg $?
;;
*)
log_action_msg "Usage: $SCRIPT_NAME {start|stop|status}" >&2
exit 3
;;
esac
"UserData": {
"Fn::Base64": {
"Fn::Join": [
"",
[
"#!/bin/bash\n",
"exec >/var/log/bootstrap.log 2>&1\n",
"EC2_AUTO_SCALING_GROUP='yes'\n",
"ROLE='docker'\n",
"DOMAIN='",
{
"Ref": "Domain"
},
"'\n",
"ORGANISATION='",
{
"Ref": "Organisation"
},
"'\n",
"PROJECT='",
{
"Ref": "Project"
},
"'\n",
"STACKNAME='",
{
"Ref": "StackName"
},
"'\n",
"ENVIRONMENT='",
{
"Ref": "Environment"
},
"'\n",
"RELEASESTAGE='",
{
"Ref": "ReleaseStage"
},
"'\n",
"BRANCH='",
{
"Ref": "Branch"
},
"'\n",
"SALT_MASTER_IP='",
{
"Fn::GetAtt": [
"InstanceMaster01",
"PrivateIp"
]
},
"'\n",
"HOSTED_ZONE_ID='",
{
"Ref": "Route53HostedZoneArn"
},
"'\n",
"export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n",
"export DOMAIN ROLE ORGANISATION PROJECT STACKNAME ENVIRONMENT RELEASESTAGE BRANCH\n",
"export EC2_AUTO_SCALING_GROUP\n",
"export SALT_MASTER_IP\n",
"export HOSTED_ZONE_ID\n",
"aws --color=off s3 cp --quiet ",
{
"Ref": "BootstrapBucketURL"
},
"compute/bootstrap.sh - | bash -s\n",
"aws --color=off s3 cp --quiet ",
{
"Ref": "BootstrapBucketURL"
},
"compute/route53/route53.init /etc/init.d/route53\n",
"chown root:root /etc/init.d/route53\n",
"chmod 755 /etc/init.d/route53\n",
"update-rc.d route53 start 99 2 3 5 . stop 99 0 6 .\n",
"aws --color=off s3 cp --quiet ",
{
"Ref": "BootstrapBucketURL"
},
"compute/route53/route53.sh /usr/local/sbin/route53\n",
"chown root:root /usr/local/sbin/route53\n",
"chmod 755 /usr/local/sbin/route53\n",
"/usr/local/sbin/route53 --add\n"
]
]
}
}
#!/bin/bash -eu
#
# route53.sh
#
# The main tasks of this script are:
#
#  - Automatically add or remove a DNS entry in Route53 based
# on *this* instance "Name" tag (assuming it was prior set
#  to a correct value);
#
# Auxiliary tasks of this script:
#
#  - Perform a look-up against the Route53 to check whether
#  a correct DNS entry exists there already, and report back.
#
# This script is to be installed as /usr/local/sbin/route53.
#
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
readonly ROUTE53_DEFAULT='/etc/default/route53'
readonly EC2_METADATA_URL='http://169.254.169.254/latest/meta-data'
readonly LOCK_FILE="/var/lock/$(basename -- "$0").lock"
# Make sure files are 644 and directories are 755.
umask 022
[[ -e "/proc/$(cat $LOCK_FILE 2>/dev/null)" ]] || rm -f $LOCK_FILE
# Load environment variables that are mandatory.
if [[ -f $ROUTE53_DEFAULT ]]; then
# Necessary details (e.g. Hosted Zone ID, etc.) should
# have been passed down in the bootstrap process.
source $ROUTE53_DEFAULT
else
echo "Unable to load environment variables from '$ROUTE53_DEFAULT', aborting..."
exit 1
fi
# Add, remove or check.
ACTION='UNKNOWN'
case "${1:-$ACTION}" in
# Add (when missing) or update.
-a|--add) ACTION='UPSERT' ;;
-r|--remove) ACTION='DELETE' ;;
-c|--check) ACTION='CHECK' ;;
-h|--help)
cat <<EOS | tee
Automatically add, remove or check a DNS entry in Route53 for this instance.
Usage:
$(basename -- "$0") <OPTION>
Options:
--add -a Add a DNS entry into Route53.
--remove -r Remove a DNS entry from Route53.
--check -c Check if a DNS entry exists in Route53.
--help -h This help screen.
EOS
exit 1
;;
*)
echo "Unknown or no action given, aborting..."
exit 1
;;
esac
# Check if environment variables are present and non-empty.
REQUIRED=( TTL HOSTED_ZONE_ID INSTANCE_ID REGION )
for v in ${REQUIRED[@]}; do
eval VALUE='$'${v}
if [[ -z $VALUE ]]; then
echo "The '$v' environment variable has to be set, aborting..."
exit 1
fi
done
if (set -o noclobber; echo $$ > $LOCK_FILE) &>/dev/null; then
# Make a secure temporary file name, needed later.
TEMPORARY_FILE=$(mktemp -ut "$(basename $0).XXXXXXXX")
# Make sure to remove the temporary
# file when terminating, and clean-up
# the lock-file too.
trap \
"rm -f $LOCK_FILE $TEMPORARY_FILE; exit" \
HUP INT KILL TERM QUIT EXIT
# Fetch current private IP address of this instance.
INSTANCE_IPV4=$(curl -s ${EC2_METADATA_URL}/local-ipv4)
# Fetch current "Name" tag that was set for this
# instance, as it will be used when adding (or
# updating) a new DNS entry (of a type "A") in
# Route53 service. The premise is that whatever
# the aforementioned tag is, then the DNS entry
# should be exactly the same.
INSTANCE_NAME_TAG=$(
aws ec2 describe-tags \
--query 'Tags[*].Value' \
--filters "Name=resource-id,Values=${INSTANCE_ID}" 'Name=key,Values=Name' \
--region $REGION --output text 2>/dev/null
)
# Make sure that the "Name" tag was actually set.
if [[ "x${INSTANCE_NAME_TAG}" == "x" ]]; then
echo "The 'Name' tag is empty or has not been set, aborting..."
exit 1
fi
if [[ $ACTION == 'CHECK' ]]; then
# Fetch details (about every resource) about given Hosted
# Zone from Route53 and format to make it easier to search
# for a particular entry. Since the amount of records can
# often be quiet large, store it in a temporary file.
aws --color=off route53 list-resource-record-sets \
--query 'ResourceRecordSets[*].[Type,TTL,Name,ResourceRecords[0].Value]' \
--hosted-zone-id $HOSTED_ZONE_ID --region $REGION --output text | \
sed -e 's/\s/,/g' 2>/dev/null > $TEMPORARY_FILE
# Assemble entry for this instance.
RESOURCE=$(printf "%s,%s,%s.,%s" "A" "$TTL" "$INSTANCE_NAME_TAG" "$INSTANCE_IPV4")
if grep -q $RESOURCE $TEMPORARY_FILE &>/dev/null; then
# Found? Then print using the JSON that can used to
# make a change request against Reoute53, if needed.
cat <<EOF
{
"ResourceRecordSet": {
"Name": "${INSTANCE_NAME_TAG}.",
"Type": "A",
"TTL": ${TTL},
"ResourceRecords": [
{
"Value": "${INSTANCE_IPV4}"
}
]
}
}
EOF
exit 0
fi
# Nothing to show? Then make
# it a non-clean exit.
exit 1
else
# Render details of the request (or a "change",
# rather) which is going to be sent to Route53.
# Note that the "UPSERT" action will both add
# the entry if it does not exist yet or update
# current value accordingly. Better option over
# the "CREATE" action (which in turn would fail
# if an entry exists already).
cat <<EOF | tee $TEMPORARY_FILE
{
"Changes": [
{
"Action": "${ACTION}",
"ResourceRecordSet": {
"Name": "${INSTANCE_NAME_TAG}.",
"Type": "A",
"TTL": ${TTL},
"ResourceRecords": [
{
"Value": "${INSTANCE_IPV4}"
}
]
}
}
]
}
EOF
# Route53 has a queue for incoming change requests.
# When a task is placed in a queue successfully,
# then a "Change ID" which represents a batch job
# will be given back, and it can be used to track
# progress (although, IAM role needs to be set
# appropriately to allow access, etc.).
aws route53 change-resource-record-sets \
--hosted-zone-id $HOSTED_ZONE_ID \
--change-batch file://${TEMPORARY_FILE} \
--region $REGION
fi
rm -f $LOCK_FILE $TEMPORARY_FILE &>/dev/null
# Reset traps to their default behaviour.
trap - HUP INT KILL TERM QUIT EXIT
else
echo "Unable to create lock file (current owner: "$(cat $LOCK_FILE 2>/dev/null)")."
exit 1
fi
#!/bin/bash
set -e
set -u
set -o pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
readonly ROUTE53_DEFAULT='/etc/default/route53'
readonly LOCK_FILE="/var/lock/$(basename -- "$0").lock"
readonly EC2_METADATA_URL='http://169.254.169.254/latest/meta-data'
# Make sure files are 644 and directories are 755.
umask 022
[[ -e "/proc/$(cat $LOCK_FILE 2>/dev/null)" ]] || rm -f $LOCK_FILE
# Load environment variables that are mandatory.
if [[ -f $ROUTE53_DEFAULT ]]; then
# Necessary details (e.g. Hosted Zone ID, etc.) should
# have been passed down in the bootstrap process.
source $ROUTE53_DEFAULT
else
echo "Unable to load environment variables from '$ROUTE53_DEFAULT', aborting..."
exit 1
fi
# Add, remove or check.
ACTION='UNKNOWN'
case "${1:-$ACTION}" in
# Add (when missing) or update.
-a|--add) ACTION='UPSERT' ;;
-r|--remove) ACTION='DELETE' ;;
-c|--check) ACTION='CHECK' ;;
-h|--help)
cat <<EOS | tee
Automatically add, remove or check a DNS entry in Route53 for this instance.
Usage:
$(basename -- "$0") <OPTION>
Options:
--add -a Add a DNS entry into Route53.
--remove -r Remove a DNS entry from Route53.
--check -c Check if a DNS entry exists in Route53.
--help -h This help screen.
Note: By default, the forward (A) and reverse (PTR)
resource records are added into Rotue53.
EOS
exit 1
;;
*)
echo "Unknown or no action given, aborting..."
exit 1
;;
esac
# Check if environment variables are present and non-empty.
REQUIRED=(TTL PRIVATE_ZONE_ID REVERSE_ZONE_ID INSTANCE_ID REGION)
for v in ${REQUIRED[@]}; do
eval VALUE='$'${v}
if [[ -z $VALUE ]]; then
echo "The '$v' environment variable has to be set, aborting..."
exit 1
fi
done
if (set -o noclobber; echo $$ > $LOCK_FILE) &>/dev/null; then
# Make a secure temporary file name, needed later.
TEMPORARY_FILE=$(mktemp -ut "$(basename $0).XXXXXXXX")
# Make sure to remove the temporary
# file when terminating, and clean-up
# the lock-file too.
trap \
"rm -f $LOCK_FILE $TEMPORARY_FILE; exit" \
HUP INT KILL TERM QUIT EXIT
if [[ "x${INSTANCE_ID}" == "x" ]]; then
# Fetch the EC2 instance ID.
INSTANCE_ID=$(curl -s ${EC2_METADATA_URL}/instance-id)
fi
# Fetch current private IP address of this instance.
PRIVATE_IP_ADDRESS=$(curl -s ${EC2_METADATA_URL}/local-ipv4)
# Make the in-addr.arpa. address for this instance.
INSTANCE_PTR=$(echo "$(printf '%s.' $PRIVATE_IP_ADDRESS | tac -s'.')in-addr.arpa.")
# Fetch current "Name" tag that was set for this
# instance, as it will be used when adding (or
# updating) a new DNS entry (of a type "A") in
# Route53 service. The premise is that whatever
# the aforementioned tag is, then the DNS entry
# should be exactly the same.
INSTANCE_NAME_TAG=$(aws ec2 describe-tags \
--query 'Tags[*].Value' \
--filters "Name=resource-id,Values=${INSTANCE_ID}" 'Name=key,Values=Name' \
--region $REGION --output text 2>/dev/null)
# Make sure that the "Name" tag was actually set.
if [[ "x${INSTANCE_NAME_TAG}" == "x" ]]; then
echo "The 'Name' tag is empty or has not been set, aborting..."
exit 1
fi
if [[ $ACTION == 'CHECK' ]]; then
# Keep a track of resource records.
SEEN_RESOURCES=0
# Fetch details (about every resource) about given Hosted
# Zone (both forward and reverse) from Route53 and format
# to make it easier to search for a particular entry,
# and filter the A and PTR resource records only.
for zone in PRIVATE_ZONE_ID REVERSE_ZONE_ID; do
eval VALUE='$'${zone}
aws route53 list-resource-record-sets \
--query 'ResourceRecordSets[*].[Type,TTL,Name,ResourceRecords[0].Value]' \
--hosted-zone-id $VALUE \
--region $REGION --output text | \
grep -E '^(A|PTR)' | sed -e 's/\s/,/g' | \
tee -a $TEMPORARY_FILE >/dev/null || true
done
# Assemble the A resource record for this instance.
RESOURCE=$(printf "%s,%s,%s.,%s" "A" "$TTL" "$INSTANCE_NAME_TAG" "$PRIVATE_IP_ADDRESS")
if grep -q $RESOURCE $TEMPORARY_FILE &>/dev/null; then
echo $RESOURCE | awk -F',' '{ print $3, $1, $4, $2 }'
SEEN_RESOURCES+=1
fi
# Assemble the PTR resource record for this instance.
RESOURCE=$(printf "%s,%s,%s,%s" "PTR" "$TTL" "$INSTANCE_PTR" "$INSTANCE_NAME_TAG")
if grep -q $RESOURCE $TEMPORARY_FILE &>/dev/null; then
echo $RESOURCE | awk -F',' '{ print $3, $1, $4, $2 }'
SSEEN_RESOURCESEEN+=1
fi
if (( $SEEN_RESOURCES < 1 )); then
# If there is nothing to show or a records are missing,
# then we make it a non-clean exit.
echo "No resource records found, aborting..." >&2
exit 1
fi
exit 0
else
# Render details of the request (or a "change",
# rather) which is going to be sent to Route53.
# Note that the "UPSERT" action will both add
# the entry if it does not exist yet or update
# current value accordingly. Better option over
# the "CREATE" action (which in turn would fail
# if an entry exists already).
cat <<EOF > $TEMPORARY_FILE
{
"Changes": [
{
"Action": "${ACTION}",
"ResourceRecordSet": {
"Name": "${INSTANCE_NAME_TAG}.",
"Type": "A",
"TTL": ${TTL},
"ResourceRecords": [
{
"Value": "${PRIVATE_IP_ADDRESS}"
}
]
}
}
]
}
EOF
# Route53 has a queue for incoming change requests.
# When a task is placed in a queue successfully,
# then a "Change ID" which represents a batch job
# will be given back, and it can be used to track
# progress (although, IAM role needs to be set
# appropriately to allow access, etc.).
#
# Add the A resource record into Route53.
aws route53 change-resource-record-sets \
--hosted-zone-id $PRIVATE_ZONE_ID \
--change-batch file://${TEMPORARY_FILE} \
--region $REGION
cat <<EOF > $TEMPORARY_FILE
{
"Changes": [
{
"Action": "${ACTION}",
"ResourceRecordSet": {
"Name": "${INSTANCE_PTR}",
"Type": "PTR",
"TTL": ${TTL},
"ResourceRecords": [
{
"Value": "${INSTANCE_NAME_TAG}"
}
]
}
}
]
}
EOF
# Add the PTR resource record into Route53.
aws route53 change-resource-record-sets \
--hosted-zone-id $REVERSE_ZONE_ID \
--change-batch file://${TEMPORARY_FILE} \
--region $REGION
fi
rm -f $LOCK_FILE $TEMPORARY_FILE &>/dev/null
# Reset traps to their default behaviour.
trap - HUP INT KILL TERM QUIT EXIT
else
echo "Unable to create lock file (current owner: "$(cat $LOCK_FILE 2>/dev/null)")."
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment