CI/CD all the things: Pi-hole

CI/CD all the things: Pi-hole
Photo by RAPHAEL MAKSIAN / Unsplash

I've spent some time exploring how to apply the concepts of CI/CD to everyday life. In my first post, I went all in - making breakfast with GitLab CI/CD as the orchestrator. In this post, I'll tell you about a home project I undertook over the holidays: getting a Pi-hole up and running at my house. (mmmm 🥧)

Pi-hole is a great little open source tool that lets you run your own DNS server on your network and send advertisement and tracking requests to a "black hole" of 0.0.0.0, providing network-level ad blocking…rather than installing ad blockers on every device, browser, and IoT item in the house. Pi-hole even ships with a great dashboard and has a number of extension points to customize it. I added an hourly speed test - just to keep my ISP honest 😉.

PiHole Dashboard with Speedtest plugin
PiHole Dashboard with Speedtest plugin

Installing Pi-hole

Installing Pi-hole is relatively simple, with straightforward instructions, and the only thing I had to watch out for was DHCP. Since my router provides Circle from Disney for parental controls, it has to retain the DNS/DHCP server but I could still map its upstream DNS to Pi-hole. This means I lost some of the granularity in the logs as most requests come from my router rather than the individual devices, but once I had it all set up Pi-Hole worked as designed.

Source control for Pi-hole

There are a lot of great resources out there for Pi-hole, including things like commonly whitelisted domains to make sure some general services aren't negatively impacted by it. There are also many additional blacklists available on the internet, depending on what you're concerned about. And, since FTL is extensible, it is even possible to use it as a lightweight local DNS server.

Because all of those elements are presented in a fashion that says, "Here's how you can run a command in the terminal," or "Here's how you add it in the UI," my internal "must source control all things" tick was activated…and so I created a repository on GitLab.

I added a couple of scripts to automate the whitelisting for common and custom domains as well as a file to contain the hosts on my network. But this still meant I had to manually check out the repository on my Raspberry Pi and manually run the scripts. That's where GitLab CI/CD comes to the rescue! Luckily, it's available on our free tier, because I was using my personal account for all of this, while I was on holiday.

Enabling CI/CD to Pi-hole

Now that I had a repository ready to go, I just had two steps until I had access to all the CI/CD goodness my little heart would ever desire.

  1. Make my Raspberry Pi a GitLab Runner so that GitLab.com changes could get into my home network with no holes poked in my firewall.
  2. Set up a .gitlab-ci.yml to run updates when changes are made.

Installing GitLab Runner on my Raspberry Pi

Installing GitLab Runner is easy on any platform that supports Golang, including Linux, OSX, Windows, FreeBSD, Kubernetes and (soon) even z/OS.

The Raspberry Pi has an ARM chip, so I used the ARM binary to install it with:

sudo wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm

Then I made the gitlab-runner binary executable with:

sudo chmod +x /usr/local/bin/gitlab-runner

After that, I installed it as a service with these commands:

sudo gitlab-runner install --user=pi
sudo gitlab-runner start

Once it was installed, registering the runner to be a private runner on my project was easy. I also added a tag of prod to make sure it only ran code I tagged to run on "production" aka actually on my home network. I'm hoping this protects me from someone I like to call "future Brendan" who may or may not have the best memory when it comes to breaking things.

Registering the GitLab runner
Registering the GitLab runner

I chose a shell executor so that it was simple to understand. Basically, the runner would just execute the commands I put in the script: section of my YAML. Just as if I had logged in and ran them myself.

Setting up the .gitlab-ci.yml

I had a few thoughts in mind when it came to setting up my pipeline for deployments. Some of the scripts required restarts of the DNS services on the Pi-hole, so I didn't want them to be running all of the time. To prevent that, I used a number of GitLab CI/CD features:

  • only:refs - Ensures we only run this on master - so if I'm on a branch trying something out (shakes fist at future Brendan) I don't break anything.
  • only:changes - This allows me to limit the scope of the run to only the changes needed. For instance, a change to the whitelist won't trigger an update to the local DNS list and vice versa.
  • tags - I mentioned this tag before, but this also means that any job I add must have the prod tag before it will run in production (aka the live Pi-hole).

As an example, let's look at automating the whitelist. I already created a script (whitelistme.sh) that automates it when ran. So getting that to run in GitLab CI/CD is as simple as this script:

whitelist:
  stage: deploy-prod
  script: 
    - echo "Run whitelist.sh"
    - ./whitelistme.sh
  only:
    refs:
      - master
    changes:
      - whitelistme.sh
  tags:
    - prod

Let's break down each section of that script:

  • For the stage, I created my own custom stage called deploy-prod.
  • The script is very simple and echoes what it is about to do and runs the script at the root of the checkout with ./whitelistme.sh.
  • The only section implements the two controls I was talking about earlier. refs: - master means this will only run on the master branch of the repository. changes: - whitelistme.sh means this will only run on a change to the whitelistme.sh script and not on every change to the repository. This ensures that script only runs when it needs to.
  • "Tags" ensures that it runs on a tagged runner - in this case, the Pi-hole at home.

Success! Running my jobs on my Pi-hole in my home network - orchestrated all from GitLab. 😺

DNS as Code

Since FTL is mostly just dnsmasq with some customizations for Pi-hole, it is relatively easy to customize. In fact, by default, it includes an additional local file (and hostnames for the Pi-hole itself) in /etc/pihole/local.list like this:

10.0.0.xx pihole
10.0.0.xx pi.hole

Again, I wanted to make sure this was source controlled - and the dream of source controlled DNS is now a reality for me. The way I implemented it was to:

Create a localDNS file that would contain all of the local DNS entries I wanted:

10.0.0.xx pihole
10.0.0.xx pi.hole
10.0.0.1 orbi.myhouse
10.0.0.xx pirack0.myhouse

Then I used GitLab CI/CD to automate replacing the /etc/pihole/local.list file with this one anytime it changed:

local-DNS:
  stage: deploy-prod
  script: 
    - echo "Copy localDNS to /etc/pihole/local.list"
    - sudo cp ./localDNS /etc/pihole/local.list
    - echo "Restart Pi-hole DNS"
    - pihole restartdns
  only:
    refs:
      - master
    changes:
      - localDNS
  tags:
    - prod

And Voilà! Source controlled and automated DNS-as-code (DaaS (tm) (r) (c))

Frequently Asked Questions

Anticipating the questions you'll have, I've prepared a short primer below:

  • Yes, I did bring down the internet in the whole house for about 20 minutes while I was tinkering away. Yes, this was while all my in-laws were here on their phones. Yes, I got a lot of grief for it
  • Yes, this will restart DNS as it is running, thus I wanted to only run the jobs as restricted above
  • What is this business about pirack0 in the DNS entry?! That will just have to wait for another post 😉