Recently, I’ve deployed this website on my RPi k3s cluster. I manage my DNS records using Cloudflare.

Even though my public IP address does not change that much, I wanted to make sure that it is periodically updated.

That’s when I searched around and stumbled accross a very simple script which suits my needs. The script makes use of the Cloudflare API which is very well written and easy to use, so it gives me hopes that it will be supported and maintained in the following years. I’m sure there are many more robust ways of achieving the same thing but this is more than enough for my setup.

The problem, though, is that the script requires secrets which I would not like to push to my infrastructure repository. Only the generated script should contain secrets but this final script version will only be accessible by an RPi which will periodically run the script.

Since I’ve already wrote a lot of my automations using Ansible, writing another playbook was a natural choice.

My Ansible playbook first generates a bash script from a Jinja template which populates data using Vault. This script is then used in a cronfile which gets executed every hour.

Here are my Ansible tasks:

- name: Create DDNS crontab directory
  ansible.builtin.file:
    path: "/var/cron"
    state: directory
    mode: '0755'
  register: app_dir
   
- name: Prepare the DDNS script
  ansible.builtin.template:
    src: "{{ role_path }}/files/cloudflare.sh.j2"
    dest: /var/cron/cloudflare.sh
    mode: '0755'
  vars: 
    cloudflare_configuration: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secrets/data/cloudflare token={{ vault_token }} url=http://vault.rpirack.home') }}"

- name: Create a cron file under /etc/cron.d
  ansible.builtin.cron:
    name: DDNS update
    special_time: "hourly"
    user: root
    job: "/bin/bash /var/cron/cloudflare.sh"

The vault_token variable is retrieved from a user prompt declared in the playbook:

- name: Setup DNS
  hosts: dns
  vars_prompt:
  - name: vault_token
    prompt: Enter the Vault token
  become: true
  roles:
    - dns
  handlers:
    - ansible.builtin.import_tasks: handlers/main.yml

As for the Jinja template cloudflare.sh.j2 itself, I’ve tried to stay to the original as close as possible, the only thing changed is the Vault secret injection at the beginning of the script:

#!/bin/bash
## change to "bin/sh" when necessary

set -eu

auth_email="{{ cloudflare_configuration['auth_email'] }}"              # The email used to login 'https://dash.cloudflare.com'
auth_method="global"                                                   # Set to "global" for Global API Key or "token" for Scoped API Token
auth_key="{{ cloudflare_configuration['auth_key'] }}"                  # Your API Token or Global API Key
zone_identifier="{{ cloudflare_configuration['zone_identifier'] }}"    # Can be found in the "Overview" tab of your domain
record_name="{{ cloudflare_configuration['record_name'] }}"            # Which record you want to be synced
ttl=$((60 * 60 * 4))                                                   # Set the DNS TTL (seconds)
proxy="true"                                                           # Set the proxy to true or false
sitename="Zezulka personal website"                                    # Title of site "Example Site"
slackchannel=""                                                        # Slack Channel #example
slackuri=""                                                            # URI for Slack WebHook "https://hooks.slack.com/services/xxxxx"
discorduri=""                                                          # URI for Discord WebHook "https://discordapp.com/api/webhooks/xxxxx"


###########################################
## Check if we have a public IP
###########################################
<hidden>

After running the playbook, a new cron job is defined for root:

rpi_admin@raspberrypi:~ $ sudo crontab -l
[sudo] password for rpi_admin: 
#Ansible: DDNS update
@hourly /bin/bash /var/cron/cloudflare.sh

Notice the special keyword @hourly which is part of the vanilla cron; this way, I do not need to use standard cron expression with numbers and stars.

After running for a few hours, I can clearly see that the cronjob works as expected without needing to do anything else:

rpi_admin@raspberrypi:~ $ cat /var/log/messages | grep DDNS

Jul  7 01:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 02:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 03:00:04 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 04:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 05:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 06:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 07:00:04 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 08:00:06 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 09:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 10:00:06 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 11:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 12:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.
Jul  7 13:00:05 DDNS Updater: IP (<hidden>) for zezulka.dev has not changed.