1
0
Fork 0

Merge pull request #1 from byjg/2.0

Version 2.0
This commit is contained in:
Joao Gilberto Magalhães 2019-07-24 09:23:36 -05:00 committed by GitHub
commit d01cbd5a3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 483 additions and 198 deletions

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
.idea
*~
venv
venv
.docker_data
__pycache__

View file

@ -1,10 +1,18 @@
FROM haproxy:1.9-alpine
FROM haproxy:2.0.2-alpine
RUN apk add --no-cache bash python3 py-yaml
WORKDIR /scripts
COPY entrypoint.* /
COPY conf.d /etc/haproxy/conf.d
COPY assets/errors-custom/* /etc/haproxy/errors-custom/
RUN apk add --no-cache bash python3 py-yaml supervisor docker
ENTRYPOINT [ "/entrypoint.sh" ]
CMD ["haproxy", "-f", "/etc/haproxy/haproxy.cfg"]
COPY requirements.txt /scripts
RUN pip3 install --upgrade pip \
&& pip install -r requirements.txt
COPY swarm.* /scripts/
COPY static.* /scripts/
COPY templates /scripts/templates/
COPY easymapping /scripts/easymapping/
COPY assets /
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf" ]

209
README.md
View file

@ -1,17 +1,141 @@
# Easy HAProxy
This Docker image will create dynamically the `haproxy.cfg` based on very simple Yaml.
This Docker image will create dynamically the `haproxy.cfg` based on the labels defined in docker containers or from
a simple Yaml instead docker
# Features
- Enable or disable Stats on port 1936 with custom password
- Simple mapping host => host:port
- Simple redirect host => host:port
- Discover and setup haproxy from Docker Tag
- Discover and setup haproxy redirect from Docker Tag
- Setup HAProxy CFG from a Yaml file.
# Basic Usage
Create a yaml file in your machine called `easyconfig.cfg` and put the contents:
The Easy HAProxy will create the `haproxy.cfg` automatically based on the containers or from a YAML provided.
The basic command line to run is:
```bash
docker run -d \
--name easy-haproxy-container \
-v /var/run/docker.sock:/var/run/docker.sock \
-e DISCOVER="swarm|docker|static" \
# + Environment Variables \
# + ports mapped to the host \
byjg/easy-haproxy
```
The mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels;
The environment variables will setup the HAProxy.
| Environment Variable | Description |
|----------------------|-------------------------------------------------------------------------------|
| DISCOVER | How `haproxy.cfg` will be created: `static`, `docker` or `swarm` |
| HAPROXY_USERNAME | (Optional) The HAProxy username to the statistics. Default: `admin` |
| HAPROXY_PASSWORD | The HAProxy password to the statistics. If not set disable stats. |
| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. Default: `1936` |
| HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. Default: false |
The environment variable `DISCOVER` will define where is located your containers (see below more details):
- docker
- swarm
- static
# DISCOVER: `docker`
This method will use a regular docker installation to discover the containers and configure the HAProxy.
The only requirement is that containers and easy-haproxy must be in the same docker network.
The discover will occur every minute.
e.g.:
```bash
docker create networkd easyhaproxy
docker run --network easyhaproxy byjg/easyhaproxy
docker run --network easyhaproxy myimage
```
# DISCOVER: `swarm`
This method requires a functional Docker Swarm Cluster. The system will search for the labels in all containers on all
swarm nodes.
The discover will occur every minute.
Important: easyhaproxy needs to be in the same network of the containers or otherwise will not access.
## Tags to be attached in the Docker Container
| Tag | Description |
|---------------------------------------------|---------------------------------------------------------------------------------------------------------|
| com.byjg.easyhaproxy.definitions | A Comma delimited list with the definitions. Each name requires the definition of the parameters below. |
| com.byjg.easyhaproxy.port.[definition] | (Optional) What is the port that the HAProxy will listen to. (Defaults to 80) |
| com.byjg.easyhaproxy.localport.[definition] | (Optional) What is the port that the container is listening. (Defaults to 80) |
| com.byjg.easyhaproxy.host.[definition] | What is the host that the HAProxy will listen to. |
| com.byjg.easyhaproxy.redirect.[definition] | (Optional) Host redirects from connections in the port defined above. |
| com.byjg.easyhaproxy.sslcert.[definition] | (Optional) Cert PEM Base64 encoded. |
Note: if you are deploying a stack set labels at the `deploy` level:
```yaml
services:
foo:
deploy:
labels:
com.byjg.easyhaproxy.definitions: "http,https"
...
```
### Single Definition:
```bash
docker run \
-l com.byjg.easyhaproxy.definitions=http \
-l com.byjg.easyhaproxy.port.http=80\
-l com.byjg.easyhaproxy.host.http=byjg.com.br \
....
```
### Multiples Definitions on the same container:
```bash
docker run \
-l com.byjg.easyhaproxy.definitions=express,admin \
-l com.byjg.easyhaproxy.port.express=80 \
-l com.byjg.easyhaproxy.localport.express=3000 \
-l com.byjg.easyhaproxy.host.express=express.byjg.com.br \
-l com.byjg.easyhaproxy.port.admin=80 \
-l com.byjg.easyhaproxy.localport.admin=3001 \
-l com.byjg.easyhaproxy.host.admin=admin.byjg.com.br \
.... \
some/myimage
```
### Redirect Example:
```bash
docker run \
-l com.byjg.easyhaproxy.redirect.<defintion>=www.byjg.com.br--http://byjg.com.br,byjg.com--http://byjg.com.br
```
# DISCOVER: `static`
This method expects a YAML file to setup the `haproxy.cfg`
Create a YAML file and map to `/etc/haproxy/easyconfig.yml`
```yaml
stats:
@ -30,7 +154,7 @@ easymapping:
www.host1.com.br: http://host1.com.br
- port: 443
ssl_cert: /etc/easyconfig/mycert.pem
ssl_cert: BASE64_PEM_CERTIFICATE
hosts:
host1.com.br: container:80
@ -39,24 +163,15 @@ easymapping:
host3.com.br: domain:8181
```
Then run (remember to enable the proper ports):
Running:
```bash
docker run \
-v /path/to/local:/etc/easyconfig \
-p 80:80 \
-p 8080:8080 \
-p 1936:1936 \
--name easy-haproxy-instance \
-d byjg/easy-haproxy
docker run -v /my/config.yml:/etc/haproxy/easyconfig.yml .... byjg/easyhaproxy
```
# Mapping custom .cfg files
Just create a folder and put files with the extension .cfg. and map the volume to the container.
This will concatenate your config into the main haproxy.cfg
Map a folder containing valid HAProxy `.cfg` files to `/etc/haproxy/conf.d`. It will be concatenated to your HAProxy CFG.
```bash
docker run \
@ -65,48 +180,41 @@ docker run \
-d byjg/easy-haproxy
```
Check if your config is ok:
```bash
docker run \
/* other parameters */
-v /your/local/conf.d:/etc/haproxy/conf.d \
-byjg/easy-haproxy -c -f /etc/haproxy/haproxy.cfg
```
# Docker Compose
```yaml
version: "3.4"
services:
front:
image: byjg/easy-haproxy
volume:
- /path/to/local:/etc/easyconfig
ports:
- 80:80
- 8080:8080
- 1936:1936
```
# Handling SSL
HaProxy can handle SSL for you. in this case add the parameter pointing to file containing
the pem of certificates and key in only one file:
You can attach a valid SSL certificate to the request.
```
- port: 443
ssl_cert: /etc/easyconfig/mycert.pem
hosts:
host1.com.br: container:80
1. First Create a single PEM file including CA.
```bash
cat example.com.crt example.com.key > single.pem
cat single.pem
-----BEGIN CERTIFICATE-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZheHqmBnEJP+
U9r1gxYWKLzdqrMrcxtQN6M1hIH9n0peuJeIrybdcV7sMbStMXI=
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL
3j4wz8/I5fdsk090j4s5KA==
-----END PRIVATE KEY-----
```
Important: Different certificates need to be handled in different entries.
2. Convert it to BASE64 in a single line:
```bash
cat single.pem | base64 -w0
```
3. Use this string to define the label `com.byjg.easyhaproxy.sslcert.[definition]`
# Setting Custom Errors
Map the volume : `/etc/haproxy/errors-custom/` and put a file named `ERROR_NUMBER.http` where ERROR_NUMBER
is the http error code (e.g. 503.http)
If enabled, map the volume : `/etc/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http`
where ERROR_NUMBER is the http error code (e.g. 503.http)
# Build
@ -114,3 +222,4 @@ is the http error code (e.g. 503.http)
docker build -t byjg/easy-haproxy .
```

9
assets/etc/crontabs/root Normal file
View file

@ -0,0 +1,9 @@
# do daily/weekly/monthly maintenance
# min hour day month weekday command
*/15 * * * * run-parts /etc/periodic/15min
0 * * * * run-parts /etc/periodic/hourly
0 2 * * * run-parts /etc/periodic/daily
0 3 * * 6 run-parts /etc/periodic/weekly
0 5 1 * * run-parts /etc/periodic/monthly
* * * * * /scripts/haproxy-reload.sh

View file

@ -0,0 +1,3 @@
# Certs Folder
Docker Easy HAProxy will save the SSL Certs here.

View file

@ -0,0 +1,45 @@
[unix_http_server]
file=/dev/shm/supervisor.sock ; (the path to the socket file)
[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
user=root ;
; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///dev/shm/supervisor.sock ; use a unix:// URL for a unix socket
[program:haproxy]
command = /scripts/haproxy.sh
autostart=true
autorestart=false
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:crond]
command=/usr/sbin/crond -f
autostart=true
autorestart=false
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[eventlistener:exit_on_any_fatal]
command=/scripts/exit-event-listener.py
events=PROCESS_STATE_FATAL,PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED

View file

@ -0,0 +1,18 @@
#!/usr/bin/env python
import os
import signal
from supervisor import childutils
def main():
while True:
headers, payload = childutils.listener.wait()
childutils.listener.ok()
events = ['PROCESS_STATE_FATAL', 'PROCESS_STATE_EXITED', 'PROCESS_STATE_STOPPED']
if not (headers['eventname'] in events):
continue
os.kill(os.getppid(), signal.SIGTERM)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,49 @@
#!/usr/bin/env bash
cd /scripts
RELOAD="true"
if [[ "$DISCOVER" == "static" ]]; then
CONTROL_FILE="/etc/haproxy/haproxy.cfg"
touch ${CONTROL_FILE}
cp ${CONTROL_FILE} ${CONTROL_FILE}.old
python3 static.py /etc/haproxy/easyconfig.yml > ${CONTROL_FILE}
else
CONTROL_FILE="/tmp/.docker_data"
touch ${CONTROL_FILE}
mv ${CONTROL_FILE} ${CONTROL_FILE}.old
touch ${CONTROL_FILE}
if [[ "$DISCOVER" == "docker" ]]; then
CONTAINERS=$(docker ps -q)
LABEL_PATH=".Config.Labels"
else
CONTAINERS=$(docker node ps $(docker node ls -q) --format "{{ .Name }}" --filter desired-state=running | cut -d. -f1 | sort | uniq)
LABEL_PATH=".Spec.Labels"
fi
for container in ${CONTAINERS}; do
docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE}
done
if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then
RELOAD="false"
else
python3 swarm.py > /etc/haproxy/haproxy.cfg
fi
fi
if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then
RELOAD="false"
fi
if [[ ! -z "$1" ]]; then
echo "Initial configuration"
RELOAD="false"
fi
if [[ "$RELOAD" == "true" ]]; then
echo "Reloading..."
/usr/local/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -x /var/run/haproxy.sock -sf $(cat /run/haproxy.pid) &
fi

9
assets/scripts/haproxy.sh Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
source /scripts/haproxy-reload.sh initial
/usr/local/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock
while true; do
sleep 60
done

12
easymapping/__init__.py Normal file
View file

@ -0,0 +1,12 @@
from jinja2 import Environment, FileSystemLoader
class HaproxyConfigGenerator:
def __init__(self, mapping):
self.mapping = mapping
def generate(self):
file_loader = FileSystemLoader('templates')
env = Environment(loader=file_loader)
template = env.get_template('haproxy.cfg.j2')
return template.render(data=self.mapping)

View file

@ -1,109 +0,0 @@
import yaml
import sys
if len(sys.argv) != 2:
print("You need to pass the easyconfig.cfg path")
exit(1)
def defaults(custom):
result = """
defaults
log global
timeout connect 3s
timeout client 10s
timeout server 10m
"""
if custom:
result += """
errorfile 400 /etc/haproxy/errors-custom/400.http
errorfile 403 /etc/haproxy/errors-custom/403.http
errorfile 408 /etc/haproxy/errors-custom/408.http
errorfile 500 /etc/haproxy/errors-custom/500.http
errorfile 502 /etc/haproxy/errors-custom/502.http
errorfile 503 /etc/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http
"""
result += """
global
log /dev/log local0
maxconn 2000
tune.ssl.default-dh-param 2048
"""
return result
def stats(map):
return """
frontend stats
bind *:{2}
mode http
stats enable
stats hide-version
stats realm Haproxy\ Statistics
stats uri /
stats auth {0}:{1}
# acl is_proxystats hdr(host) -i some.host.com
# default_backend srv_stats
# use_backend srv_stats if is_proxystats
default_backend srv_stats
backend srv_stats
mode http
server Local 127.0.0.1:{2}
""".format(map["username"], map["password"], map["port"] if "port" in map else 1936)
def easymapping(o, salt):
port = o["port"]
ssl = " ssl crt " + o["ssl_cert"] if "ssl_cert" in o else ""
hosts = o["hosts"] if "hosts" in o else dict()
redir = o["redirect"] if "redirect" in o else dict()
result = """
frontend http_in_{0}_{1}
bind *:{0} {2}
mode http
""".format(port, salt, ssl)
for k in redir:
result += " redirect prefix " + redir[k] + " code 301 if { hdr(host) -i " + k + " }\n"
result += "\n"
for k in hosts:
host = k.replace(".", "_") + "_{0}_{1}".format(port, salt)
result += " acl is_rule_{0}_1 hdr(host) -i {1}\n".format(host, k)
result += " acl is_rule_{0}_2 hdr(host) -i {1}:{2}\n".format(host, k, port)
result += " use_backend srv_{0} if is_rule_{0}_1 OR is_rule_{0}_2\n\n".format(host)
for k in hosts:
host = k.replace(".", "_") + "_{0}_{1}".format(port, salt)
result += """
backend srv_{0}
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]""".format(host) + """
http-request add-header X-Forwarded-Proto https if { ssl_fc }""" + """
server srv {0} check weight 1
""".format(hosts[k])
return result
with open(sys.argv[1], 'r') as content_file:
parsed = yaml.load(content_file.read())
n = 0
print(defaults(parsed["customerrors"] if "customerrors" in parsed else False))
if "stats" in parsed:
print(stats(parsed["stats"]))
if "easymapping" in parsed:
for k in parsed["easymapping"]:
n = n + 1
print(easymapping(k, n))

View file

@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -e
echo " ______ _ _ _____ "
echo "| ____| | | | | /\ | __ \ "
echo "| |__ __ _ ___ _ _ | |__| | / \ | |__) | __ _____ ___ _ "
echo "| __| / _\` / __| | | | | __ | / /\ \ | ___/ '__/ _ \ \/ / | | |"
echo "| |___| (_| \__ \ |_| | | | | |/ ____ \| | | | | (_) > <| |_| |"
echo "|______\__,_|___/\__, | |_| |_/_/ \_\_| |_| \___/_/\_\\__, |"
echo " __/ | __/ |"
echo " |___/ |___/ "
echo ""
HAPROXY_CFG="/etc/haproxy/haproxy.cfg"
EASYCONFIG_CFG="/etc/easyconfig/easyconfig.cfg"
if [ ! -f "$EASYCONFIG_CFG" ]
then
echo "File '$EASYCONFIG_CFG' does not exist"
exit 1
fi
python3 /entrypoint.py "$EASYCONFIG_CFG" > $HAPROXY_CFG
/sbin/syslogd -O /proc/1/fd/1
/docker-entrypoint.sh "$@"

View file

@ -1 +1,3 @@
pyyaml
pyyaml
docker
jinja2

16
static.py Normal file
View file

@ -0,0 +1,16 @@
import yaml
import sys
from easymapping import HaproxyConfigGenerator
if len(sys.argv) != 2:
print("You need to pass the easyconfig.yml path")
exit(1)
with open(sys.argv[1], 'r') as content_file:
parsed = yaml.load(content_file.read(), Loader=yaml.FullLoader)
cfg = HaproxyConfigGenerator(parsed)
print(cfg.generate())
exit(0)

74
swarm.py Normal file
View file

@ -0,0 +1,74 @@
import os
import json
import time
import base64
import hashlib
from easymapping import HaproxyConfigGenerator
# path = os.path.dirname(os.path.realpath(__file__))
with open("/tmp/.docker_data", 'r') as content_file:
lineList = content_file.readlines()
result = {
"easymapping": [],
"customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False
}
easymapping = dict()
if os.getenv("HAPROXY_PASSWORD"):
result["stats"] = {
"username": os.getenv("HAPROXY_USERNAME") if os.getenv("HAPROXY_USERNAME") else "admin",
"password": os.getenv("HAPROXY_PASSWORD"),
"port": os.getenv("HAPROXY_STATS_PORT") if os.getenv("HAPROXY_STATS_PORT") else "1936",
}
for line in lineList:
line = line.strip()
i = line.find("=")
container = line[:i]
jsonStr = line[i+1:]
d = json.loads(jsonStr)
if "com.byjg.easyhaproxy.definitions" in d.keys():
definitions = d["com.byjg.easyhaproxy.definitions"].split(",")
for definition in definitions:
if "com.byjg.easyhaproxy.host." + definition not in d:
continue
port = d["com.byjg.easyhaproxy.port." + definition] if "com.byjg.easyhaproxy.port." + definition in d else "80"
hash = hashlib.md5(d["com.byjg.easyhaproxy.sslcert." + definition].encode('utf-8')).hexdigest() if "com.byjg.easyhaproxy.sslcert." + definition in d else ""
key = port+hash
if key not in easymapping:
easymapping[key] = {
"port": port,
"hosts": dict(),
"redirect": dict(),
# "ssl_cert": ""
}
easymapping[key]["hosts"][d["com.byjg.easyhaproxy.host." + definition]] = container + ":" + (d["com.byjg.easyhaproxy.localport." + definition] if "com.byjg.easyhaproxy.localport." + definition in d else "80")
if "com.byjg.easyhaproxy.sslcert." + definition in d:
filename = '/etc/haproxy/certs/' + d["com.byjg.easyhaproxy.host." + definition] + "." + str(time.time()) + ".pem"
easymapping[key]["ssl_cert"] = filename
with open(filename, 'wb') as file:
file.write(base64.b64decode(d["com.byjg.easyhaproxy.sslcert." + definition]))
if "com.byjg.easyhaproxy.redirect." + definition in d:
redirect = d["com.byjg.easyhaproxy.redirect." + definition] if "com.byjg.easyhaproxy.redirect." + definition in d else ""
for r in redirect.split(","):
r_parts = r.split("--")
easymapping[key]["redirect"][r_parts[0]] = r_parts[1]
result["easymapping"] = easymapping.values()
cfg = HaproxyConfigGenerator(result)
print(cfg.generate())
# print(jsonStr)

68
templates/haproxy.cfg.j2 Normal file
View file

@ -0,0 +1,68 @@
defaults
log global
timeout connect 3s
timeout client 10s
timeout server 10m
{% if data["customerrors"] %}
errorfile 400 /etc/haproxy/errors-custom/400.http
errorfile 403 /etc/haproxy/errors-custom/403.http
errorfile 408 /etc/haproxy/errors-custom/408.http
errorfile 500 /etc/haproxy/errors-custom/500.http
errorfile 502 /etc/haproxy/errors-custom/502.http
errorfile 503 /etc/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http
{% endif %}
global
log /dev/log local0
maxconn 2000
tune.ssl.default-dh-param 2048
{% if "stats" in data %}
frontend stats
bind *:{{ data["stats"]["port"] | default(1936) }}
mode http
stats enable
stats hide-version
stats realm Haproxy\ Statistics
stats uri /
stats auth {{ data["stats"]["username"] }}:{{ data["stats"]["password"] }}
# acl is_proxystats hdr(host) -i some.host.com
# default_backend srv_stats
# use_backend srv_stats if is_proxystats
default_backend srv_stats
backend srv_stats
mode http
server Local 127.0.0.1:{{ data["stats"]["port"] | default(1936) }}
{% endif %}
{% for o in data["easymapping"] %}
{% set salt = loop.index %}
frontend http_in_{{ o["port"] }}_{{ salt }}
bind *:{{ o["port"] }} {{ " ssl crt " + o["ssl_cert"] if "ssl_cert" in o else "" }}
mode http
{% for k in o["redirect"] -%}
redirect prefix {{ o["redirect"][k] }} code 301 if { hdr(host) -i {{ k }} }
{% endfor -%}
{% for k in o["hosts"] %}
{% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %}
acl is_rule_{{ host }}_1 hdr(host) -i {{ k }}
acl is_rule_{{ host }}_2 hdr(host) -i {{ k }}:{{ o["port"] }}
use_backend srv_{{ host }} if is_rule_{{ host }}_1 OR is_rule_{{ host }}_2
{% endfor %}
{% for k in o["hosts"] %}
{% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %}
backend srv_{{ host }}
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv {{ o["hosts"][k] }} check weight 1
{% endfor %}
{% endfor %}