So You Got Yourself a Load Balancer
When you put your web application behind a load balancer, or any type of reverse proxy, you immediately need to take some important factors into consideration.
This article will cover those considerations, as well as discuss common solutions.
Using a load balancer implies that you have more than one web server processing requests. In this situation, how you manage your static assets (images, JS, CSS files) becomes important.
If you accept user uploads (such as with a CMS), the uploaded file can't simply live on the web server it was uploaded to. When an uploaded jpg file only lives on one web server, a request for that image will result in a 404 response when the load balancer attempts to find it on web server which does not have the image.
The easiest solution to this is to have all assets on all web servers. This can work if your site is simple, and doesn't take any file uploads from users. Any images or assets can be part of your sites code repository and pushed to all servers.
However, if you get into the realm of managing content or otherwise allowing file uploads, then the web servers need to have a common file store they can access.
One way this is done is via a shared network drive (NAS, for example). This, however, get's slow when there are many files or high levels of traffic. Furthermore, if your architecture is distributed across several data centers, then a shared network drive can become too slow; Your web servers would be too far away from them and the network latency too high.
A common (and better) solution is to host all static assets in a separate location, such as Amazon's S3.
Within Amazon, this can be taken a step further. An S3 bucket can be integrated with their CDN CloudFront. Your files can then be served via a true CDN.
For your static assets, you can use automated tools such as Grunt to automate these tasks for you. For example, you can have Grunt watch your files for changes, minify and concatenate CSS, JS and images, as well as generate production-ready files, and then upload them to a location of your choice.
For user-uploaded content, you'll likely need to do some coding around uploading files to a temporary location, and then sending them off to S3 via AWS's API. This is actually pretty easy.
One thing I do on projects is to change the URL of assets based on the environment. Using a helper function of some sort, I'll have code output the development machine's URL out to HTML so the files are loaded locally. However, in production, this helper can output URLs for your file-store or CDN of choice. Combined with some automation (Grunt), this gives you a fairly seamless workflow between development and production.
Similarly to the issue of Asset Management, how you handle sessions becomes an important consideration. Session information is often saved on a temporary location within a web server. A user may log in, creating a session on one web server. On a subsequent request, however, the load balancer may bounce that user to another web server which doesn't have that session information. The client would think they were forcefully logged out.
There are two common fixes for this.
The first is to set your load balancer to user "sticky sessions", often also called "session affinity". This will take one client and always route their request to the same web server. This let's the web server keep its default behavior of saving the session locally, leaving it up to the load balancer to get a client back to that server. This can skew the sharing of work load around your web servers a bit.
The second fix for this is to use a shared session storage mechanism. Typical memory stores used for sessions are in-memory stores such as Redis or Memcached. Persistent stores such as a database are also commonly used. Since session data does not necessarily beed to be persistent, and can have lots of traffic, an in-memory data store may be preferred. In any case, this architecture lets all the web servers connect to a central session store, growing your infrastructure a bit, but letting your work load be truly distributed.
Lost Client Information
Closely related to the session issue is detecting who the client is. If the load balancer is a proxy to your web application, it might appear to your application that every request is coming from the load balancer! Your application wouldn't be able to tell one client from the other!
Luckily, most load balancers provide a mechanism for giving your application this information. If you inspect the headers of a request received from a load balancer, you might see these included:
These headers can tell you (respectively) the client's IP address, the schema used (http vs https) and which port the client made the request on. If these are present, your application's job is to sniff these headers out and use them in place of the usual client information (to avoid thinking every client is the load balancer).
Having an accurate IP address of a client is important. Web applications often use a user's IP address to help identify a client as part of the authentication process. Some applications use the client's IP address to perform functions such as rate limiting or other throttling techniques. Furthermore, having a client's IP address can help identify malicious traffic patterns when inspecting access logs.
X-Forwarded-For header, which should include the client's IP address, should be used if the header is found (assuming the source of the proxy request is trusted).
Protocol/Schema and Port
Knowing the protocol (http, https) and port used by the client is also important. If the client is connecting over an SSL (with a https url), that encrypted connection might end at the load balancer. The load balancer would then end a "http" request to the web servers. This means the web servers will receive the traffic over "http" instead of "https". Many application such as Laravel attempt to guess the site address based on the request information. If your web application is receiving a "http" request over port 80, then any URLs it generates or redirects it sends will likely be on the same protocol. This means that a user might get redirected to a page with the wrong protocol or port when behind a load balancer!
Sniffing out the
X-Forwarded-Port header then becomes important so that the web application can generate correct URLs for redirects or for printing out in templates (think form actions and links to other site pages).
Many frameworks, including Symfony and Laravel, handle this for you. However, they ask you to configure a "trusted proxy". If the request comes from a proxy who's IP address is trusted, then the framework will seek out and use the
X-Forwarded-* headers in place of the usual mechanisms for gathering that information.
This provides a very nice abstraction over this HTTP mechanism, allowing you to forget this is a potential issue while coding!
As noted above, in a load balanced environment, SSL traffic is often decrypted at the load balancer. However, there's actually a few ways to handle SSL traffic when using a load balancer.
When the load balancer is responsible for decrypting SSL traffic before passing the request on, it's referred to as "SSL Termination". In this scenario, the load balancer alleviates the web servers of the extra CPU power needed to decrypt SSL traffic. It also gives the load balancer the opportunity to append the
X-Forwarded-* headers to the request before passing it onward.
The downside of SSL Termination is that the traffic between the load balancers and the web servers is not encrypted. This leaves the application open to possible man-in-the-middle attacks. However, this is a risk usually mitigated by the fact that the load balancers are often within the same infrastructure (data center) as the web servers. Someone would have to get access to traffic between the load balancers and web servers by being within the data-centers internal network (possible, but less likely).
Amazon AWS load balancers also give you the option of generating a (self-signed) SSL for use between the load balancer and the web servers, giving you a secure connection all around. This, of course, means more CPU power being used, but if you need the extra security due to the nature of your application, this is an great option.
Alternatively, there is "SSL Pass-Through". In this scenario, the load balancer does not decrypt the request, but instead passes the request through to a web server. The web server than must decrypt it.
This solution obviously costs the web servers more CPU cycles. You also often lose some extra functionality that load-balancing proxies can provide, such as DDoS protection. However, this option is often used when security is an important concern (although SSL Termination followed by re-encryption seems to be a good compromise).
I've heard mixed reports on whether or not SSL Pass-Through prevents the addition of the
X-Forwared-* headers, even from the help desk of some popular PaaS providers, so if anyone knows for sure if these headers can be added to a request by the load balancer when passing the traffic through, please comment and say so!
So, now you have multiple web servers, but each one generates their own log files! Going through each servers' logs is tedious and slow. Centralizing your logs can be very beneficial.
The simplest ways I've done this is to combine Logrotate's functionality with an uploaded to an S3 bucket. This at least puts all the log files in one place that you can look into.
However, there's plenty of centralized logging servers that you can install in your infrastructure or purchase. The SaaS offerings in this arena are often easily integrated, and usually provide extra services such as alerting, search and analysis.
Here are some resources to go along with the article.
Grunt and Asset Management
Some popular self-install loggers:
Some popular SaaS loggers:
- Splunk Storm
- Paper Trail
- Bugsnap - Captures errors, not necessarily all logs
Other Log Tools:
Trusted Proxycomments powered by Disqus