├── .gitignore ├── scripts ├── postrelease ├── release ├── release-notes ├── prerelease └── publish ├── entries.toml ├── stack ├── docker-compose.yml └── rancher-compose.yml ├── lb-conf ├── supervisor.conf ├── rsyslog.conf ├── package.json ├── LICENSE ├── entries.tpl ├── lb-bootstrap ├── History.md ├── Dockerfile ├── Makefile ├── lb-reload ├── haproxy.cfg.tpl └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | lb/ 2 | prod/ 3 | -------------------------------------------------------------------------------- /scripts/postrelease: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git push 3 | git push --tags 4 | scripts/publish 5 | -------------------------------------------------------------------------------- /entries.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | src = "entries.tpl" 3 | dest = "/etc/lb/entries" 4 | mode = "0644" 5 | keys = [ 6 | "/stacks", 7 | "/self" 8 | ] 9 | reload_cmd = "/usr/bin/lb-reload" 10 | -------------------------------------------------------------------------------- /stack/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | haproxy: 4 | ports: 5 | - 80:80/tcp 6 | labels: 7 | io.rancher.scheduler.global: 'true' 8 | io.rancher.container.pull_image: always 9 | lb.haproxy.9090.frontend: 80/http 10 | image: finboxio/rancher-lb 11 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the new package version 4 | VERSION=$(node -pe "require('./package.json').version") 5 | 6 | # Update changelog 7 | git changelog -t $VERSION 8 | 9 | # Attempt to prevent race where .git/index.lock 10 | # isn't cleared immediately 11 | sleep 0.5 12 | 13 | git add History.md 14 | -------------------------------------------------------------------------------- /lb-conf: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | version= 4 | while true; do 5 | updated=$(wget -qO- http://rancher-metadata.rancher.internal/2015-12-19/version) 6 | if [[ "$updated" != "$version" ]]; then 7 | version=$updated 8 | confd -confdir /etc/confd -log-level ${LOG_LEVEL:-info} -backend rancher -node rancher-metadata.rancher.internal -prefix /2015-12-19 -onetime true 9 | fi 10 | sleep 4 11 | done 12 | -------------------------------------------------------------------------------- /stack/rancher-compose.yml: -------------------------------------------------------------------------------- 1 | haproxy: 2 | health_check: 3 | port: 80 4 | interval: 2000 5 | initializing_timeout: 20000 6 | unhealthy_threshold: 3 7 | strategy: recreate 8 | response_timeout: 2000 9 | healthy_threshold: 2 10 | metadata: 11 | scope: service 12 | stats: 13 | port: 9090 14 | global: 15 | - maxconn 4096 16 | - debug 17 | domains: 18 | - http://rancher 19 | -------------------------------------------------------------------------------- /supervisor.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile=/var/log/supervisord.log 3 | pidfile=/var/run/supervisord.pid 4 | nodaemon=true 5 | 6 | [program:haproxy-log] 7 | process_name=haproxy-log 8 | stdout_logfile=/dev/stdout 9 | stdout_logfile_maxbytes=0 10 | redirect_stderr=true 11 | command=tail -F /var/log/haproxy.log 12 | 13 | [program:haproxy-conf] 14 | process_name=haproxy-conf 15 | stdout_logfile=/dev/stdout 16 | stdout_logfile_maxbytes=0 17 | redirect_stderr=true 18 | command=/usr/bin/lb-conf 19 | -------------------------------------------------------------------------------- /scripts/release-notes: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | current_tag=$(git tag | tail -n 1) 6 | current_version=${current_tag#"v"} 7 | 8 | previous_tag=$(hub release --include-drafts | head -n 1) 9 | previous_version=${previous_tag#"v"} 10 | 11 | if [ -z "$previous_tag" ]; then 12 | echo "Couldn't detect previous release tag" >&2 13 | exit 1 14 | fi 15 | 16 | cat History.md | 17 | sed "1,/^${current_version} \//d" | 18 | sed -n "/^${previous_version} \//q;p" | 19 | grep '^\s*[*]' 20 | -------------------------------------------------------------------------------- /rsyslog.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Use traditional timestamp format. 3 | # To enable high precision timestamps, comment out the following line. 4 | # 5 | $ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat 6 | 7 | $DebugFile /var/log/rsyslog.log 8 | $DebugMode 2 9 | 10 | # 11 | # Set the default permissions for all log files. 12 | # 13 | $FileOwner root 14 | $FileGroup adm 15 | $FileCreateMode 0640 16 | $DirCreateMode 0755 17 | $Umask 0022 18 | 19 | # 20 | # Where to place spool and state files 21 | # 22 | $WorkDirectory /var/spool/rsyslog 23 | 24 | # UDP Syslog Server: 25 | $ModLoad imudp.so # provides UDP syslog reception 26 | $UDPServerRun 32000 # start a UDP syslog server at port 32000 27 | 28 | local2.* -/var/log/haproxy.log 29 | & ~ 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rancher-lb", 3 | "version": "0.3.1", 4 | "description": "active haproxy load balancer for rancher", 5 | "main": "index.js", 6 | "scripts": { 7 | "preversion": "scripts/prerelease", 8 | "version": "scripts/release", 9 | "postversion": "scripts/postrelease", 10 | "publish": "scripts/publish" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/finboxio/rancher-lb.git" 15 | }, 16 | "keywords": [ 17 | "rancher", 18 | "haproxy", 19 | "loadbalancer" 20 | ], 21 | "author": "finbox.io", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/finboxio/rancher-lb/issues" 25 | }, 26 | "homepage": "https://github.com/finboxio/rancher-lb#readme" 27 | } 28 | -------------------------------------------------------------------------------- /scripts/prerelease: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Verifying that local master has all remote work..." 4 | 5 | git branch | grep "* master" &> /dev/null || \ 6 | { echo "Must be on master branch! Aborting." && exit 1; } 7 | 8 | git rev-parse @{u} &> /dev/null || \ 9 | { echo "Upstream is not set for branch! Aborting." && exit 1; } 10 | 11 | git remote -v update &> /dev/null 12 | LOCAL=$(git rev-parse @) 13 | REMOTE=$(git rev-parse @{u}) 14 | BASE=$(git merge-base @ @{u}) 15 | 16 | if [[ "$LOCAL" = "" || "$REMOTE" = "" || "$BASE" = "" ]]; then 17 | echo "Could not determine repository status! Aborting." 18 | exit 1 19 | fi 20 | 21 | if [[ $LOCAL = $REMOTE || $REMOTE = $BASE ]]; then 22 | echo "Local master is up-to-date" 23 | else 24 | echo "Local repository is out of sync with remote! Aborting." 25 | exit 1 26 | fi 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 finbox.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GITHUB_USER=$(node -pe "require('./package.json').repository.url.split('/')[3]") 4 | GITHUB_REPO=$(node -pe "require('./package.json').repository.url.split('/')[4].replace('.git', '')") 5 | CURRENT_BRANCH=$(git symbolic-ref HEAD 2>/dev/null | cut -d"/" -f 3) 6 | CURRENT_VERSION=$(node -pe "require('./package.json').version") 7 | PUBLISH_VERSION=${1:-$CURRENT_VERSION} 8 | 9 | [[ $PUBLISH_VERSION == *-* ]] && PRERELEASE=1 || PRERELEASE= 10 | 11 | git checkout tags/v$PUBLISH_VERSION &> /dev/null || { 12 | echo "Failed to checkout version tag!" 13 | exit 1 14 | } 15 | 16 | git status | grep "nothing to commit, working directory clean" || { 17 | echo "Working directory is not clean!" 18 | git checkout $CURRENT_BRANCH &> /dev/null 19 | exit 1 20 | } 21 | 22 | if hub release --include-drafts | grep -q "^v${PUBLISH_VERSION}\$"; then 23 | echo "Release already exists!" 24 | git checkout $CURRENT_BRANCH &> /dev/null 25 | open https://github.com/${GITHUB_USER}/${GITHUB_REPO}/releases 26 | exit 1 27 | else 28 | echo "Creating release draft for v${PUBLISH_VERSION}" 29 | 30 | RELEASE_NOTES=$( 31 | echo "Release ${PUBLISH_VERSION}" 32 | echo 33 | echo "## Changes" 34 | echo 35 | 36 | ./scripts/release-notes 37 | ) 38 | 39 | RELEASE_URL=$(echo "$RELEASE_NOTES" | hub release create -F - --draft ${PRERELEASE:+--prerelease} "v$PUBLISH_VERSION") 40 | 41 | open $RELEASE_URL 42 | 43 | git checkout $CURRENT_BRANCH &> /dev/null 44 | fi 45 | 46 | 47 | -------------------------------------------------------------------------------- /entries.tpl: -------------------------------------------------------------------------------- 1 | {{- $split := getenv "SPLIT_TOKEN" "__split__" }} 2 | {{- $my_stack := getv "/self/service/stack_name" }} 3 | {{- $my_service := getv "/self/service/name" }} 4 | {{- range $s, $stack_name := ls "/stacks" }} 5 | {{- range $i, $service_name := ls (printf "/stacks/%s/services" $stack_name) }} 6 | {{- range $l, $label := ls (printf "/stacks/%s/services/%s/labels" $stack_name $service_name) }} 7 | {{- $list := split $label "." }} 8 | {{- if gt (len $list) 1 }} 9 | {{- if (and (eq (index $list 0) $my_stack) (eq (index $list 1) $my_service)) }} 10 | {{- $port := index $list 2 }} 11 | {{- $key := index $list 3 }} 12 | {{- $value := getv (printf "/stacks/%s/services/%s/labels/%s" $stack_name $service_name $label) }} 13 | {{ $stack_name }}{{ $split }}{{ $service_name }}{{ $split }}{{ $port }}{{ $split }}{{ $key }}{{ $split }}{{ $value }} 14 | {{- range $c, $container_name := ls (printf "/stacks/%s/services/%s/containers" $stack_name $service_name) }} 15 | {{- if exists (printf "/stacks/%s/services/%s/containers/%s/primary_ip" $stack_name $service_name $container_name) }} 16 | {{- $ip := getv (printf "/stacks/%s/services/%s/containers/%s/primary_ip" $stack_name $service_name $container_name) }} 17 | {{- $state := getv (printf "/stacks/%s/services/%s/containers/%s/state" $stack_name $service_name $container_name) }} 18 | {{- $health := getv (printf "/stacks/%s/services/%s/containers/%s/health_state" $stack_name $service_name $container_name) }} 19 | {{ $stack_name }}{{ $split }}{{ $service_name }}{{ $split }}{{ $port }}{{ $split }}container{{ $split }}{{ $ip }}{{ $split }}{{ $state }}{{ $split }}{{ $health }} 20 | {{- end }} 21 | {{- end }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | {{- end }} 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /lb-bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ "$ERROR_URL" != "" ]]; then 4 | cat < /etc/lb/500.http 5 | HTTP/1.0 500 Internal Server Error 6 | Cache-Control: no-cache 7 | Connection: close 8 | Content-Type: text/html 9 | 10 | 11 | 12 | 13 | ERR 14 | 15 | cat < /etc/lb/502.http 16 | HTTP/1.0 502 Bad Gateway 17 | Cache-Control: no-cache 18 | Connection: close 19 | Content-Type: text/html 20 | 21 | 22 | 23 | 24 | ERR 25 | 26 | cat < /etc/lb/503.http 27 | HTTP/1.0 503 Service Unavailable 28 | Cache-Control: no-cache 29 | Connection: close 30 | Content-Type: text/html 31 | 32 | 33 | 34 | 35 | ERR 36 | 37 | cat < /etc/lb/504.http 38 | HTTP/1.0 504 Gateway Timeout 39 | Cache-Control: no-cache 40 | Connection: close 41 | Content-Type: text/html 42 | 43 | 44 | 45 | 46 | ERR 47 | else 48 | cp /etc/haproxy/errors/*.http /etc/lb/ 49 | fi 50 | 51 | if [[ "$FALLBACK_URL" != "" ]]; then 52 | cat < /etc/lb/404.http 53 | HTTP/1.0 404 Not Found 54 | Cache-Control: no-cache 55 | Connection: close 56 | Content-Type: text/html 57 | 58 | 59 | 60 | 61 | ERR 62 | else 63 | cp /etc/haproxy/errors/503.http /etc/lb/404.http 64 | fi 65 | 66 | confd -confdir /etc/confd -log-level debug -backend rancher -node rancher-metadata.rancher.internal -prefix /2015-12-19 -onetime true 67 | while [[ ! -e /etc/haproxy/haproxy.cfg ]]; do 68 | echo "Waiting for HAProxy config" 69 | sleep 10 70 | confd -confdir /etc/confd -log-level debug -backend rancher -node rancher-metadata.rancher.internal -prefix /2015-12-19 -onetime true 71 | done 72 | 73 | rsyslogd 74 | /usr/local/sbin/haproxy -D -p /var/run/haproxy.pid -f /etc/haproxy/haproxy.cfg -sf $(cat /var/run/haproxy.pid) 75 | supervisord -c /etc/supervisor/supervisor.conf 76 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.3.1 / 2020-01-02 3 | ================== 4 | 5 | * Fix line spacing 6 | 7 | 0.3.0 / 2020-01-02 8 | ================== 9 | 10 | * add gitignore 11 | * Add support for basic auth in domain 12 | 13 | 0.2.1 / 2017-09-17 14 | ================== 15 | 16 | * Update stack file 17 | 18 | 0.2.0 / 2017-09-17 19 | ================== 20 | 21 | * Update makefile 22 | * Update stack file 23 | * Init npm package 24 | * Upgrade stack files 25 | * Add release scripts 26 | 27 | 0.1.2 / 2016-11-09 28 | ================== 29 | 30 | * update makefile 31 | * reload haproxy with only one pid 32 | 33 | 0.1.1 / 2016-10-28 34 | ================== 35 | 36 | * add X-Path-Prefix header when path is rewritten (for safe redirects) 37 | 38 | 0.1.0 / 2016-10-28 39 | ================== 40 | 41 | * move default backend selection below custom domains 42 | * add support for path-based routing 43 | 44 | 0.0.10 / 2016-10-12 45 | =================== 46 | 47 | * make default log level 'info' 48 | * make default log level 'warn' 49 | * add rancher deploy command to makefile 50 | * remove .dev tld 51 | * readme updates 52 | * readme updates 53 | * document the sonofabitch 54 | 55 | 0.0.9 / 2016-09-10 56 | ================== 57 | 58 | * setup default errorfiles if not given 59 | 60 | 0.0.8 / 2016-09-10 61 | ================== 62 | 63 | * add checks for ERROR_URL and FALLBACK_URL before replacing errorfiles 64 | * update example domain to rancher.dev 65 | 66 | 0.0.7 / 2016-09-10 67 | ================== 68 | 69 | * make proxy-protocol optional 70 | 71 | 0.0.6 / 2016-09-10 72 | ================== 73 | 74 | * use absolute path for entrypoint 75 | 76 | 0.0.5 / 2016-09-10 77 | ================== 78 | 79 | * add sample stack for local rancher cluster 80 | * set entrypoint in dockerfile 81 | 82 | 0.0.4 / 2016-09-06 83 | ================== 84 | 85 | * fix https-redirect rules 86 | * only redirect to https for root domain if host matches 87 | 88 | 0.0.3 / 2016-08-25 89 | ================== 90 | 91 | * add latest/dev tags to current docker build 92 | 93 | 0.0.2 / 2016-08-25 94 | ================== 95 | 96 | * check container state and health before exposing 97 | * recognize labels based on ... pattern 98 | 99 | 0.0.1 / 2016-08-25 100 | ================== 101 | 102 | * Initial commit 103 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13-alpine AS go 2 | 3 | # install gotpl & confd 4 | RUN set -x \ 5 | && apk add --no-cache git \ 6 | && export GOPATH=/usr/src/go \ 7 | && go get github.com/tsg/gotpl \ 8 | && go get github.com/kelseyhightower/confd \ 9 | && apk del --no-cache git 10 | 11 | FROM alpine:3.4 AS final 12 | 13 | # install dependencies 14 | RUN apk add --no-cache curl nodejs rsyslog supervisor \ 15 | && npm install -g json2yaml merge-yaml js-yaml lodash.merge \ 16 | && curl -L -o /usr/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 \ 17 | && chmod +x /usr/bin/jq \ 18 | && mkdir -p /etc/rsyslog.d /etc/haproxy /etc/confd /etc/lb /etc/supervisor /var/spool/rsyslog 19 | ENV NODE_PATH /usr/lib/node_modules 20 | 21 | # install haproxy 22 | ENV HAPROXY_MAJOR 1.6 23 | ENV HAPROXY_VERSION 1.6.7 24 | ENV HAPROXY_MD5 a046ed63b00347bd367b983529dd541f 25 | RUN set -x \ 26 | && apk add --no-cache --virtual .build-deps \ 27 | curl \ 28 | gcc \ 29 | libc-dev \ 30 | linux-headers \ 31 | make \ 32 | openssl-dev \ 33 | pcre-dev \ 34 | zlib-dev \ 35 | && curl -SL "http://www.haproxy.org/download/${HAPROXY_MAJOR}/src/haproxy-${HAPROXY_VERSION}.tar.gz" -o haproxy.tar.gz \ 36 | && echo "${HAPROXY_MD5} haproxy.tar.gz" | md5sum -c \ 37 | && mkdir -p /usr/src \ 38 | && tar -xzf haproxy.tar.gz -C /usr/src \ 39 | && mv "/usr/src/haproxy-$HAPROXY_VERSION" /usr/src/haproxy \ 40 | && rm haproxy.tar.gz \ 41 | && make -C /usr/src/haproxy \ 42 | TARGET=linux2628 \ 43 | USE_PCRE=1 PCREDIR= \ 44 | USE_OPENSSL=1 \ 45 | USE_ZLIB=1 \ 46 | all \ 47 | install-bin \ 48 | && cp -R /usr/src/haproxy/examples/errorfiles /etc/haproxy/errors \ 49 | && rm -rf /usr/src/haproxy \ 50 | && runDeps="$( \ 51 | scanelf --needed --nobanner --recursive /usr/local \ 52 | | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ 53 | | sort -u \ 54 | | xargs -r apk info --installed \ 55 | | sort -u \ 56 | )" \ 57 | && apk add --no-cache --virtual .haproxy-rundeps $runDeps \ 58 | && apk del --no-cache .build-deps 59 | 60 | # install gotpl & confd 61 | COPY --from=go /usr/src/go/bin/gotpl /usr/bin/gotpl 62 | COPY --from=go /usr/src/go/bin/confd /usr/bin/confd 63 | 64 | ADD entries.tpl /etc/confd/templates/entries.tpl 65 | ADD entries.toml /etc/confd/conf.d/entries.toml 66 | ADD rsyslog.conf /etc/rsyslog.d/rsyslog.conf 67 | ADD haproxy.cfg.tpl /etc/lb/haproxy.cfg.tpl 68 | ADD supervisor.conf /etc/supervisor/supervisor.conf 69 | 70 | VOLUME /etc/haproxy 71 | 72 | COPY lb-* /usr/bin/ 73 | RUN chmod +x /usr/bin/lb-* 74 | 75 | ENTRYPOINT /usr/bin/lb-bootstrap 76 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_USER=finboxio 2 | DOCKER_IMAGE=rancher-lb 3 | RANCHER_ENV=local 4 | 5 | GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 6 | GIT_COMMIT := $(shell git rev-parse HEAD) 7 | GIT_REPO := $(shell git remote -v | grep origin | grep "(fetch)" | awk '{ print $$2 }') 8 | GIT_DIRTY := $(shell git status --porcelain | wc -l) 9 | GIT_DIRTY := $(shell if [[ "$(GIT_DIRTY)" -gt "0" ]]; then echo "yes"; else echo "no"; fi) 10 | 11 | VERSION := $(shell git describe --abbrev=0) 12 | VERSION_DIRTY := $(shell git log --pretty=format:%h $(VERSION)..HEAD | wc -l | tr -d ' ') 13 | 14 | BUILD_COMMIT := $(shell if [[ "$(GIT_DIRTY)" == "yes" ]]; then echo $(GIT_COMMIT)+dev; else echo $(GIT_COMMIT); fi) 15 | BUILD_COMMIT := $(shell echo $(BUILD_COMMIT) | cut -c1-12) 16 | BUILD_VERSION := $(shell if [[ "$(VERSION_DIRTY)" -gt "0" ]]; then echo "$(VERSION)-$(BUILD_COMMIT)"; else echo $(VERSION); fi) 17 | BUILD_VERSION := $(shell if [[ "$(VERSION_DIRTY)" -gt "0" ]] || [[ "$(GIT_DIRTY)" == "yes" ]]; then echo "$(BUILD_VERSION)-dev"; else echo $(BUILD_VERSION); fi) 18 | BUILD_VERSION := $(shell if [[ "$(GIT_BRANCH)" != "master" ]]; then echo $(GIT_BRANCH)-$(BUILD_VERSION); else echo $(BUILD_VERSION); fi) 19 | 20 | DOCKER_IMAGE := $(shell if [[ "$(DOCKER_REGISTRY)" ]]; then echo $(DOCKER_REGISTRY)/$(DOCKER_USER)/$(DOCKER_IMAGE); else echo $(DOCKER_USER)/$(DOCKER_IMAGE); fi) 21 | DOCKER_VERSION := $(shell echo "$(DOCKER_IMAGE):$(BUILD_VERSION)") 22 | DOCKER_LATEST := $(shell if [[ "$(VERSION_DIRTY)" -gt "0" ]] || [[ "$(GIT_DIRTY)" == "yes" ]]; then echo "$(DOCKER_IMAGE):dev"; else echo $(DOCKER_IMAGE):latest; fi) 23 | 24 | RANCHER_URL := $(shell renv $(RANCHER_ENV) | grep RANCHER_URL | cut -d= -f2) 25 | RANCHER_ACCESS_KEY := $(shell renv $(RANCHER_ENV) | grep RANCHER_ACCESS_KEY | cut -d= -f2) 26 | RANCHER_SECRET_KEY := $(shell renv $(RANCHER_ENV) | grep RANCHER_SECRET_KEY | cut -d= -f2) 27 | 28 | docker.build: 29 | @docker build -t $(DOCKER_VERSION) -t $(DOCKER_LATEST) . 30 | 31 | docker.push: docker.build 32 | @docker push $(DOCKER_VERSION) 33 | @docker push $(DOCKER_LATEST) 34 | 35 | info: 36 | @echo "git branch: $(GIT_BRANCH)" 37 | @echo "git commit: $(GIT_COMMIT)" 38 | @echo "git repo: $(GIT_REPO)" 39 | @echo "git dirty: $(GIT_DIRTY)" 40 | @echo "version: $(VERSION)" 41 | @echo "commits since: $(VERSION_DIRTY)" 42 | @echo "build commit: $(BUILD_COMMIT)" 43 | @echo "build version: $(BUILD_VERSION)" 44 | @echo "docker images: $(DOCKER_VERSION)" 45 | @echo " $(DOCKER_LATEST)" 46 | 47 | version: 48 | @echo $(BUILD_VERSION) | tr -d '\r' | tr -d '\n' | tr -d ' ' 49 | 50 | rancher.deploy: 51 | @rancher-compose \ 52 | --url $(RANCHER_URL) \ 53 | --access-key $(RANCHER_ACCESS_KEY) \ 54 | --secret-key $(RANCHER_SECRET_KEY) \ 55 | -p lb \ 56 | -f stack/docker-compose.yml \ 57 | -r stack/rancher-compose.yml \ 58 | up --force-upgrade -d 59 | -------------------------------------------------------------------------------- /lb-reload: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | SPLIT_TOKEN=${SPLIT_TOKEN:-__split__} 4 | 5 | entries=$(cat /etc/lb/entries | grep . | sort | uniq) 6 | 7 | # Compile backend data into entries.yml 8 | domains= 9 | frontends= 10 | current_backend= 11 | current_domain= 12 | current_ip= 13 | echo "backends:" > /etc/lb/entries.yml 14 | for entry in $entries; do 15 | stack=$(echo $entry | awk -F "$SPLIT_TOKEN" '{ print $1 }') 16 | service=$(echo $entry | awk -F "$SPLIT_TOKEN" '{ print $2 }') 17 | port=$(echo $entry | awk -F "$SPLIT_TOKEN" '{ print $3 }') 18 | if [[ "${stack}_${service}_${port}" != "$current_backend" ]]; then 19 | echo " -" >> /etc/lb/entries.yml 20 | echo " id: ${stack}_${service}_${port}" >> /etc/lb/entries.yml 21 | echo " stack: $stack" >> /etc/lb/entries.yml 22 | echo " service: $service" >> /etc/lb/entries.yml 23 | echo " port: $port" >> /etc/lb/entries.yml 24 | current_backend="${stack}_${service}_${port}" 25 | current_domain= 26 | current_ip= 27 | fi 28 | key=$(echo $entry | awk -F "$SPLIT_TOKEN" '{ print $4 }') 29 | value=$(echo $entry | awk -F "$SPLIT_TOKEN" '{ print $5 }') 30 | if [[ "$key" == "frontend" ]]; then 31 | frontends="$frontends $value" 32 | port=$(echo $value | awk -F '/' '{ print $1 }') 33 | mode=$(echo $value | awk -F '/' '{ print $2 }') 34 | case $port in 35 | ''|*[!0-9]*) is_number="false" ;; 36 | *) is_number="true" ;; 37 | esac 38 | echo " frontend: $value" >> /etc/lb/entries.yml 39 | if [[ "$is_number" == 'true' && "$mode" != "" ]]; then 40 | echo " mode: $mode" >> /etc/lb/entries.yml 41 | fi 42 | elif [[ "$key" == "domain" || "$key" == "domains" ]]; then 43 | if [[ "$current_domain" == "" ]]; then 44 | echo " domains:" >> /etc/lb/entries.yml 45 | fi 46 | current_domain=$value 47 | for domain in $(echo $value | tr ',' ' '); do 48 | proto="$(echo $domain | grep :// | sed -e's,^\(.*://\).*,\1,g')" 49 | url="$(echo ${domain##$proto})" 50 | proto="$(echo ${proto%://})" 51 | auth="$(echo $url | grep @ | cut -d@ -f1)" 52 | user="$(echo "$auth" | awk -F ':' '{ print $1 }')" 53 | pass="$(echo "$auth" | awk -F ':' '{ print $2 }')" 54 | host="$(echo ${url##$auth@} | cut -d/ -f1)" 55 | port="$(echo $host | sed -e 's,^.*:,:,g' -e 's,.*:\([0-9]*\).*,\1,g' -e 's,[^0-9],,g')" 56 | path="$(echo $url | grep / | cut -d/ -f2-)" 57 | echo " -" >> /etc/lb/entries.yml 58 | echo " id: $(echo $domain | sed -E 's/[[:punct:]]/_/g')" >> /etc/lb/entries.yml 59 | if [[ "$proto" != "" ]]; then echo " scheme: $proto" >> /etc/lb/entries.yml; fi 60 | if [[ "$user" != "" ]]; then echo " user: $user" >> /etc/lb/entries.yml; fi 61 | if [[ "$pass" != "" ]]; then echo " pass: $pass" >> /etc/lb/entries.yml; fi 62 | if [[ "$host" != "" ]]; then echo " host: $host" >> /etc/lb/entries.yml; fi 63 | if [[ "$path" != "" ]]; then echo " path: /$path" >> /etc/lb/entries.yml; fi 64 | 65 | domains="${current_backend} $(echo $domain | sed -E 's/[[:punct:]]/_/g') $(echo $host/$path | wc -c | tr -d ' ') 66 | $domains" 67 | done 68 | elif [[ "$key" == "container" ]]; then 69 | ip=$value 70 | state=$(echo $entry | awk -F "$SPLIT_TOKEN" '{ print $6 }') 71 | health=$(echo $entry | awk -F "$SPLIT_TOKEN" '{ print $7 }') 72 | 73 | if [[ "$health" == "healthy" && "$state" == "running" ]]; then 74 | health="true" 75 | else 76 | health="false" 77 | fi 78 | 79 | if [[ "$current_ip" == "" ]]; then 80 | echo " containers:" >> /etc/lb/entries.yml 81 | fi 82 | 83 | current_ip=$ip 84 | echo " -" >> /etc/lb/entries.yml 85 | echo " ip: $ip" >> /etc/lb/entries.yml 86 | echo " healthy: $health" >> /etc/lb/entries.yml 87 | fi 88 | done 89 | 90 | if [[ "$domains" == "" ]]; then 91 | echo "_sorted: []" >> /etc/lb/entries.yml 92 | else 93 | domains=$(echo "$domains" | sort -k3 -n -r) 94 | echo "_sorted:" >> /etc/lb/entries.yml 95 | IFS=$'\n'; for domain in $domains; do 96 | b=$(echo "$domain" | awk '{ print $1 }') 97 | d=$(echo "$domain" | awk '{ print $2 }') 98 | echo " - " >> /etc/lb/entries.yml 99 | echo " backend: '$b'" >> /etc/lb/entries.yml 100 | echo " domain: '$d'" >> /etc/lb/entries.yml 101 | done 102 | fi 103 | 104 | frontends=$(echo "$frontends" | tr ' ' '\n' | sort | uniq) 105 | echo "frontends:" >> /etc/lb/entries.yml 106 | for frontend in $frontends; do 107 | port=$(echo $frontend | awk -F '/' '{ print $1 }') 108 | mode=$(echo $frontend | awk -F '/' '{ print $2 }') 109 | case $port in 110 | ''|*[!0-9]*) is_number="false" ;; 111 | *) is_number="true" ;; 112 | esac 113 | if [[ "$is_number" == 'true' ]]; then 114 | echo " $frontend:" >> /etc/lb/entries.yml 115 | echo " name: $(echo $frontend | sed -E 's/[[:punct:]]/_/g')" >> /etc/lb/entries.yml 116 | echo " port: $port" >> /etc/lb/entries.yml 117 | echo " mode: ${mode:-http}" >> /etc/lb/entries.yml 118 | fi 119 | done 120 | 121 | # Download service metadata to metadata.yml and parse root domains 122 | curl -s -H 'Accept: application/json' http://rancher-metadata.rancher.internal/latest/self/service/metadata \ 123 | | jq '.domains = (.domains | map(capture("(?.*://)?(?.*)") 124 | | { scheme: .scheme[0:-3], host } 125 | | { scheme, host, id: (.scheme + "_" + .host | gsub("[.]"; "_")) }))' \ 126 | | json2yml > /etc/lb/metadata.yml 127 | 128 | # Merge entries and metadata to config.yml 129 | node -e "m = require('merge-yaml'); console.log(JSON.stringify(m(['/etc/lb/entries.yml','/etc/lb/metadata.yml'])));" | json2yml > /etc/lb/config.yml 130 | 131 | # Generate haproxy config file 132 | gotpl /etc/lb/haproxy.cfg.tpl /etc/lb/haproxy.cfg 133 | 134 | # Validate haproxy config and reload 135 | if haproxy -c -f /etc/lb/haproxy.cfg; then 136 | cp /etc/lb/haproxy.cfg /etc/haproxy/haproxy.cfg 137 | /usr/local/sbin/haproxy -D -p /var/run/haproxy.pid -f /etc/haproxy/haproxy.cfg -sf $(cat /var/run/haproxy.pid) 138 | else 139 | echo "invalid haproxy config" 140 | fi 141 | 142 | -------------------------------------------------------------------------------- /haproxy.cfg.tpl: -------------------------------------------------------------------------------- 1 | global 2 | log 127.0.0.1:32000 local2 3 | stats socket /var/run/haproxy.sock mode 777 level admin 4 | {{- range .global }} 5 | {{ . }} 6 | {{- end }} 7 | 8 | defaults 9 | timeout connect 5000 10 | timeout client 50000 11 | timeout server 50000 12 | errorfile 500 /etc/lb/500.http 13 | errorfile 502 /etc/lb/502.http 14 | errorfile 503 /etc/lb/503.http 15 | errorfile 504 /etc/lb/504.http 16 | {{- range .defaults }} 17 | {{ . }} 18 | {{- end }} 19 | 20 | {{ if .stats }} 21 | listen stats 22 | bind 0.0.0.0:{{ if .stats.port }}{{ .stats.port }}{{ else }}9090{{ end }} 23 | mode http 24 | stats uri {{ if .stats.path }}{{ .stats.path }}{{ else }}/{{ end }} 25 | stats admin if {{ if .stats.admin -}} TRUE {{ else -}} FALSE {{ end }} 26 | 27 | {{ end }} 28 | 29 | {{ range $backend := .backends -}} 30 | {{ range $domain := $backend.domains -}} 31 | {{ if $domain.user -}} 32 | {{ $bdid := printf "%s_%s" $backend.id $domain.id -}} 33 | userlist {{ $bdid }} 34 | user {{ $domain.user }} insecure-password {{ $domain.pass }} 35 | {{ end }} 36 | {{- end }} 37 | {{- end }} 38 | 39 | #### 40 | # START live-check 41 | #### 42 | 43 | {{ if .health }} 44 | frontend live_check 45 | bind *:{{ .health.port }} 46 | mode http 47 | monitor-uri {{ .health.path }} 48 | {{ end }} 49 | 50 | #### 51 | # END live-check 52 | #### 53 | 54 | {{ range $name, $frontend := .frontends -}} 55 | {{ if $frontend.name -}} frontend {{ $frontend.name }} 56 | {{- else -}} frontend {{ $name }} {{ end }} 57 | 58 | ############################ 59 | # START http-frontend 60 | ############################ 61 | 62 | {{ if eq $frontend.mode "http" -}} 63 | bind *:{{ .port }} {{ if $frontend.proxy -}} accept-proxy {{ end }} 64 | mode http 65 | 66 | #### 67 | # START frontend-options 68 | #### 69 | 70 | log 127.0.0.1:32000 local2 71 | option httplog 72 | {{- range $frontend.options }} 73 | {{ . }} 74 | {{- end }} 75 | 76 | #### 77 | # END frontend-options 78 | #### 79 | 80 | #### 81 | # START proxy-protocol 82 | #### 83 | 84 | acl xff_exists hdr_cnt(X-Forwarded-For) gt 0 85 | acl is_proxy_https dst_port 443 86 | http-request add-header X-Forwarded-For %[src] unless xff_exists 87 | http-request set-header X-Forwarded-Port %[dst_port] 88 | http-request add-header X-Forwarded-Proto https if is_proxy_https 89 | http-request add-header X-Forwarded-Proto http unless is_proxy_https 90 | 91 | #### 92 | # END proxy-protocol 93 | #### 94 | 95 | #### 96 | # START root-https 97 | # [Add HTTPS redirect for root domains if specified] 98 | #### 99 | 100 | {{ range $root := $.domains -}} 101 | {{ $did := $root.id -}} 102 | {{ if $root.scheme -}} 103 | acl acl_{{ $did }}_default hdr_end(host) -i {{ $root.host }} 104 | acl acl_{{ $did }}_default_https {{ if eq $root.scheme "https" -}} always_true {{ else -}} always_false {{- end }} 105 | redirect scheme https code 301 if !is_proxy_https acl_{{ $did }}_default acl_{{ $did }}_default_https 106 | {{ end }} 107 | {{- end }} 108 | 109 | #### 110 | # END root-https 111 | #### 112 | 113 | #################### 114 | # START backend-acls 115 | #################### 116 | 117 | {{ range $backend := $.backends -}} 118 | {{ if eq $backend.frontend $name -}} 119 | {{ $bid := $backend.id }} 120 | 121 | ############ 122 | # START domain-acls 123 | ############ 124 | 125 | {{ range $domain := $backend.domains -}} 126 | {{ $did := printf "%s_%s" $bid $domain.id -}} 127 | 128 | #### 129 | # START domain-https 130 | #### 131 | 132 | {{ if $domain.scheme -}} 133 | acl acl_{{ $did }}_https {{ if eq $domain.scheme "https" -}} always_true {{ else -}} always_false {{- end }} 134 | acl acl_{{ $did }}_https_host {{ if $domain.host }} hdr(host) -i {{ $domain.host }} {{ else }} always_true {{ end }} 135 | acl acl_{{ $did }}_https_path {{ if $domain.path }} path_beg -i {{ $domain.path }} {{ else }} always_true {{ end }} 136 | redirect scheme https code 301 if !is_proxy_https acl_{{ $did }}_https_host acl_{{ $did }}_https_path acl_{{ $did }}_https 137 | {{- end }} 138 | 139 | #### 140 | # END domain-https 141 | #### 142 | 143 | #### 144 | # START host-acls 145 | #### 146 | 147 | {{ if $domain.host -}} 148 | {{ if eq (index $domain.host 0) '*' -}} acl acl_{{ $did }}_domain hdr_end(host) -i {{ $domain.host }} 149 | {{ else -}} acl acl_{{ $did }}_domain hdr(host) -i {{ $domain.host }} {{- end }} 150 | {{ else }} acl acl_{{ $did }}_domain always_false {{- end }} 151 | 152 | #### 153 | # END host-acls 154 | #### 155 | 156 | #### 157 | # START path-acls 158 | #### 159 | 160 | {{ if $domain.path -}} acl acl_{{ $did }}_path path_beg -i {{ $domain.path }} 161 | {{ else }} acl acl_{{ $did }}_path always_true {{- end }} 162 | 163 | #### 164 | # END path-acls 165 | #### 166 | 167 | {{- end }} 168 | 169 | ############ 170 | # END domain-acls 171 | ############ 172 | 173 | #### 174 | # START default-acls 175 | #### 176 | 177 | {{ range $root := $.domains -}} 178 | 179 | {{ $did := $root.id -}} 180 | 181 | {{ if $backend.scope }} 182 | 183 | {{ if eq $backend.scope "environment" }} 184 | acl acl_{{ $bid }}_{{ $did }}_default hdr(host) -i {{ $backend.service }}.{{ $backend.stack }}.{{ $backend.environment }}.{{ $root.host }} 185 | {{ else if eq $backend.scope "service" }} 186 | acl acl_{{ $bid }}_{{ $did }}_default hdr(host) -i {{ $backend.service }}.{{ $root.host }} 187 | {{ else }} 188 | acl acl_{{ $bid }}_{{ $did }}_default hdr(host) -i {{ $backend.service }}.{{ $backend.stack }}.{{ $root.host }} 189 | {{ end }} 190 | 191 | {{ else }} 192 | 193 | {{ if eq $.scope "environment" }} 194 | acl acl_{{ $bid }}_{{ $did }}_default hdr(host) -i {{ $backend.service }}.{{ $backend.stack }}.{{ $backend.environment }}.{{ $root.host }} 195 | {{ else if eq $.scope "service" }} 196 | acl acl_{{ $bid }}_{{ $did }}_default hdr(host) -i {{ $backend.service }}.{{ $root.host }} 197 | {{ else }} 198 | acl acl_{{ $bid }}_{{ $did }}_default hdr(host) -i {{ $backend.service }}.{{ $backend.stack }}.{{ $root.host }} 199 | {{ end }} 200 | 201 | {{ end }} 202 | {{ end }} 203 | 204 | #### 205 | # END default-acls 206 | #### 207 | 208 | {{- end }} 209 | {{- end }} 210 | 211 | {{ range $._sorted }} 212 | {{ $sbid := .backend }} 213 | {{ $sdid := printf "%s_%s" $sbid .domain }} 214 | use_backend {{ $sbid }} if acl_{{ $sdid }}_domain acl_{{ $sdid }}_path 215 | {{- end }} 216 | 217 | 218 | {{ range $backend := $.backends -}} 219 | {{ if eq $backend.frontend $name -}} 220 | {{ $dbid := $backend.id }} 221 | 222 | {{ range $root := $.domains -}} 223 | {{ $ddid := $root.id -}} 224 | use_backend {{ $dbid }} if acl_{{ $dbid }}_{{ $ddid }}_default 225 | {{ end }} 226 | 227 | {{ end }} 228 | {{ end }} 229 | 230 | #################### 231 | # END backend-acls 232 | #################### 233 | 234 | default_backend fallback 235 | 236 | {{- end }} 237 | 238 | ############################ 239 | # END http-frontend 240 | ############################ 241 | 242 | ######## 243 | # START tcp-frontend 244 | ######## 245 | 246 | {{ if eq $frontend.mode "tcp" -}} 247 | bind *:{{ .port }} 248 | mode tcp 249 | 250 | #### 251 | # START frontend-options 252 | #### 253 | 254 | log 127.0.0.1:32000 local2 255 | option tcplog 256 | {{- range $frontend.options }} 257 | {{ . }} 258 | {{- end }} 259 | 260 | #### 261 | # END frontend-options 262 | #### 263 | 264 | {{ range $backend := $.backends -}} 265 | {{ if eq $backend.frontend $name -}} 266 | default_backend {{ $backend.id }} 267 | {{ end }} 268 | {{ end }} 269 | 270 | {{- end }} 271 | 272 | ######## 273 | # END tcp-frontend 274 | ######## 275 | 276 | {{ end }} 277 | 278 | {{ range $backend := .backends -}} 279 | backend {{ $backend.id }} 280 | {{ if $backend.mode -}} mode {{ $backend.mode }} {{ else }} mode http {{ end }} 281 | 282 | {{ range $domain := $backend.domains -}} 283 | {{ $bdid := printf "%s_%s" $backend.id $domain.id -}} 284 | 285 | {{ if $domain.host -}} 286 | {{ if eq (index $domain.host 0) '*' -}} acl acl_{{ $bdid }}_domain hdr_end(host) -i {{ $domain.host }} 287 | {{ else -}} acl acl_{{ $bdid }}_domain hdr(host) -i {{ $domain.host }} {{- end }} 288 | {{ else }} acl acl_{{ $bdid }}_domain always_false {{- end }} 289 | 290 | {{ if $domain.user -}} 291 | acl acl_{{ $bdid }}_auth http_auth({{ $bdid }}) 292 | http-request auth realm lb unless acl_{{ $bdid }}_auth 293 | {{- end }} 294 | 295 | {{ if $domain.path -}} acl acl_{{ $bdid }}_path path_beg -i {{ $domain.path }} 296 | {{ else }} acl acl_{{ $bdid }}_path always_true {{- end }} 297 | 298 | {{ if $domain.path -}} 299 | http-request set-header X-Path-Prefix {{ $domain.path }} 300 | reqirep ^([^\ ]*)\ {{ $domain.path }}/?(.*) \1\ /\2 if acl_{{ $bdid }}_path acl_{{ $bdid }}_domain 301 | {{- end }} 302 | 303 | {{ end }} 304 | 305 | {{- $port := .port }} 306 | {{- range $container := .containers }} 307 | server {{ $container.ip }} {{ $container.ip }}:{{ $port }} {{ if not $container.healthy }} disabled {{ end }} 308 | {{- end }} 309 | 310 | {{ end }} 311 | 312 | backend fallback 313 | mode http 314 | errorfile 503 /etc/lb/404.http 315 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rancher-lb 2 | Active HAProxy Load Balancer as a Rancher Service 3 | 4 | This is similar to the built-in load balancer offered by rancher, but is extended with support for the following features: 5 | 6 | - Add arbitrary custom HAProxy configuration via service metadata 7 | - Automatically add new services using docker labels 8 | - Default vhost domains for services using root domains and service/stack names 9 | - Easily expose HAProxy stats page 10 | - Optional redirect to a custom error and/or 404 page 11 | - Optional force redirect to https 12 | - Redirect haproxy logs to stdout by default (this is notoriously painful to configure in docker) 13 | - Expose a 'ping' port for maintaining host membership of external load-balancers via external health checks (think ELB) 14 | - Support for `accept-proxy` frontend option (again, useful for ELB) 15 | 16 | ## Getting started 17 | 18 | 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: 19 | 20 | ##### stack/docker-compose.yml 21 | 22 | ``` 23 | haproxy: 24 | ports: 25 | - 80:80/tcp 26 | labels: 27 | io.rancher.scheduler.global: 'true' 28 | lb.haproxy.9090.frontend: 80/http 29 | image: finboxio/rancher-lb 30 | 31 | ``` 32 | 33 | ##### stack/rancher-compose.yml 34 | 35 | ``` 36 | haproxy: 37 | health_check: 38 | port: 80 39 | interval: 2000 40 | initializing_timeout: 20000 41 | unhealthy_threshold: 3 42 | strategy: recreate 43 | response_timeout: 2000 44 | healthy_threshold: 2 45 | metadata: 46 | stats: 47 | port: 9090 48 | global: 49 | - maxconn 4096 50 | - debug 51 | defaults: 52 | - timeout connect 5000 53 | domains: 54 | - http://rancher 55 | scope: service 56 | ``` 57 | 58 | The docker-compose file is pretty straightforward. Run the image on all hosts and bind to port 80. 59 | 60 | 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.** 61 | 62 | 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. 63 | 64 | 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). 65 | 66 | 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. 67 | 68 | > **IMPORTANT** 69 | > 70 | > 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. 71 | 72 | ## Configuration 73 | 74 | #### tl;dr 75 | 76 | 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: 77 | 78 | ``` 79 | metadata: 80 | scope: {service|stack|environment} 81 | health: 82 | port: 83 | path: 84 | stats: 85 | port: 86 | path: 87 | admin: {true|false} 88 | global: 89 | - 90 | - 91 | - ... 92 | defaults: 93 | - 94 | - 95 | - ... 96 | domains: 97 | - 98 | - ... 99 | frontends: 100 | /{http|tcp}: 101 | proxy: {true|false} 102 | options: 103 | - 104 | - 105 | - ... 106 | /{http|tcp}: 107 | ... 108 | ... 109 | ``` 110 | 111 | You can also specify custom error/fallback pages with environment variables `ERROR_URL=` and `FALLBACK_URL=`. 112 | 113 | 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. 114 | 115 | `...frontend=/{http|tcp}` 116 | 117 | `...domain[s]={http|https}://,...` 118 | 119 | #### The details 120 | 121 | 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: 122 | 123 | ``` 124 | ...{frontend|domain|domains}= 125 | ``` 126 | 127 | `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. 128 | 129 | ##### Specifying a frontend 130 | 131 | In the sample configuration, we specified the frontend value for our stats page as `80/http`. In general, any value of the form `/{http|tcp}` will configure an http or tcp haproxy frontend listening on 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.** 132 | 133 | 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: 134 | 135 | ``` 136 | metadata: 137 | frontends: 138 | 80/http: 139 | proxy: true 140 | options: 141 | - acl not_found status 404 142 | - acl type_html res.hdr(Content-Type) -m sub text/html 143 | - http-request capture req.hdr(Host) len 64 144 | - http-response redirect location https://null.finbox.io/?href=http://%{+Q}[capture.req.hdr(0)]%HP code 303 if not_found type_html 145 | 146 | ``` 147 | 148 | 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. 149 | 150 | > **Protip** 151 | > 152 | > 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. 153 | 154 | 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. 155 | 156 | ##### Specifying domains 157 | 158 | Just like we specified our frontend, we can also specify one or more domains that should be routed to our service. 159 | 160 | ``` 161 | lb.haproxy.9090.domains=http://foo.finbox.io,https://bar.finbox.io 162 | ``` 163 | 164 | > If you only need one domain and have a ***perfectly rational*** aversion to unnecessary pluralization like me, you can use the form 165 | `lb.haproxy.9090.domain=http://foo.finbox.io` 166 | 167 | **It's your responsibility to ensure that the DNS records for these domains are properly configured to point to your load-balancer.** 168 | 169 | 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. 170 | 171 | Additionally, any `bar.finbox.io` request that is not sent over ssl will be redirected to `https://bar.finbox.io`. 172 | 173 | > **Note** 174 | > 175 | > 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. 176 | 177 | ##### Default domains 178 | 179 | 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 `.` semantics. 180 | 181 | `` can be defined in your service metadata, and it determines the prefix for default domains: 182 | 183 | Scope | Prefix 184 | --- | --- 185 | service | `` 186 | stack (default) | `.` 187 | environment | `..` 188 | 189 | 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. 190 | 191 | 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. 192 | 193 | Note that the https redirection semantics described above also apply to root domains if you specify them with `https://` protocol. 194 | 195 | > 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. 196 | 197 | ##### Custom error page 198 | 199 | Custom error pages can be configured via environment variables. If you run this load-balancer with `ERROR_URL=`, 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. 200 | 201 | You can also specify a `FALLBACK_URL=` 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. 202 | 203 | 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. 204 | 205 | > These urls should obviously be accessible independent of this load-balancer. We host ours statically on S3. 206 | 207 | ##### Liveness check 208 | 209 | 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. 210 | 211 | ``` 212 | metadata: 213 | health: 214 | port: 79 215 | path: / 216 | ``` 217 | 218 | > 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. 219 | 220 | ### Possible improvements 221 | 222 | `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. 223 | 224 | - [ ] LetsEncrypt support 225 | - [ ] Local SSL termination 226 | - [ ] Routing based on uri paths as well as hostnames (partially implemented, totally untested) 227 | - [ ] Use [janeczku/rancher-template](https://github.com/janeczku/rancher-template) to simplify the config templates 228 | --------------------------------------------------------------------------------