DEV Community

Cover image for Amazon Route 53: How to automatically update IP addresses without using Elastic IPs
John Rotenstein for AWS

Posted on • Edited on

Amazon Route 53: How to automatically update IP addresses without using Elastic IPs

Here's a handy way to automatically update an A-Record in Amazon Route 53 whenever an EC2 instance changes IP address.

Scenario: You have a domain name in Amazon Route 53 pointing to an Amazon EC2 instance. However, if the instance is stopped and started, its public IP address changes. This breaks the A-Record since it is pointing to the wrong IP address.

"Wait!" you might say, "Why not simply use an Elastic IP address?"

(An Elastic IP address is a static IP address that can be assigned to resources in an Amazon VPC. The IP address stays the same until you return the Elastic IP address to AWS.)

Yes, using an Elastic IP address on the instance will prevent the A-Record from breaking. However, AWS gives a default limit of 5 Elastic IP addresses per region. You can request a limit increase, but what if you need lots of them?

As an example, a service might provide a separate Amazon EC2 instance for each customer, to ensure secure separation of data. Each customer is given a domain name to access their server (acme.example.com). The required number of Elastic IP addresses could be huge.

Fortunately, there is a fairly simple way to achieve the objective without needing any Elastic IP addresses.

Architecture

Architecture

The architecture is quite simple:

  • When the EC2 instance starts, it should get its new public IP address and update its own record in Route 53
  • The DNS name to update is stored in a Tag on the EC2 instance
  • The script should execute every time the EC2 instance starts (that is, every time it starts, not just the first boot)

Implementation

First, there should be a Record Set in Amazon Route 53 that defines the existing domain name.

Route 53 Record Set

It is currently pointing to the public IP address of an EC2 instance.

Next, add some tags to the EC2 instance that will be used by the script:

  • DNS Name: The DNS Name to associate with the instance
  • Hosted Zone ID: Uniquely identifies the Zone record in Route 53 that needs to be updated (get it from your Route 53 Hosted Zone record)

EC2 instance tags

Whenever the EC2 instance starts, it will run a script that will:

  • Grab the information from the above tags
  • Retrieve the instance's current public IP address
  • Update the Route 53 record set with the new IP address


#!/bin/bash
# Extract information about the Instance
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id/)
AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone/)
MY_IP=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4/)

# Extract tags associated with instance
ZONE_TAG=$(aws ec2 describe-tags --region ${AZ::-1} --filters "Name=resource-id,Values=${INSTANCE_ID}" --query 'Tags[?Key==`AUTO_DNS_ZONE`].Value' --output text)
NAME_TAG=$(aws ec2 describe-tags --region ${AZ::-1} --filters "Name=resource-id,Values=${INSTANCE_ID}" --query 'Tags[?Key==`AUTO_DNS_NAME`].Value' --output text)

# Update Route 53 Record Set based on the Name tag to the current Public IP address of the Instance
aws route53 change-resource-record-sets --hosted-zone-id $ZONE_TAG --change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"'$NAME_TAG'","Type":"A","TTL":300,"ResourceRecords":[{"Value":"'$MY_IP'"}]}}]}'


Enter fullscreen mode Exit fullscreen mode

To execute the script automatically each time the instance starts (as opposed to User Data scripts that only run on the first boot), put the above script in this directory:

/var/lib/cloud/scripts/per-boot/

Finally, the EC2 instance will need an IAM Role assigned that has permission to run the above commands:



{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ec2:DescribeTags",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "route53:ChangeResourceRecordSets",
            "Resource": "arn:aws:route53:::hostedzone/HOSTED-ZONE-ID"
        }
    ]
}


Enter fullscreen mode Exit fullscreen mode

How to test

To test the script, simply Stop the instance then Start it again.

This will result in a new public IP address being assigned to the instance. The script will call Amazon Route 53 to update the record set. This might take a minute to update.

Then, return to Route 53 and look at the IP address assigned to the A-Record. It should be updated with the new IP address.

Top comments (20)

Collapse
 
robkenis profile image
Rob Kenis

Nice idea, sounds very useful! But I'm still stuck with one question.
Doesn't this introduce a major security risk ? When your ec2 would get compromised, the attacker has the permissions to update your hosted zone. Because of this, he/she could potentially point your domain to their site.
Or I might be missing something, not quite sure.

Collapse
 
aws_john profile image
John Rotenstein

I love your phraseology: "When your ec2 would get compromised", rather than "If". With a pessimistic attitude like that, you'd make a good security professional! (We're hiring!)

You are correct — it is always important to grant only the permissions that are required, lest the credentials accidentally, or intentionally, are used for other purposes. The above policy limits the potential changes that a specific Hosted Zone. I'm not sure whether it is possible to lock-down further to a specific Record Set.

