Skip to main content

Hacky Nonsense: Bookstack Load Balancer

When I set up my home Bookstack instance (wiki server), I realized it didn't have a good way to handle failover between two Active Directory (AD) domain controllers (DCs). I initially put the domain name in the LDAP server config hoping that DNS would fail over to the online DC, but that was a bust because Windows' DNS server still resolves an offline DC a lot of the time.

I spent half a day trying to get HA Proxy to work, but it ultimately just wasn't smart enough to detect a DC that was online but which hadn't fully loaded AD DS (server is up, but it can't handle authentication yet).

I started down the path of writing a hacky little load balancer but abandoned the idea temporarily to focus on a couple work projects, my family, and getting back into school.

Anyway, now that I've sufficiently driven away any prospective readers, here's one of my early attempts at a load balancing shell script. Maybe somebody will find it marginally helpful or at least a little educational.

Here it is in all its half-baked glory:

#! /bin/bash

while true
do
	active_dc=$(adcli info newbadmin.com | grep -m 1 -i domain-controller | cut -d " " -f 3 | tr [:upper:] [:lower:])
	bookstack_dc=$(grep LDAP_SERVER /var/www/bookstack/.env | cut -d ":" -f 2 | tr -d '/')
	if [[ $active_dc != $bookstack_dc ]]
	then
		# Get relevant line in BookStack config to edit with sed
		rewrite_line=$(sudo grep -n LDAP_SERVER /var/www/bookstack/.env | cut -d ":" -f 1)
		sed -i "${rewrite_line}s/.*/LDAP_SERVER=ldaps:\/\/${active_dc}/g" /var/www/bookstack/.env &&
		date_and_time=$(date +"%D %r")
		echo "${date_and_time}: Now using $active_dc for BookStack authentication"
	fi
	sleep 1
done

This script requires that the Linux server be joined to AD. Once you've done that, you can use adcli to figure out which DC is currently in use. The active DC is then inserted into the appropriate config file. The script is basically forcing Bookstack to defer to the OS for where to send authentication requests. There's also a neat grep flag in there which allows you to identify the line number where a pattern is matched. It might not seem that useful on the surface, but you can pass the line number to sed in order to target a specific line.

Early attempt number two (see below) is little more than a ping test. It pings the DC currently listed in the config file every 0.5 seconds. If the DC doesn't respond, it falls back to another one. Rinse and repeat. This one is a little longer, but it doesn't require the server to be joined to AD; it just relies on dig to identify an alternate DC.

#! /bin/bash

#### Define Variables ####
primary_server=$(grep ldaps /tmp/.env | cut -d ":" -f 2 | tr -d "/") # <-- Gets current server being used by BookStack
backup_server="" # <-- Defined later
rewrite_line="" # <-- Defined later

#### Define Functions ####
get_ldap_servers() # Identify LDAP servers for the specified domain
{
  # This is really just to identify a secondary server to use in case the primary goes down
  for server in $(dig newbadmin.com +short)
  do
    if [[ $server == $primary_server ]]
    then
      echo "$server is currently in use by BookStack"
    else
      backup_server=$server
      echo "$backup_server will be used if $primary_server goes down"
    fi
  done
}

test_primary_server() # Watch the primary LDAP server
{
  # Loop terminates if the primary server goes offline
  while ping -c 1 -W 0.5 $primary_server > /dev/null
  do
    echo "LDAP Server: $primary_server"
    sleep 0.5
  done
}

switch_primary_server() # Switches backup server to primary by editing the .env file
{
  primary_server=$backup_server
  rewrite_line=$(sudo grep -n LDAP_SERVER /tmp/.env | cut -d ":" -f 1)
  sed -i "${rewrite_line}s/.*/LDAP_SERVER=ldaps:\/\/${primary_server}/g" /tmp/.env
  echo "Switched LDAP server for BookStack to $primary_server"
}

#### Run Script ####
while true
do
  get_ldap_servers
  test_primary_server
  switch_primary_server
done

I had a handful of other ideas for how to do this, but they're not as simple. In any case, I had fun playing around with the idea. I hope you got something out of it too.

Thanks for reading!