16

I have couple of instances on GCE that I don't really need static addresses for, but I still need to make them accessible via dns name. Since ephemeral external ip addresses change every time an instance is restarted, I thought that I could use some sort of startup script to update dns entry for that instance in Google Cloud DNS (a bit like dyndns).

Did I miss something and there is an easier way to map ephemeral external ip addresses to a dns record via gcloud?

If not, any pointers on how to write such script would be highly appreciated!

Nakilon
  • 34,866
  • 14
  • 107
  • 142
s3ncha
  • 417
  • 1
  • 5
  • 11

4 Answers4

7

The following assumes that you are using Google Cloud DNS for foo.bar.com (ie. dns name "foo.bar.com.") with zone name "foo-bar-com" in the same project as your VM and that your VM has configuration option "This instance has full API access to all Google Cloud services." selected. Your VM will be called "my-vm.foo.bar.com" in DNS.

I'm sure this could be appropriately modified to work with DNS in a different project and/or more limited permissions.

Probably worth noting: this assumes you are using 'Google Cloud DNS' and not (just) 'Google Domains' registrar, if you're using the latter (to host your DNS, and not just as a registrar) then they have direct support for synthetic dynamic ip address with some dyndns like update mechanism (but they're more limited in a bunch of other ways).

Also note that for transaction to succeed there already has to be a record with the right IP and the right TTL (ie. the first time you run this you may want to delete any entry by hand via the UI, and run this code with dns_del commented out).

#!/bin/bash

ttlify() {
  local i
  for i in "$@"; do
    [[ "${i}" =~ ^([0-9]+)([a-z]*)$ ]] || continue
    local num="${BASH_REMATCH[1]}"
    local unit="${BASH_REMATCH[2]}"
    case "${unit}" in
                     weeks|week|wee|we|w) unit=''; num=$[num*60*60*24*7];;
                           days|day|da|d) unit=''; num=$[num*60*60*24];;
                     hours|hour|hou|ho|h) unit=''; num=$[num*60*60];;
      minutes|minute|minut|minu|min|mi|m) unit=''; num=$[num*60];;
      seconds|second|secon|seco|sec|se|s) unit=''; num=$[num];;
    esac
    echo "${num}${unit}"
  done
}

dns_start() {
  gcloud dns record-sets transaction start    -z "${ZONENAME}"
}

dns_info() {
  gcloud dns record-sets transaction describe -z "${ZONENAME}"
}

dns_abort() {
  gcloud dns record-sets transaction abort    -z "${ZONENAME}"
}

dns_commit() {
  gcloud dns record-sets transaction execute  -z "${ZONENAME}"
}

dns_add() {
  if [[ -n "$1" && "$1" != '@' ]]; then
    local -r name="$1.${ZONE}."
  else
    local -r name="${ZONE}."
  fi
  local -r ttl="$(ttlify "$2")"
  local -r type="$3"
  shift 3
  gcloud dns record-sets transaction add      -z "${ZONENAME}" --name "${name}" --ttl "${ttl}" --type "${type}" "$@"
}

dns_del() {
  if [[ -n "$1" && "$1" != '@' ]]; then
    local -r name="$1.${ZONE}."
  else
    local -r name="${ZONE}."
  fi
  local -r ttl="$(ttlify "$2")"
  local -r type="$3"
  shift 3
  gcloud dns record-sets transaction remove   -z "${ZONENAME}" --name "${name}" --ttl "${ttl}" --type "${type}" "$@"
}

lookup_dns_ip() {
  host "$1" | sed -rn 's@^.* has address @@p'
}

my_ip() {
  ip -4 addr show dev eth0 | sed -rn 's@^    inet ([0-9.]+).*@\1@p'
}

doit() {
  ZONE=foo.bar.com
  ZONENAME=foo-bar-com
  dns_start
  dns_del my-vm 5min A `lookup_dns_ip "my-vm.${ZONE}."`
  dns_add my-vm 5min A `my_ip`
  dns_commit
}
MaZe
  • 356
  • 2
  • 4
7

I'm going to take a slight spin on the answer from @MaZe. Also, I'll show using systemd so this script starts automatically at startup if you're on Ubuntu or another distro that uses systemd.

#!/bin/bash

EXISTING=`gcloud dns record-sets list --zone="{your domain}" | grep xxx.yyyy.com | awk '{print $4}'`
NEW=`gcloud compute instances describe {your instance} --zone={your zone} | grep natIP | awk -F': ' '{print $2}'`
gcloud dns record-sets transaction start -z={your domain}
gcloud dns record-sets transaction remove -z={your domain} \
    --name="xxx.yyyy.com." \
    --type=A \
    --ttl=300 "$EXISTING"
gcloud dns record-sets transaction add -z={your domain} \
   --name="xxx.yyyy.com." \
   --type=A \
   --ttl=300 "$NEW"
gcloud dns record-sets transaction execute -z={your domain}

Save it to /path/to/script.sh and start it up in systemd:

