Automate Dynamic DNS Updates with Gandi API and Docker

Published Sunday, Mar 26, 2023 by Tony Davis

Automate Dynamic DNS Updates with Gandi API and Docker

Managing a web domain can be a hassle, especially if you have a dynamic IP address. A dynamic IP address can change often, which makes it difficult to keep your DNS A record up-to-date. Fortunately, Gandi API provides a simple solution for updating DNS records programmatically.

In this tutorial, we’ll show you how to use the Gandi API, Docker, and shell scripting to automate the process of updating your DNS A record to reflect your current external IP address.

Follow-up to Dynamic DNS Using Gandi This tutorial is a follow-up to the Dynamic DNS Using Gandi tutorial, which explains how to update DNS records using the Gandi API. The follow-up tutorial builds on the previous tutorial by demonstrating how to create a Docker container that runs the script as a service. By using Docker, you can package the script and its dependencies into a single container, making it easy to deploy and run on any platform. This approach ensures that the script is always running and updating your DNS records, even in the event of container restarts or system failures. In summary, this tutorial builds on the previous tutorial by demonstrating how to create a Docker container that runs the update_dns.sh script as a service, ensuring that your DNS records are always up-to-date.

Prerequisites

Before we start, you will need the following:

  • A Gandi account with an API key
  • A domain name and a subdomain that you want to update
  • Docker installed on your computer

Setting up the environment variables

First, create a .env file with the following environment variables:

GANDI_API_KEY=<api_key>
DOMAIN=example.com
SUBDOMAIN=subdomain
TTL=300
IPLOOKUP=http://whatismyip.akamai.com/

Replace api_key with your Gandi API key, example.com with your domain name, subdomain with your subdomain, and 300 with your desired TTL value. The IPLOOKUP variable is the URL to check your public IP address. The default value is http://whatismyip.akamai.com/

Creating the scripts

Now, let’s create the scripts that will update the DNS records automatically.

Create a start.sh file with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh

# Set the log file path
LOG_FILE="/var/log/update_dns.log"

# Log when the container starts
echo "$(date): Starting container" >> "$LOG_FILE"

# Run the update_dns script once
/bin/sh /usr/local/bin/update_dns.sh

# Start the cron daemon
crond -L /var/log/cron.log

# Tail the logs to keep the container running
tail -f /var/log/update_dns.log /var/log/cron.log &

# Log when the container stops
trap "echo $(date): Stopping container >> $LOG_FILE" EXIT

# Wait for the container to stop
wait

This start.sh script sets up the log file path, logs when the container starts, runs the update_dns.sh script once, starts the crond daemon, tails the logs to keep the container running, logs when the container stops, and waits for the container to stop.

Finally, create an update_dns.sh file with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/bin/bash

# Set your Gandi API key, domain name, and subdomain
GANDI_API_KEY="$GANDI_API_KEY"
DOMAIN="$DOMAIN"
SUBDOMAIN="$SUBDOMAIN"
# Set the TTL value for the DNS A record in seconds (default is 1800 seconds / 30 minutes)
TTL="$TTL"
IPLOOKUP="$IPLOOKUP"

# Set the log file path
LOG_FILE="/var/log/update_dns.log"

# Get the current external IP address
CURRENT_IP=$(curl -s $IPLOOKUP)

# Get the IP address and TTL of the DNS A record via the Gandi API
DNS_INFO=$(curl -s -H "Authorization: Apikey $GANDI_API_KEY" \
     "https://dns.api.gandi.net/api/v5/domains/$DOMAIN/records/$SUBDOMAIN/A")

# Check if the DNS record exists
if [ -z "$DNS_INFO" ]; then
    # Log an error if the DNS record doesn't exist
    echo "$(date): Error: DNS record doesn't exist" >> "$LOG_FILE"
    exit 1
fi

# Extract the DNS IP address and TTL value from the API response
DNS_IP=$(echo "$DNS_INFO" | jq -r '.rrset_values[0]')
DNS_TTL=$(echo "$DNS_INFO" | jq -r '.rrset_ttl')

# Check if the DNS IP is empty
if [ -z "$DNS_IP" ]; then
    # Log an error if the DNS IP is empty
    echo "$(date): Error: DNS IP is empty" >> "$LOG_FILE"
    exit 1
fi

