Building HA Load Balancer with HAProxy and keepalived

For this tutorial I'll demonstrate how to build a simple yet scalable highly available HTTP load balancer using HAProxy [1] and keepalived [2], then later I'll show how to front-end HAProxy with Pound [5] and implement SSL termination and redirect the insecure connections from port 80 to 443.

For a tutorial on how to do this for HAProxy 1.5 please read the newer post here.

Let's assume we have two servers LB1 and LB2 that will host HAProxy and will be made highly available through the use of the VRRP protocol [3] as implemented by keepalived. LB1 will have an IP address of 192.168.29.129 and LB2 will have an IP address of 192.168.29.130. The HAProxy will listen on the "shared/floating" IP address of 192.168.29.100, which will be raised on the active LB1. If LB1 fails that IP will be moved and raised on LB2 with the help of keepalived.
We are also going to have two back-end nodes that run apache -  WEB1 192.168.29.131 and WEB2 192.168.29.132 - that will be receiving traffic from the HAProxy using round-robing load-balancing algorithm.

First let's install keepalived on both LB1 and LB2. We can either get it from the EPEL repo, or install it from source.


Edit the configuration file on both servers to match except the priority parameter:


Save the config on both servers and start keepalived:


Now that keepalived is running check that LB1 has raised 192.168.29.100:


You can test if the IP will move from LB1 to LB2 by failing LB1 (shutdown or bring the network down) and running the above command on LB2.

Now that we have high availability of the IP resource we can install HAProxy on LB1 and LB2:


Edit the configuration file, and start HAProxy:


This is a very simplistic configuration that uses HTTP load-balancing with cookie prefixing. This is how it works:

- LB1 is VRRP master (keepalived), LB2 is backup. Both monitor the haproxy process, and lower their prio if it fails, leading to a failover to theother node.
- LB1 will receive clients requests on IP 192.168.29.100.
- both load-balancers send their checks from their native IP.
- if a request does not contain a cookie, it will be forwarded to a validserver
- in return, if a JESSIONID cookie is seen, the server name will be prefixedinto it, followed by a delimitor ('~')
- when the client comes again with the cookie "JSESSIONID=A~xxx", LB1 will know that it must be forwarded to server A. The server name will then be extracted from the cookie before it is sent to the server.
- if server "webA" dies, the requests will be sent to another valid serverand a cookie will be reassigned.

For more information and examples see [4].

Let's start HA proxy on both LB's:


To start it on LB2 you might have to fail LB1 first so that the shared IP moves to LB2 or make the following kernel change:


On the back-end apache nodes create a simple index.html like so:


Now hit 192.168.29.100 in your browser and refresh few times. You should see both nodes rotating in a round-robin fashion.
Also test the HA setup by failing one of the LB servers making sure that you always get a response back from the back-end nodes. Do the same for the back-end nodes.

To send logs from HAProxy to syslog-ng add the following lines to the syslog-ng config file:

We can use Pound, which is a reverse proxy that supports SSL termination to listen for SSL connections on port 443 and terminate them using a local certificate. Pound will then insert a header in each HTTP packet called "X-Forwarded-Proto: https" that HAproxy will look for and if absent HAProxy will forward the insecure connections to port 443.
Installing pound is straight forward and can be done from a package or from source. Once installed the config file should look like this:
Pound will now listen on port 443 for secure connections, terminate them using the local.server.pem certificate then inset the "X-Forwarded-Proto: https" header in the HTTP packet and forward it to HAProxy which is running and listening on the same host on port 80.
To make HAProxy forward all insecure connections from port 80 to port 443 all we need to do is create an access list that looks for the header that Pound inserts and if missing redirect the HTTP connections to Pound (listening on port 443).
The new config needs to look like this:
The two new lines at 31 and 32 create an access list that looks for (case insensitive -i) the https string in the X-Forwarded-Proto header. If the string is not there (meaning the connection came on port 80 directly hitting HAproxy) redirect to the secure SSL port 443 that Pound is listening on. This will ensure that each time a client hits port 80 the connection will be redirected to port 443 and secured. Same goes for if the client connects directly to port 443.
To generate a self-signed cert to use in Pound run this:

Resources:

[1] http://haproxy.1wt.eu/
[2] http://www.keepalived.org/
[3] http://en.wikipedia.org/wiki/Virtual_Router_Redundancy_Protocol
[4] http://haproxy.1wt.eu/download/1.2/doc/architecture.txt
[5] http://www.apsis.ch/pound/




10 comments:

  1. Good tutorial. Thanks.

    ReplyDelete
  2. I'm a little confused by the inclusion of Pound. I understand we're setting up an SSL termination point, but I don't know what IP should be used for the Pound backend. If it's a backend HTTP server, we've lost our high-availability component; if it's the keepalive IP, aren't we stuck in an infinite loop?

    ReplyDelete
  3. Pound is listening on the same VIP, but on port 443 instead. All requests once unencrypted are then forwarded to HAProxy. Both HAProxy and Pound can be made highly available using keepalived, or whatever other technology you might choose.

    ReplyDelete
  4. Ah, I see. I read the HAProxy redirect logic incorrectly. Serves me right for reading this at 3 AM ;) Thanks!

    ReplyDelete
  5. Hi, where does the IP address in this line come from : "listen webfarm 10.23.168.254:80" ?

    ReplyDelete
    Replies
    1. This should be the VIP - 192.168.29.100 - instead. I updated the blog with the correct IP. Thank you for pointing that out!

      Delete
  6. Thanks for this blog post.
    Please may you update it and add the following before starting the HAproxy service:

    nano /etc/default/haproxy
    Change ENABLED=0 to ENABLED=1 .

    If this isn't done, you can't start the haproxy using the init.d script.

    Thanks

    ReplyDelete
  7. Thank you for the post. Could it work with nginx load balancer?

    ReplyDelete