[Unit]
Description=Set xxx.yyyy.com to the correct external ip address of this instance
After=network.target auditd.service

[Service]
ExecStart=/path/to/script.sh
Type=oneshot

[Install]
WantedBy=multi-user.target

Save it in /etc/systemd/system as filename.service and enabled it with:

sudo systemctl enable filename.service
Software Prophets
  • 2,838
  • 3
  • 21
  • 21
  • What is the advantage of doing this compared to making it a cron job? – paradroid Feb 10 '19 at 18:25
  • 2
    None, I suppose, if you can run the job once at startup. Typically, if there's something you want to run at startup you use a systemd script like this one and if there's something you want to schedule, like a recurring job, you use cron. I've found that I only need this to run once and I'm set until the next startup. This systemd script has been serving me well. – Software Prophets Feb 11 '19 at 00:35
  • Ah okay, makes sense. I was actually intending to use this on a non-google hardware server where the dynamic IP address changes relatively often, but to update DNS records on Google Cloud DNS. – paradroid Feb 11 '19 at 03:19
  • @paradroid I believe the ephemeral IP is only changed at startup, so you don't have to run it repeatedly with cron. – zypA13510 Aug 18 '19 at 11:53
  • In case the instance to update is always the current instance, `{your instance}` and `{your zone}` can be replaced by `\`curl http://metadata.google.internal/computeMetadata/v1/instance/name -H Metadata-Flavor:Google\`` and `\`curl http://metadata.google.internal/computeMetadata/v1/instance/zone -H Metadata-Flavor:Google\`` respectively. – zypA13510 Aug 18 '19 at 12:19
  • I've found out that ephemeral IP address may also change upon resuming from suspended state. Is there any event for resume inside the VM to run the IP update script upon? cc @user5532169 – noseratio Sep 27 '20 at 12:20
  • 1
    @noseratio I haven't tried it yet but, an internet search reveals that you can place a script in /lib/systemd/system-sleep/ and make it executable and then any script that is in that directory will be executed during a suspend and resume. For the resume you will want to have a check like this: if [ "${1}" = "post" ]; then. Search for systemd/system-sleep for more information. – Software Prophets Sep 28 '20 at 00:42
  • @SoftwareProphets this sounds great, I'll try it, tks! – noseratio Sep 28 '20 at 07:14
3

It's been a while since you've posted this question, but i'll post my answer here for future reference.

I've had a similar need and I didn't want to use gcloud CLI.

I've created a simple python script that does pretty much the same as the bash script above does, but uses Apache Libcloud and Google Cloud API credentials (service account and key).

You can find the code in GitHub.

Or Polaczek
  • 126
  • 4
0

I modified @Software Prophets answer slightly, since his script started failing once I had added more DNS records for my domains. I'm using the root domain for the A record, and the line gcloud dns record-sets list --zone="{your domain} suddenly started returning a whole bunch of fields instead of the IP from the A record.

Since we only want the IP, we can dispense with the CNAME, MX, TXT and whatever else was being returned in column 4.

Here is my modified script to auto-update the external IP when/if it changes. I also added an if block so that it does nothing if the IP hasn't changed.

Bash script to update IP:

#!/usr/bin/bash
#
# This script updates the IP for the domain A record if it has changed.
# This allows us to use an ephemeral (dynamic) IP without having to pay
# for a static IP address.
# Update these with your DNS, Zone and Instance (DO NOT INCLUDE THE 
# TRAILING DOT for the URL, we add this later!)
ZONENAME="example-zone-name"
INSTANCE="example-instance-VM-name"
URL="example.com"

# get EXISTING ip from Google Cloud DNS example.com from ZONENAME zone
EXISTING=`gcloud dns record-sets list --name="$URL." --type="A" --zone="$ZONENAME" | grep $URL | awk '{print $4}'`
# get NEW external IP from our INSTANCE record
NEW=`gcloud compute instances describe "$INSTANCE" --zone="europe-west1-b" | grep natIP | awk -F': ' '{print $2}'`

if [ "$EXISTING" = "$NEW" ]; then
    # Do nothing, IP unchanged
    echo "IP unchanged, do nothing";
else
    # Update the record for this domain
    gcloud dns record-sets transaction start -z="$ZONENAME"
    gcloud dns record-sets transaction remove -z="$ZONENAME" \
        --name="$URL." \
        --type="A" \
        --ttl=300 "$EXISTING"
    gcloud dns record-sets transaction add -z="$ZONENAME" \
        --name="$URL." \
        --type="A" \
        --ttl=300 "$NEW"
    gcloud dns record-sets transaction execute -z="$ZONENAME"
fi

And the Systemd service hasn't changed, but here it is for completeness:

[Unit]
Description=Set root A records pointing to this VM to the correct external IP address for this instance
After=network.target auditd.service

[Service]
ExecStart=/path/to/your/bin/ephemeral.ip.sh
Type=oneshot

[Install]
WantedBy=multi-user.target
Noscere
  • 417
  • 3
  • 8