For the really security-minded, you could have the instance call an AWS Lambda function that makes the call on its behalf. This way, the instance would not have any permissions to change the Route 53 information. The Lambda function would require such permission, but Lambda could be considered more "locked-down" than an Amazon EC2 instance.

Collapse
 
ericljiang profile image
Eric Jiang

You should be able to use CloudWatch Events to trigger the Lambda on EC2 instance state-changes. This way you don't even need to grant permissions to Lambda. Something like aws.amazon.com/premiumsupport/know...

Collapse
 
robkenis profile image
Rob Kenis

Thank you for clearing that up! The lambda could also do some validation when updating the record set, so that the record could only point to targets that you own yourself.

Collapse
 
josephseeley profile image
Joseph Seeley • Edited

In case anybody else is concerned about this, AWS now supports resource record set permissions with Route53, so you can dial in the EC2 IAM Role policy to allow route53:ChangeResourceRecordSets only on a given record set. Back when this article was originally written, it wasn't possible:

Announcement: aws.amazon.com/about-aws/whats-new...

Examples: docs.aws.amazon.com/Route53/latest...

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:ChangeResourceRecordSets",
"Resource": "arn:aws:route53:::hostedzone/Z11111112222222333333",
"Condition": {
"ForAllValues:StringEquals":{
"route53:ChangeResourceRecordSetsNormalizedRecordNames": ["acme.example.com"]
}
}
}
]
}

Collapse
 
nonbeing profile image
Ambar • Edited

Great article.

I just tried it out, and there was just one little detail that I was banging my head against for the last hour: the shell script needs to start with a shebang like: #!/bin/bash.

Until, I made this change to the script, it was not executing properly on each boot, and I was constantly running into this error in /var/log/cloud-init.log :

2020-02-17 06:57:29,292 - util.py[WARNING]: Failed running /var/lib/cloud/scripts/per-boot/auto-bootIP-route53.sh

Enter fullscreen mode Exit fullscreen mode

I got the hint that the shebang line was needed from this:

cloudinit.util.ProcessExecutionError: Exec format error. Missing #! in script?
Enter fullscreen mode Exit fullscreen mode

that I found over here. Your script is working great now!

I'd also like to point out that you might want to change this line:
"Resource": "arn:aws:route53:::hostedzone/Z3NAOAAAA11BB22"
to something like "Resource": "arn:aws:route53:::hostedzone/<YOUR_ROUTE53_ZONE_ID_here>". Just in case someone is not alert enough to modify the literal ZoneId in their IAM policy, when copy+pasting from the article :)

Overall, super-useful post. Thank you!

Collapse
 
aws_john profile image
John Rotenstein

Oops! Sorry for causing you that problem. I've now updated the post.

Collapse
 
mayankdedhia profile image
Mayank

Is there any other setting required for the the script to run on boot? I followed exactly same steps mentioned in this article but the script doesn't fire up, no log entry in the cloud-init.log as well. I'm able to execute the script manually and the record set is getting updated.

Collapse
 
mayankdedhia profile image
Mayank

Nevermind I managed to fix it, the script had to be made executable and also had to update /etc/cloud/cloud.cfg file's - scripts-per-boot to - [scripts-per-boot, always]

Collapse
 
aycarambo profile image
Lucas Stanislas

It worked for me in 2024 by simply putting the script in .../per-boot/ like the article mentioned

Collapse
 
fgfuentes profile image
Federico Fuentes

Hi guys, took me a while but i was able to create a batch script to have this done for windows server.

Hope it helps.

@echo off

for /F %%I in ('curl -s http://169.254.169.254/latest/meta-data/instance-id/') DO SET INSTANCE_ID=%%I
for /F %%X in ('curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone/') DO SET AZ=%%X
for /F %%Y in ('curl -s http://169.254.169.254/latest/meta-data/public-ipv4/') DO SET MY_IP=%%Y

SET AZ=%AZ:~0,-1%

for /F %%Z in ('aws ec2 describe-tags --region %AZ% --filters "Name=resource-id,Values=%INSTANCE_ID%"  --query Tags[?Key"=="`AUTO_DNS_ZONE`].Value --output text') DO SET ZONE_TAG=%%Z

for /F %%S in ('aws ec2 describe-tags --region %AZ% --filters "Name=resource-id,Values=%INSTANCE_ID%"  --query Tags[?Key"=="`AUTO_DNS_NAME`].Value --output text') DO SET NAME_TAG=%%S

