Raspberry Pi Cluster, Part 6 - Dynamic DNS
29 Sep 2021- Raspberry Pi Cluster, Part 1 - Hardware
- Raspberry Pi Cluster, Part 2 - Assembly
- Raspberry Pi Cluster, Part 3 - Operating Systems
- Raspberry Pi Cluster, Part 4 - Jekyll and Docker
- Raspberry Pi Cluster, Part 5 - Kubernetes
- Raspberry Pi Cluster, Part 6 - Dynamic DNS
- Raspberry Pi Cluster, Part 7 - SSL
- Raspberry Pi Cluster, Part 8 - Automation
We are nearing the end of this epic journey, now we just need to point a domain to the cluster so that it accessible to the outside world and apply a SSL certificate to make sure that any connections are secure.
Simple right? Well, no it isn’t (it could have been, but where is the fun in that).
This isn’t the place to delve too deeply into the inner workings of DNS, quite frankly, neither of us has the time, but there are plenty of resources available. As the cluster is sitting inside my home network, it means that I need to point my domain name to the external IP address of my router.
Here is my first problem - my ISP is BT Infinity and as any customer of theirs will know, they do not provide static IP’s for their home customers. This is a fairly major problem when it comes to DNS, it means that my external IP will change often, maybe even a few times a day. That will result in me having to go and manually update my DNS records every time the IP changes - not ideal.
Fortunately there a pretty well established solution to the problem, it’s called Dynamic DNS. The concept is pretty simple, there is normally an agent sitting somewhere inside the network, it repeatedly checks the external IP address and automatically updates a Dynamic DNS provider.
I’ve been using No-IP for years. My router runs the agent and it works perfectly. However, one minor downside is that to use their free tier, I need to manually confirm the hostname every month, which isn’t that much of a pain, but we can do better…
Cloudflare
In my professional life, I am a web developer and I’ve used Cloudflare a few times for various clients and I’ve always been impressed by their service.
(I must point out here that I am in no way associated with them, I just like their service - this ain’t no ad!)
There are many, many parts to Cloudflare that sets it apart from other services, mainly their security and DDOS protection, but there is one feature I am particularly interested in and that is their DNS proxy.
As I’m running this on my home network, it means that I am exposing my IP to the world, while in reality isn’t too much of an issue, but I would sleep easier at night knowing that I’m not creating easy access to my home network. Quite simply, Cloudflare has an option called DNS Proxy for your DNS records where they replace my IP address with one of theirs, then forward any requests in the background. Nice.
That is enough talking, let’s set it up…
Updating the DNS records
First of all, you’ll need a Cloudflare account, head over there and create one if you haven’t already.
Second, watch this awesome tutorial by NetworkChuck over on YouTube. The general idea is that we create our own DDNS agent (like we discussed earlier) via a very handy script by someone called K0p1-Git (thanks for that!) that makes it easy to communicate with the Cloudflare API.
All we need do is update the script with our API credentials and then execute the script on a schedule via a cron job. Job done!
Except, I didn’t go through the pain of building an awesome Kubernetes cluster and not to use it, did I! Surely we can run this script via a Kubernetes deployment?
Migrating the script to Kubernetes
As I mentioned in part 5, the current stable release of Kubernetes at the time of writing this series is v1.21.4, which is handy because in v1.21 they introduced the Cronjob resource.
This should have made this migration to Kubernetes easy right? Well, not quite. It did make setting the cron task super easy, but trying to run this task on Kubernetes became one of those things where it became a matter of principle to find a way of getting it to work.
You’ll be please to hear, dear reader, that I persevered and in the end found an excellent solution to the problem. I also learnt a lot about Kubernetes in the process. I’ll guide you through the steps here to prevent you from spending hours trying to figure it out!
Step 1
Make sure you have watched the video above, completed the steps there and you have confirmed the script is working by testing on your server. Make sure you are in the same folder as your cloudflare.sh script.
Step 2
To run the cron task, we need a container to do that in with an OS, I found that the best base was Alpine Linux. There are a couple of reasons for this, the first is the size, it’s barely 4Mb in size, which is essential as we want to keep this efficient as possible. Using Ubuntu or another flavour of Linux would be way too bloated for what we want to do here. This is what Alpine was created for.
The Second reason is that we need to be able to install packages into the distro (more in this in a sec), which is something that Busybox can’t do.
The problem we have now is that Alpine doesn’t include Bash, so we need to modify our script to tell it which shell binary to use. Fortunately, that is very easy. Open up the script again with:
sudo nano cloudflare.sh
Change the very first line of the file from #!/bin/bash to #!/bin/sh, save and exit.
Step 3
The Cronjob deployment we are creating needs a way to be able to read our script. As it’s just a simple file, we can utilise a config map here. This is a really nice way to give Kubernetes access to the script:
kubectl create configmap cloudflare --from-file=cloudflare.sh
Step 4
Create a new file with sudo nano cloudflare-cronjob.yml and copy in the following:
apiVersion: batch/v1
kind: CronJob
metadata:
name: cloudflare
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: cloudflare
image: carronmedia/alpine-curl-arm64
command: ["/scripts/cloudflare.sh"]
volumeMounts:
- name: cloudflare
mountPath: /scripts
volumes:
- name: cloudflare
configMap:
name: cloudflare
defaultMode: 0744
restartPolicy: OnFailure
You can edit this to your needs, but here are a couple of notes. This will work on older versions of Kubernetes, but you’ll need to change the first line to apiVersion: batch/v1beta1.
The schedule is currently set to run the script every minute, this is standard cron notation, so you can update this as you wish.
Finally, the last point is the image. This is where a lot of my headaches came from.
At first, I set this to use the base alpine image, but the script was failing with the dreaded exec user process caused “exec format error” error. I mentioned this in part 4, which I forgot about. So after a lot of reading and messing about, I remembered that this is because arm64 CPU architecture of the Raspberry Pi isn’t compatible with the standard alpine image. I needed to use the arm64/alpine image instead.
Next it threw an error about cURL not being installed. Alpine by default doesn’t include cURL, so I created a pair of simple Docker images to run the cURL install for us. These are available on DockerHub: carronmedia/alpine-curl for the standard version and carronmedia/alpine-curl-arm64 for the arm64 version for Raspberry Pis.
Step 5
Finally, let’s deploy our Cronjob with:
kubectl create -f cloudflare-cronjob.yml
This creates all the necessary pods on Kubernetes and this will run our script to update our DNS settings on Cloudflare. We can confirm that everything is working by running:
kubectl get cronjob cloudflare
All being well, you should see your Cronjob deployment in an active state.
Well done! That was a long post, but hopefully you found it useful.
Next, Part 7 - SSL