Global Server Load Balancing via DNS

The following guide outlines the deployment methodology and installation instructions for DNS based Global Load Balancing service. The solution includes PowerDNS Authoritative application deployed on Centos 7.9 servers. The application supports static as well as dynamic DNS records. The dynamic records contain small snippets which can perform different checks based on End User IP address, Geo Location, Application health and return appropriate DNS records.

PowerDNS Auth Server Installation

Install the EPEL and the PowerDNS Auth repositories. Install the PowerDNS server with GeoIP and SQLite backends.

$ sudo yum install epel-release yum-plugin-priorities
$
$ sudo curl -o /etc/yum.repos.d/powerdns-auth-45.repo \ https://repo.powerdns.com/repo-files/centos-auth-45.repo
$
$ sudo yum update
$ sudo yum install pdns pdns-backend-geoip pdns-backend-sqlite

Create the Database directory for the PowerDNS Sqlite backend. Import the DB schema and set proper File/Directory permissions.

$ sudo mkdir /var/lib/powerdns
$
$ sudo sqlite3 /var/lib/powerdns/pdns.sqlite3 < \
/usr/share/doc/pdns-backend-sqlite-4.5.1/schema.sqlite3.sql
$
$ sudo chown -R pdns:pdns /var/lib/powerdns
$
$ sudo chown -R pdns /etc/pdns/

Download Maxmind GeoIP2 City database and move the file into the /etc/pdns directory.

Configuration of PowerDNS Application

The configurations need to be done in the configuration file /etc/pdns/pdns.conf. The Primary Authoritative server needs to be defined as Primary in the configuration file. The IPs to which zone transfer is allowed and which IP subnets to send notifications for zone updates needs to be configured. The Secondary servers need to be configured as secondary and the IP subnets from which to receive zone updates should be configured.

Primary Auth Server Configuration

#################################
# primary       Act as a primary
#
primary=yes

#################################
# slave Act as a secondary
secondary=no

#################################
# allow-axfr-ips        Allow zonetransfers only to these subnets
#
allow-axfr-ips=192.168.10.0/24,192.168.20.0/24

#################################
# daemon        Operate as a daemon
#
daemon=no

#################################
# edns-subnet-processing        If we should act on EDNS Subnet options
#
edns-subnet-processing=yes

#################################
# guardian      Run within a guardian process
#
guardian=no

#################################
# launch        Which backends to launch and order to query them in
#
launch=gsqlite3,geoip
gsqlite3-database=/var/lib/powerdns/pdns.sqlite3

#################################
# local-address Local IP addresses to which we bind
#
local-address=0.0.0.0, ::

#################################
# only-notify   Only send AXFR NOTIFY to these IP addresses or netmasks
#
only-notify=192.168.10.0/24, 192.168.20.0/24

#################################
# loglevel      Amount of logging. Higher is more. Do not set below 3
#
loglevel=6

#################################
# lua-health-checks-interval    LUA records health checks monitoring interval in seconds
#
lua-health-checks-interval=20

#################################
# prevent-self-notification   Don't send notifications to what we think is ourself
#
prevent-self-notification=yes

#################################
# allow-notify-from     Allow AXFR NOTIFY from these IP ranges. If empty, drop all incoming notifies.
#
allow-notify-from=

#################################
# receiver-threads      Default number of receiver threads to start
#
receiver-threads=2

#################################
# reuseport     Enable higher performance on compliant kernels by using SO_REUSEPORT allowing each receiver thread to open its own socket
#
reuseport=yes

#################################
# setgid        If set, change group id to this gid for more security
#
setgid=pdns

#################################
# setuid        If set, change user id to this uid for more security
#
setuid=pdns

#################################
# webserver     Start a webserver for monitoring (api=yes also enables the HTTP listener)
#
webserver=yes
webserver-address=0.0.0.0
webserver-allow-from=127.0.0.1,192.168.20.0/27
webserver-password=secure-pass
webserver-port=8081

#################################
# consistent-backends   Assume individual zones are not divided over backends. Send only ANY lookup operations to the backend to reduce the number of lookups
#
consistent-backends=yes

#################################
# enable-lua-records    Process LUA records for all zones (metadata overrides this)
#
enable-lua-records=yes

geoip-database-files=/etc/pdns/GeoLite2-City.mmdb

#################################
# cache-ttl     Seconds to store packets in the PacketCache
#
cache-ttl=0

#################################
# query-cache-ttl       Seconds to store query results in the QueryCache
#
query-cache-ttl= 0

Secondary Auth Server Configuration

#################################
# primary       Act as a primary
#
primary=no

#################################
# slave Act as a secondary
secondary=yes

#################################
# allow-axfr-ips        Allow zonetransfers only to these subnets
#
allow-axfr-ips=

#################################
# daemon        Operate as a daemon
#
daemon=no