echo {"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"%NAME_TAG%","Type":"A","TTL":300,"ResourceRecords":[{"Value":"%MY_IP%"}]}}]} > C:\Users\Administrator\Desktop\r53.json

aws route53 change-resource-record-sets --hosted-zone-id %ZONE_TAG% --change-batch file://C:\Users\Administrator\Desktop\r53.json
Enter fullscreen mode Exit fullscreen mode
Collapse
 
turchinc profile image
Chris Turchin

For posterity, another version of the script that works with Amazon Linux 2023 using ec2-metadata instead of the curl commands.

#!/bin/bash
# Extract information about the Instance
INSTANCE_ID=$(ec2-metadata --instance-id | cut -d " " -f 2)
AZ=$(ec2-metadata --availability-zone| cut -d " " -f 2)
MY_IP=$(ec2-metadata --public-ipv4| cut -d " " -f 2)

# Extract tags associated with instance
# NAME_TAG=$(sed -n 's/.*AUTO_DNS_NAME:\(.*\)/\1/p' <<< $(ec2-metadata --tags))
# ZONE_TAG=$(sed -n 's/.*AUTO_DNS_ZONE:\(.*\)/\1/p' <<< $(ec2-metadata --tags))

ZONE_TAG=$(aws ec2 describe-tags --region ${AZ::-1} --filters "Name=resource-id,Values=${INSTANCE_ID}" --query 'Tags[?Key==`AUTO_DNS_ZONE`].Value' --output text)
NAME_TAG=$(aws ec2 describe-tags --region ${AZ::-1} --filters "Name=resource-id,Values=${INSTANCE_ID}" --query 'Tags[?Key==`AUTO_DNS_NAME`].Value' --output text)

# Update Route 53 Record Set based on the Name tag to the current Public IP address of the Instance
aws route53 change-resource-record-sets --hosted-zone-id $ZONE_TAG --change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"'$NAME_TAG'","Type":"A","TTL":300,"ResourceRecords":[{"Value":"'$MY_IP'"}]}}]}' 
Enter fullscreen mode Exit fullscreen mode

You can even use ec2-metadata to extract the tags, but it is ugly (see above).

Then I installed cronie (crontab) and just ran the script on reboot with cron:

@reboot /home/ec2-user/bin/update-dns-on-boot.sh

Collapse
 
jrotenstein profile image
jrotenstein

Update: The method for running scripts on "every boot" appears to have changed with Amazon Linux 2. You might need to find the newer method for starting the script on every boot. (See other comments too.)

Collapse
 
sapebcfrance profile image
sapebcfrance

Hi,
I followed your instructions (I guess...) and I am getting the following error :
An error occurred (AccessDenied) when calling the ChangeResourceRecordSets operation: User: arn:aws:sts::6XXXXXXXX118:assumed-role/UpdateRoute53/i-XXXXXXXXX is not authorized to perform: route53:ChangeResourceRecordSets on resource: arn:aws:route53:::hostedzone/XXXXXXXX2I
I add the policy to the EC2 instance and the user (mine respectively) but I still get this error. Do you have an idea of what I could have done wrong ?

thanks in advance,

Olivier

Collapse
 
aws_john profile image
John Rotenstein

That's odd!

The IAM policy is specifically granting permission to call ChangeResourceRecordSets on the Hosted Zone. All I can suggest is that you check that you have put the correct HOSTED_ZONE_ID in the IAM Policy. It should start with a 'Z'.

Collapse
 
sapebcfrance profile image
sapebcfrance

Thanks John, as usual the problem was between the chair and the keyboard. This was exactly my mistake.

Collapse
 
aycarambo profile image
Lucas Stanislas • Edited

For anyone trying to use this tutorial after 2023.

The bash script will fail because the api for retrieving meta-data has changed. You need to first fetch a token and pass it as a header to subsequent api calls.

Here's the working script :

#!/bin/bash
# Get api token
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`

# Extract information about the Instance
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id/)
AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone/)
MY_IP=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4/)

# Extract tags associated with instance
ZONE_TAG=$(aws ec2 describe-tags --region ${AZ::-1} --filters "Name=resource-id,Values=${INSTANCE_ID}" --query 'Tags[?Key==`AUTO_DNS_ZONE`].Value' --output text)
NAME_TAG=$(aws ec2 describe-tags --region ${AZ::-1} --filters "Name=resource-id,Values=${INSTANCE_ID}" --query 'Tags[?Key==`AUTO_DNS_NAME`].Value' --output text)

# Update Route 53 Record Set based on the Name tag to the current Public IP address of the Instance
aws route53 change-resource-record-sets --hosted-zone-id $ZONE_TAG --change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"'$NAME_TAG'","Type":"A","TTL":300,"ResourceRecords":[{"Value":"'$MY_IP'"}]}}]}'
Enter fullscreen mode Exit fullscreen mode
Collapse
 
maxbarbul profile image
Maksym Barbul

Great article! I love the way you used tags similar to ENV variables.
Thank you for taking time to share!

Collapse
 
dawn98f profile image
dawn98f

Great Article,

I just tried this and it work. The explanation is quite simple to understand but I am having a bit difficulty understanding the script.

Can someone help?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.