Dynamic DNS
I’ve been self-hosting some services on my homelab. However, since I don’t have a static IP through my Internet service provider, I need some sort of dynamic DNS script that will update the requisite A record on my name server.
Here’s how I do it.
Server Configuration
Provision a new virtual machine with OpenBSD 7.0 installed, setup NTP, all of that stuff.
Ruby is a great scripting language, with a clean, easy to understand syntax. Therefore, I need to install Ruby onto the machine, which I’ve gone over before.
I also installed chruby
, a pair of shell scripts that set environment variables like PATH
and RUBIES
so that the system knows where to find the installed Ruby. However, it has a dependency on bash
, so be aware of that.
I’d like to port chruby
to ksh
, or KornShell, but I haven’t had the time yet. Maybe one of these days!
Script
I use AWS Route 53 at the moment, mostly because it has an API I’m familiar with, but also because I haven’t found anything better that suits my needs.
The below script queries an IP lookup URL and compares that value with the A record in Route 53. If they match, then the script exits early. Otherwise, it sends an upsert changeset to AWS.
require 'net/http'
require 'oga'
require 'aws-sdk-route53'
hosted_zone_id = ENV["ZONE_ID"]
dns_name = ENV["DNS_NAME"]
ip_lookup_uri = ENV["IP_LOOKUP_URI"]
uri = URI(ip_lookup_uri)
current_ip = Net::HTTP.get(uri)
client = Aws::Route53::Client.new
response = client.list_resource_record_sets({
:hosted_zone_id => hosted_zone_id,
:start_record_name => dns_name,
:start_record_type => "A",
:max_items => 1
})
record_set = response.resource_record_sets.first
resource_record = record_set.resource_records.first
previous_ip = resource_record.value
if current_ip == previous_ip
exit 0
end
client.change_resource_record_sets({
:change_batch => {
:changes => [
{
:action => "UPSERT",
:resource_record_set => {
:name => dns_name,
:resource_records => [
{
:value => current_ip
},
],
:ttl => 900,
:type => "A"
},
},
],
},
:hosted_zone_id => hosted_zone_id
})
exit 0
The only third party dependencies I’m using are to the AWS Ruby Route 53 SDK and oga
, which is itself a dependency of the SDK. Other than that, I’m using net/http
which is part of the Ruby Standard Library, rather than use one of the many HTTP gems out there.
You should always keep dependencies down to the bare minimum. I could have used one of the many, many HTTP Ruby libraries, but I’m often more focused on future stability than using the hottest new thing. I can rely on the net/http
library to be stable far more than I can one of HTTP Ruby libraries, if I’m honest.
Because I’m using environment variables, I need to export those in a separate shell script:
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=...
export ZONE_ID=...
export DNS_NAME=example.com
export IP_LOOKUP_URI=https://api.example.org
Use whatever IP lookup service makes sense for you, though I use my own self-hosted one.
I wrote a script to source things like chruby
and the requisite environment variables and then run the Ruby script itself.
#!/usr/bin/env bash
set -e
HOME=/home/joseph
source $HOME/.bashrc
source $HOME/dynamic-dns/env.sh
ruby $HOME/dynamic-dns/dynamic_dns.rb
Finally, I setup a cronjob to execute the script every fifteen minutes.
*/15 * * * * /home/joseph/bin/dynamic-dns.sh
I can also call the script manually if I want.
Conclusion
This setup is a bit more finicky than I would like. I’d like to move away from bash
at some point, for example, and adding one more dependency to it means one more thing to change when I eventually do.
Still, it works, and while that’s not the greatest factor in determining whether to deploy something, it is a factor.