#################################
# edns-subnet-processing        If we should act on EDNS Subnet options
#
edns-subnet-processing=yes

#################################
# guardian      Run within a guardian process
#
guardian=no

#################################
# launch        Which backends to launch and order to query them in
#
launch=gsqlite3,geoip
gsqlite3-database=/var/lib/powerdns/pdns.sqlite3

#################################
# local-address Local IP addresses to which we bind
#
local-address=0.0.0.0, ::

#################################
# only-notify   Only send AXFR NOTIFY to these IP addresses or netmasks
#
only-notify=192.168.10.0/24, 192.168.20.0/24

#################################
# loglevel      Amount of logging. Higher is more. Do not set below 3
#
loglevel=5

#################################
# lua-health-checks-interval    LUA records health checks monitoring interval in seconds
#
lua-health-checks-interval=20

#################################
# prevent-self-notification     Don't send notifications to what we think is ourself
#
prevent-self-notification=yes

#################################
# allow-notify-from     Allow AXFR NOTIFY from these IP ranges. If empty, drop all incoming notifies.
#
allow-notify-from=192.168.10.0/24,192.168.20.0/24

#################################
# receiver-threads      Default number of receiver threads to start
#
receiver-threads=2

#################################
# reuseport     Enable higher performance on compliant kernels by using SO_REUSEPORT allowing each receiver thread to open its own socket
#
reuseport=yes

#################################
# setgid        If set, change group id to this gid for more security
#
setgid=pdns

#################################
# setuid        If set, change user id to this uid for more security
#
setuid=pdns

#################################
# webserver     Start a webserver for monitoring (api=yes also enables the HTTP listener)
#
webserver=yes
webserver-address=0.0.0.0
webserver-allow-from=127.0.0.1,192.168.20.0/27
webserver-password=secure-pass
webserver-port=8081

#################################
# consistent-backends   Assume individual zones are not divided over backends. Send only ANY lookup operations to the backend to reduce the number of lookups
#
consistent-backends=yes

#################################
# enable-lua-records    Process LUA records for all zones (metadata overrides this)
#
enable-lua-records=yes

geoip-database-files=/etc/pdns/GeoLite2-City.mmdb

#################################
# cache-ttl     Seconds to store packets in the PacketCache
#
cache-ttl=0

#################################
# query-cache-ttl       Seconds to store query results in the QueryCache
#
query-cache-ttl= 0

Creation of GLB Zone

A subdomain will be used as the GLB zone. The subdomain will be delegated to the PowerDNS Auth servers in the master domain.com zone.

$ORIGIN domain.com.
.
.
.
.

glbns1.domain.com. IN A 192.168.10.101
glbns2.domain.com. IN A 192.168.20.101


$ORIGIN glb.domain.com.
$TTL 1D
@ IN NS glbns1.domain.com.
@ IN NS glbns2.domain.com.

The glb.domain.com zone will be created in the Primary/Secondary GLB servers. On the Primary server create the zone and set it as primary

$ sudo pdnsutil create-zone glb.domain.com
$ sudo pdnsutil set-kind glb.domain.com primary

Define the variable EDITOR in /etc/environment file. The “pdnsutil edit-zone” command uses this variable to open file editor for configuring the zone files.

# /etc/environment
EDITOR=/usr/bin/vi

Edit the zone glb.domain.com on the Primary and configure the SOA and NS record.

$ sudo pdnsutil edit-zone glb.domain.com

Enter the following in the opened editor and save the file.

$ORIGIN .
glb.domain.com	3600	IN	SOA	glbns1.domain.com hostmaster.glb.domain.com 2021102801 10800 3600 604800 3600
glb.domain.com	3600	IN	NS	glbns1.domain.com.
glb.domain.com	3600	IN	NS	glbns2.domain.com.

On the Secondary server create the slave zone and define the IP of primary to replicate the zone.

$ sudo pdnsutil create-slave-zone glb.domain.com 192.168.10.101

Verify from log files that the transfer of zone was done from Primary to Secondary.

$ sudo journalctl -u pdns -f
.
.
19:33:54 glbns1.domain.com pdns_server[15611]: 1 domain for which we are master needs notifications
19:33:54 glbns1.domain.com pdns_server[15611]: Skipped notification of domain 'glb.domain.com' to glbns1.domain.com. because it does not match only-notify.
19:33:54 glbns1.domain.com pdns_server[15611]: Queued notification of domain 'glb.domain.com' to 192.168.20.101:53
19:33:54 glbns1.domain.com pdns_server[15611]: AXFR-out zone 'glb.domain.com', client '192.168.20.101:38721', transfer initiated
ا19:33:54 glbns1.domain.com pdns_server[15611]: AXFR-out zone 'glb.domain.com', client '192.168.20.101:38721', allowed: client IP is in allow-axfr-ips
19:33:54 glbns1.domain.com pdns_server[15611]: gsqlite3: connection to '/var/lib/powerdns/pdns.sqlite3' successful
19:33:54 glbns1.domain.com pdns_server[15611]: AXFR-out zone 'glb.domain.com', client '192.168.20.101:38721', AXFR finished
19:33:55 glbns1.domain.com pdns_server[15611]: Removed from notification list: 'glb.domain.com' to 192.168.20.101:53 (was acknowledged)
19:33:57 glbns1.domain.com pdns_server[15611]: No master domains need notifications

