Active HAProxy Load Balancer as a Rancher Service
This is similar to the built-in load balancer offered by rancher, but is extended with support for the following features:
- Add arbitrary custom HAProxy configuration via service metadata
- Automatically add new services using docker labels
- Default vhost domains for services using root domains and service/stack names
- Easily expose HAProxy stats page
- Optional redirect to a custom error and/or 404 page
- Optional force redirect to https
- Redirect haproxy logs to stdout by default (this is notoriously painful to configure in docker)
- Expose a 'ping' port for maintaining host membership of external load-balancers via external health checks (think ELB)
- Support for
accept-proxy
frontend option (again, useful for ELB)
In its most basic form, finboxio/rancher-lb
requires minimal configuration to work. You can try it out by deploying the docker/rancher-compose sample files included here. A brief explanation of each follows:
haproxy:
ports:
- 80:80/tcp
labels:
io.rancher.scheduler.global: 'true'
lb.haproxy.9090.frontend: 80/http
image: finboxio/rancher-lb
haproxy:
health_check:
port: 80
interval: 2000
initializing_timeout: 20000
unhealthy_threshold: 3
strategy: recreate
response_timeout: 2000
healthy_threshold: 2
metadata:
stats:
port: 9090
global:
- maxconn 4096
- debug
defaults:
- timeout connect 5000
domains:
- http://rancher
scope: service
The docker-compose file is pretty straightforward. Run the image on all hosts and bind to port 80.
You might notice that the global
section in our service metadata accepts arbitrary haproxy configuration lines. These will be inserted into the global configuration section of the HAProxy config file. Likewise, you can specify a defaults
list, whose lines will be added to the defaults
section of the HAProxy config. If you don't know what to put there, just leave them out. It'll still work. Wow. Such power. So simple.
The label lb.haproxy.9090.frontend=80/http
is where the magic happens, and exactly what it does depends on how we configure our service metadata.
In this case, it tells finboxio/rancher-lb
to create an http frontend in HAProxy that listens on port 80, and to further create a backend that balances all requests with the Host header haproxy.rancher
to port 9090 across all of the healthy containers in this service. This is our haproxy stats page, since our metadata is configured to expose that on :9090/
(see metadata.stats.{port,path}
in rancher-compose).
That's it. Our frontend is automatically created, our backend is automatically set up, our acls routing to that backend are automatically configured, and healthy containers are automatically added as they come and go. As services with similar labels are added/removed, the router will automatically expose/drop them.
IMPORTANT
Containers will only be activated if they are explicitly marked healthy by rancher. This means that if your service does not have a rancher healthcheck defined,
finboxio/rancher-lb
will never send traffic to any of its containers.
To customize your finboxio/rancher-lb
deployment, you can define options in the service metadata. Any of these options can be omitted, but here's a complete sample of what's supported. Details of what each option does are given in the following sections:
metadata:
scope: {service|stack|environment}
health:
port: <port>
path: <path>
stats:
port: <port>
path: <path>
admin: {true|false}
global:
- <global setting 1>
- <global setting 2>
- ...
defaults:
- <default setting 1>
- <default setting 2>
- ...
domains:
- <root domain 1>
- ...
frontends:
<port>/{http|tcp}:
proxy: {true|false}
options:
- <frontend option 1>
- <frontend option 2>
- ...
<other-port>/{http|tcp}:
...
...
You can also specify custom error/fallback pages with environment variables ERROR_URL=<url>
and FALLBACK_URL=<url>
.
Finally, to enable automatic service registration with your load-balancer, add the following labels to each health-checked service you'd like to access. Note that *.frontend
is required, while *.domain[s]
is optional.
<lb-stack>.<lb-service>.<service-port>.frontend=<frontend-port>/{http|tcp}
<lb-stack>.<lb-service>.<service-port>.domain[s]={http|https}://<hostname1>,...
It may not be obvious from our sample configuration, but the labels that finboxio/rancher-lb
recognizes and uses to update the HAProxy config take the following form:
<lb-stack>.<lb-service>.<container-port>.{frontend|domain|domains}=<value>
lb-stack
and lb-service
are the stack and service name of your finboxio/rancher-lb
deployment. In the sample case, it's assumed that finboxio/rancher-lb
is deployed as a service named haproxy
in a stack named lb
. Using dynamic labels like this allows us to run multiple load balancer deployments and only expose certain services/ports on one or the other without conflicts. container-port
tells our load-balancer to apply the corresponding rule to the given container port. This allows us to expose different ports of the same service under different hostnames or frontend ports.
In the sample configuration, we specified the frontend value for our stats page as 80/http
. In general, any value of the form <port>/{http|tcp}
will configure an http or tcp haproxy frontend listening on port <port>
in the rancher-lb
container and add ACL rules for your service to this frontend. It's your responsibility to make sure this port is appropriately exposed to whoever needs to access it.
If you want to set defaults or otherwise further configure this frontend, you can add details to your rancher-lb
service metadata under the frontends
key, eg:
metadata:
frontends:
80/http:
proxy: true
options:
- acl not_found status 404
- acl type_html res.hdr(Content-Type) -m sub text/html
- http-request capture req.hdr(Host) len 64
- http-response redirect location https://null.finbox.io/?href=http://%{+Q}[capture.req.hdr(0)]%HP code 303 if not_found type_html
The proxy
setting enables proxy-protocol for the frontend. If you don't know what that is, you probably don't want it, but it is really useful when running behind something like ELB, as it's required if you want to support websockets (I think), redirect to https, or get the original client IP passed along.
Protip
This specific configuration in
options
is an example of a useful haproxy trick to set up a universal 404 page for all of your services. It listens for 404 html responses for any of your backends and redirects them all to a single page with a reference to the missing resource that was requested.
You should be aware that it's impossible to have both a tcp and an http frontend listening on the same port. So try not to specify conflicting labels like lb.haproxy.5000.frontend=80/tcp
on one service and lb.haproxy.8080.frontend=80/http
on another. It doesn't make sense and it won't work. I don't know exactly what will happen, but all of your friends will definitely make fun of you and your mother will probably stop answering your calls.
Just like we specified our frontend, we can also specify one or more domains that should be routed to our service.
lb.haproxy.9090.domains=http://foo.finbox.io,https://bar.finbox.io
If you only need one domain and have a perfectly rational aversion to unnecessary pluralization like me, you can use the form
lb.haproxy.9090.domain=http://foo.finbox.io
It's your responsibility to ensure that the DNS records for these domains are properly configured to point to your load-balancer.
This will setup ACLs such that incoming requests with a Host
header matching foo.finbox.io
or bar.finbox.io
will be routed to port 9090 of your service.
Additionally, any bar.finbox.io
request that is not sent over ssl will be redirected to https://bar.finbox.io
.
Note
This https redirection assumes you're using proxy-protocol, and that the destination port for all https traffic is 443. If either of these assumptions aren't true, it probably won't behave the way you want it to. This project doesn't have built-in support for local SSL termination (though you could probably set it up with metadata and mounted certs). It's limited in this respect simply because it fits the only use-case we have right now (running everything behind ELB with SSL termination there using ACM certificates), but PRs are welcome.
You might have noticed that we didn't specify any domains for our stats page in the sample configuration. finboxio/rancher-lb
will generate a default domain for any service with a registered frontend using <scope>.<domain>
semantics.
<scope>
can be defined in your service metadata, and it determines the prefix for default domains:
Scope | Prefix |
---|---|
service | <service_name> |
stack (default) | <service_name>.<stack_name> |
environment | <service_name>.<stack_name>.<environment_name> |
This prefix is combined with each root domain
specified in your service metadata (you may specify more than one), to generate a list of default domains for each service.
So in the sample configuration, we're using the service
scope, and have specified a single root domain of http://rancher
. Since our stats page is running under a service named haproxy
, default rules are created to route Host: haproxy.rancher
traffic to it. If we're happy with that, we don't need to specify any additional domains.
Note that the https redirection semantics described above also apply to root domains if you specify them with https://
protocol.
It goes without saying (but should probably be said anyways) that vhosts don't apply to tcp-only frontends. Any domains you specify for a service with a tcp frontend will be ignored.
Custom error pages can be configured via environment variables. If you run this load-balancer with ERROR_URL=<your-url.com>
, errors generated by HAProxy will trigger a redirection to your-url.com
. This works for things like 504 gateway timeouts, 503 no healthy servers available, etc. but does not redirect for errors generated from your own web service.
You can also specify a FALLBACK_URL=<not-found.com>
url. If a request comes for which no appropriate backend can be found, it will be redirected to this url. The subtle difference between this and ERROR_URL
allows you to send visitors to different pages depending on whether a service is unhealthy or simply does not exist.
When such a redirect is activated, HAProxy will append an ?href=
query parameter with the value of the original url that was requested, so you can react accordingly on your custom error/fallback page.
These urls should obviously be accessible independent of this load-balancer. We host ours statically on S3.
Since we run everything behind ELB, it's important to be able to automatically determine when an instance is ready to accept traffic. In your service metadata, you can configure a 'ping' port that always reports 200.
metadata:
health:
port: 79
path: /
The
path
property is optional here. Also, make sure you bind this port to the host if you plan to use it for something like ELB healthchecks.
finboxio/rancher-lb
is super flexible and is works really well for what we need right now, so we don't have plans to add anything in the near-term. But here's a list of things that I could see us needing in the future or might be cool to add if anyone wants to submit a PR.
- LetsEncrypt support
- Local SSL termination
- Routing based on uri paths as well as hostnames (partially implemented, totally untested)
- Use janeczku/rancher-template to simplify the config templates