# Compare the IP addresses
if [ "$CURRENT_IP" != "$DNS_IP" ]; then
    # Log when there is an IP change
    echo "$(date): IP address changed from $DNS_IP to $CURRENT_IP" >> "$LOG_FILE"

    # Update the DNS A record via the Gandi API
    RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
         -X PUT -H "Content-Type: application/json" -H "Authorization: Apikey $GANDI_API_KEY" \
         -d '{"rrset_values": ["'$CURRENT_IP'"], "rrset_ttl": '$TTL'}' \
         "https://dns.api.gandi.net/api/v5/domains/$DOMAIN/records/$SUBDOMAIN/A")

    if [ "$RESPONSE" == "200" ] || [ "$RESPONSE" == "201" ]; then
        # Log when the DNS record is updated
        echo "$(date): DNS A record updated to $CURRENT_IP with TTL $TTL seconds" >> "$LOG_FILE"
    else
        # Log an error if the API request fails
        echo "$(date): API request failed with status code $RESPONSE" >> "$LOG_FILE"
    fi
else
    # Log when the script is run without any IP change
    echo "$(date): IP address unchanged at $CURRENT_IP with TTL $DNS_TTL seconds" >> "$LOG_FILE"
fi

This update_dns.sh script sets up the required variables, gets the current external IP address, gets the IP address and TTL of the DNS A record via the Gandi API, checks if the DNS record exists and the DNS IP, compares the IP addresses and updates the DNS A record via the Gandi API if there is an IP change.

Building the Docker container

Now, let’s create a Docker container to run our script. Create a Dockerfile with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM alpine:3.15

RUN apk add --no-cache curl jq

COPY update_dns.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/update_dns.sh

COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/start.sh

ENTRYPOINT ["/usr/local/bin/start.sh"]
CMD ["crond", "-f"]

This Dockerfile uses the alpine:3.15 image, installs curl and jq, copies the update_dns.sh and start.sh scripts to the container, and sets start.sh as the entry point.

Next, create a docker-compose.yml file with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
version: "3.9"
services:
  update-dns:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./crontab.txt:/etc/crontabs/root
      - ./logs:/var/log
      - "/etc/timezone:/etc/timezone:ro"
      - "/etc/localtime:/etc/localtime:ro"
    env_file:
      - .env
    command: ["crond", "-f"]

This docker-compose.yml file defines a service named update-dns that builds the Docker image using the Dockerfile and sets up the required volumes, environment variables, and command to run.

Using crontab.txt to schedule tasks

In addition to the scripts and Dockerfile, the docker-compose.yml file in the repository references a file named crontab.txt as a volume. This file is used to schedule tasks using the cron utility.

The crontab.txt file in the repository contains the following line:

*/30 * * * * /bin/sh /usr/local/bin/update_dns.sh

This line specifies that the update_dns.sh script should be run every 30 minutes.

When the Docker container is started, the crontab.txt file is mounted as a volume in the container’s /etc/crontabs/root directory. The cron daemon reads this file and runs the scheduled tasks at the specified intervals.

In summary, the crontab.txt file is used to schedule the execution of the update_dns.sh script every 30 minutes, ensuring that the DNS records are updated regularly.

Running the Docker container

To run the Docker container, use the following command:

docker-compose up -d

This command builds the Docker image, creates a container, and starts the container in detached mode. The -d flag indicates that the container should run in the background.

You can build the container seperatly if you want to by running

docker build -t gandi-dyndns .

You can check the logs in the /logs/ directory. There are two logs that will be output. They are cron.log and update_dns.log.

update_dns.log contains all the log output fromt he script and will look something like this:

Wed Mar 22 16:26:10 UTC 2023: Starting container
Wed Mar 22 16:27:01 UTC 2023: IP address changed from <old_ip> to  <new_ip>
Wed Mar 22 16:27:01 UTC 2023: DNS A record updated to <new_ip> with TTL 300 seconds
Wed Mar 22 16:28:01 UTC 2023: IP address unchanged at <old_ip> with TTL 300 seconds
Wed Mar 22 16:29:00 UTC 2023: IP address unchanged at <old_ip> with TTL 300 seconds

Source code

You can find the complete source code for this tutorial on the GitHub repository virtuallytd/gandi-dyndns. The repository contains the Dockerfile, docker-compose.yml, update_dns.sh, start.sh, .env and crontab.txt files used in this tutorial.

Feel free to fork the repository and modify the code to suit your needs.

Conclusion

In this tutorial, we have learned how to update DNS records automatically using Docker and the Gandi API. We have created a Docker container with the required scripts and environment variables, built the Docker image, and run the container in detached mode. We have also checked the logs to make sure that the scripts are running correctly.

With this setup, you can rest assured that your DNS records will be updated automatically, keeping your website online 24/7.

Original Article Dynamic DNS Using Gandi