Verify on the Secondary that the zone transfer was done from Primary.

$ sudo journalctl -u pdns -f
.
.
19:33:54 glbns2.domain.com pdns_server[887]: Received NOTIFY for glb.domain.com from 192.168.10.101:12791 - queueing check
19:33:54 glbns2.domain.com pdns_server[887]: Domain 'glb.domain.com' is stale, master 192.168.10.101 serial 2021102803, our serial 2021102802
19:33:54 glbns2.domain.com pdns_server[887]: XFR-in zone: 'glb.domain.com', primary: '192.168.10.101', initiating transfer
19:33:54 glbns2.domain.com pdns_server[887]: XFR-in zone: 'glb.domain.com', primary: '192.168.10.101', starting AXFR
19:33:54 glbns2.domain.com pdns_server[887]: AXFR-in zone 'glb.domain.com', primary '192.168.10.101', retrieval started
19:33:54 glbns2.domain.com pdns_server[887]: AXFR-in zone: 'glb.domain.com', primary: '192.168.10.101', retrieval finished
19:33:54 glbns2.domain.com pdns_server[887]: AXFR-in zone: 'glb.domain.com', primary: '192.168.10.101', zone committed with serial 2021102803

Configuration of GLB Zone Records

The zone files can be edited using the “pdnsutil edit-zone” command.

Create A records

internal.glb.domain.com	1800	IN	A	192.168.10.10
external.glb.domain.com	1800	IN	A	192.168.20.20

Configure record to return the Latitude/Longitude of the client IP.

latlon.glb.domain.com	60	IN	LUA	LOC "latlonloc()"

Configure record to return the client IP.

whoami.glb.domain.com	60	IN	LUA	TXT "who:toString()"

Configure DNS views based on client IP. The following will return 192.168.1.1 for client source network 192.168.30.0/24 and 192.168.2.2 for everyone else.

view.glb.domain.com	60	IN	LUA	A "view({ " "{ {'192.168.30.0/24'}, {'192.168.1.1'}}," "{ {'0.0.0.0/0'}, {'192.168.2.2'}} " " }) "

Configure DNS CNAME records to return records based on client source network/IP.

www.glb.domain.com	60	IN	LUA	CNAME ";if(netmask({'192.168.30.0/24','172.16.0.0/16'})) then return 'internal.glb.domain.com' " "else return 'external.glb.domain.com' end"

Configure HTTP(s) health check for specific URL backend IPs and return records which return 200 OK.

web.glb.domain.com	60	IN	LUA	A "ifurlup('https://domain.com/', {'172.16.43.219', '172.16.188.234'}, {selector='all', timeout=4} )"

Configure TCP port status and return the IP which is closest to the Geo Location of the requester client.

app.glb.domain.com    IN   LUA A ( "ifportup(995, {'192.168.100.10', '192.168.200.10'}, " "{selector='pickclosest'}) ")

List the domain zone records

$ sudo pdnsutil list-zone glb.domain.com

$ORIGIN .
careers.glb.domain.com	60	IN	LUA	A "ifurlup('https://careers.domain.com/', {'124.109.43.219', '203.82.54.18'}, {selector='all', timeout=4})"
glb.domain.com	3600	IN	NS	glbns1.domain.com.
glb.domain.com	3600	IN	NS	glbns2.domain.com.
glb.domain.com	3600	IN	SOA	glbns1.domain.com hostmaster.domain.com 2021102803 10800 3600 604800 3600
latlong.glb.domain.com	60	IN	LUA	LOC "latlonloc()"
web.glb.domain.com	60	IN	LUA	A "ifurlup('https://domain.com/', {'172.16.43.219', '172.16.188.234'}, {selector='all', timeout=4} )"
whoami.glb.domain.com	60	IN	LUA	TXT "who:toString()"

Check zone for errors

$ sudo pdnsutil check-zone glb.domain.com

Checked 7 records of 'glb.domain.com', 0 errors, 0 warnings.

GLB Domain Resolution Flow

References

https://docs.varnish-software.com/tutorials/dns-based-gslb/
https://doc.powerdns.com/authoritative/lua-records/functions.html
https://blog.powerdns.com/2017/12/15/powerdns-authoritative-lua-records/

Leave a Reply