Introduction
L’idée de ce tutoriel est de vous permettre de mettre en place un load balancer (Traefik) et de lui associer un cache (Varnish) pour accélérer un peu le tout.
Quelques notions
Traefik est un routeur Edge open-source qui fait de la publication de vos services une expérience amusante et facile. Il reçoit les demandes au nom de votre système et trouve les composants qui sont responsables de leur traitement.Ce qui distingue Traefik, outre ses nombreuses fonctionnalités, est qu’il découvre automatiquement la bonne configuration pour vos services. La magie opère lorsque Traefik inspecte votre infrastructure, où il trouve les informations pertinentes et découvre quel service sert quelle requête.Traefik est nativement compatible avec les principales technologies de cluster, telles que Kubernetes, Docker, Docker Swarm, AWS, Mesos, Marathon, et bien d’autres encore. (Il fonctionne même pour les logiciels hérités fonctionnant sur du métal nu).Avec Traefik, il n’est pas nécessaire de maintenir et de synchroniser un fichier de configuration séparé : tout se passe automatiquement, en temps réel (pas de redémarrage, pas d’interruption de connexion). Avec Traefik, vous passez votre temps à développer et déployer de nouvelles fonctionnalités sur votre système, et non à configurer et maintenir son état de fonctionnement.En développant Traefik, notre objectif principal est de le rendre simple à utiliser, et nous sommes sûrs que vous allez l’apprécier.
Varnish est un serveur de cache HTTP apparu en 2006 et distribué sous licence BSD.Déployé en tant que proxy inverse entre les serveurs d’applications et les clients, il permet de décharger les premiers en mettant en cache leurs données, selon des règles définies par l’administrateur système et les développeurs du site, pour servir plus rapidement les requêtes, tout en allégeant la charge des serveurs.
Traefik, pour faire simple, va servir de gare de triage automatique entre tout ce qui est extérieur à votre serveur, et ce qui est à l’intérieur.
Varnish lui va garder en mémoire ce qui a à l’intérieur de votre serveur et au lieu d’aller à chaque fois solliciter les services (ce qui va prendre du temps et nécessiter parfois beaucoup de ressources) il va redonner directement à l’appelant ce qu’il a conservé en mémoire.
En combinant ces deux outils, vous aurez une gare de triage efficace :)
Prérequis
Comme pour la majorité de mes tutoriels, je considère que vous avez un nom de domaine valide et que vous utilisez Cloudflare.
Fichiers requis
docker-compose.yml
[Fichier]
version: "3.1"
# echo $(htpasswd -nb "[username]" "[password]") | sed -e s/\\$/\\$\\$/g
#
# [username]
# [password]
#
# username:hashed_password
services:
traefik-varnish:
container_name: traefik-varnish
hostname: traefik-varnish
restart: always
security_opt:
- no-new-privileges:true
image: varnish:latest
stdin_open: true
tty: true
networks:
- proxy
ports:
- "1080:80"
environment:
TZ: "Europe/Paris"
PUID: 1000
PGID: 1000
command: ["-a :1080,PROXY", "-s file,/cache,1G"]
volumes:
- /mnt/varnish:/cache
- /opt/docker/traefik/work/varnish.vcl:/etc/varnish/default.vcl
tmpfs:
- /tmp:exec
traefik:
container_name: traefik
hostname: traefik
restart: always
security_opt:
- no-new-privileges:true
image: traefik:latest
stdin_open: true
tty: true
networks:
- proxy
ports:
- "80:80"
- "443:443/tcp"
- "443:443/udp"
- "6082:6082"
depends_on:
- traefik-varnish
links:
- traefik-varnish
environment:
TZ: "Europe/Paris"
PUID: 1000
PGID: 1000
#CF_API_EMAIL: [cloudflare API email account]
#CF_DNS_API_TOKEN: "[cloudflare dns token]"
#CF_API_KEY: "[cloudflare api key]"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /opt/docker/traefik/work/acme.json:/acme.json
- /opt/docker/traefik/work/traefik.yml:/traefik.yml
- /opt/docker/traefik/work/config.yml:/config.yml
labels:
- "traefik.enable=true"
networks:
proxy:
external: true
traefik.yml
[Fichier]
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
dashboard: true
debug: false
insecure: false
entryPoints:
http:
address: ':80'
http:
redirections:
entryPoint:
to: http3
scheme: https
http2:
maxConcurrentStreams: 256
http3:
address: ':443'
metrics:
address: ':6082'
http2:
maxConcurrentStreams: 256
streaming:
address: ":1704/udp"
serversTransport:
insecureSkipVerify: true
providers:
docker:
endpoint: 'unix:///var/run/docker.sock'
exposedByDefault: false
network: proxy
file:
filename: /config.yml
certificatesResolvers:
cloudflare:
acme:
email: [your email]
storage: acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- '1.1.1.1:53'
- '1.0.0.1:53'
metrics:
prometheus:
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
entryPoint: metrics
buckets:
- 0.1
- 0.3
- 1.2
- 5.0
respondingTimeouts:
readTimeout: '120s'
writeTimeout: '10s'
idleTimeout: '360s'
forwardingTimeouts:
dialTimeout: '10s'
responseHeaderTimeout: '60s'
experimental:
http3: true
#accessLog: true
#log:
# level: "DEBUG"
config.yml
[Fichier]
enabled: true
http:
routers:
#traefik-http:
# service: api@internal
#traefik-https:
# service: api@internal
traefik:
entryPoints:
- http
rule: Host(`traefik.home`)
middlewares:
- traefik-https-redirect
service: api@internal
traefik-secure:
entryPoints:
- http3
rule: Host(`traefik.home`)
middlewares:
- traefik-auth
tls: {}
service: api@internal
subdomain:
entryPoints:
- http3
rule: Host(`[subdomain.domain.com]`)
middlewares:
- default
tls: {}
service: subdomain
services:
subdomain:
loadBalancer:
servers:
- url: 'http://[varnish ip]:[varnish port]'
passHostHeader: true
middlewares:
traefik-https-redirect:
redirectScheme:
scheme: https
traefik-auth:
basicAuth:[user:hash password]"
sslheader:
headers:
customRequestHeaders:
X-Forwarded-Proto: https
wss:
headers:
customRequestHeaders:
X-Forwarded-Proto: https
https-redirectscheme:
redirectScheme:
scheme: https
permanent: true
default-headers:
headers:
frameDeny: true
browserXssFilter: false
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
customFrameOptionsValue: SAMEORIGIN
security-headers:
headers:
customResponseHeaders:
X-Robots-Tag: 'none,noarchive,nosnippet,notranslate,noimageindex'
server: ''
X-Forwarded-Proto: https
sslProxyHeaders:
X-Forwarded-Proto: https
referrerPolicy: same-origin
hostsProxyHeaders:
- X-Forwarded-Host
customRequestHeaders:
X-Forwarded-Proto: https
contentTypeNosniff: false
contentsecuritypolicy: "default-src 'self' data: wss: *.cloudflare.com *.gstatic.com *.github.com; img-src 'self' https: data: blob:; script-src 'self' 'unsafe-eval' 'unsafe-inline' blob: *.cloudflare.com *.jsdelivr.net *.jquery.com *.github.com; style-src 'self' 'unsafe-inline' https:; connect-src 'self' wss:"
hsts-headers:
headers:
frameDeny: true
sslRedirect: true
browserXssFilter: false
contentTypeNosniff: false
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
forceStsHeader: true
referrerPolicy: same-origin
customResponseHeaders:
permissions-Policy: vibrate=(self), geolocation=(self), midi=(self), notifications=(self), push=(self), microphone=(), $
X-Permitted-Cross-Domain-Policies: none
cors-all:
headers:
customRequestHeaders:
Access-Control-Allow-Origin: origin-list-or-null
Sec-Fetch-Site: cross-site
X-Forwarded-Proto: https
Access-Control-Allow-Headers: '*, Authorization'
customResponseHeaders:
Access-Control-Allow-Origin: '*'
Sec-Fetch-Site: cross-site
X-Forwarded-Proto: https
Access-Control-Allow-Headers: '*, Authorization'
accessControlAllowMethods:
- OPTIONS
- POST
- GET
- PUT
- DELETE
- PATCH
accessControlAllowHeaders:
- '*, Authorization'
accessControlExposeHeaders:
- '*, Authorization'
accessControlMaxAge: 100
addVaryHeader: true
accessControlAllowCredentials: true
accessControlAllowOriginList:
- '*'
default-whitelist:
ipWhiteList:
sourceRange:
- '10.0.0.0/8'
- '192.168.0.0/16'
- '172.16.0.0/12'
- '173.245.48.0/20'
- '103.21.244.0/22'
- '103.22.200.0/22'
- '103.31.4.0/22'
- '141.101.64.0/18'
- '108.162.192.0/18'
- '190.93.240.0/20'
- '188.114.96.0/20'
- '197.234.240.0/22'
- '198.41.128.0/17'
- '162.158.0.0/15'
- '104.16.0.0/13'
- '104.24.0.0/14'
- '172.64.0.0/13'
- '131.0.72.0/22'
- '2400:cb00::/32'
- '2606:4700::/32'
- '2803:f800::/32'
- '2405:b500::/32'
- '2405:8100::/32'
- '2a06:98c0::/29'
- '2c0f:f248::/32'
compress-all:
compress:
excludedContentTypes:
- text/event-stream
minResponseBodyBytes: 1024
inflight-req:
inFlightReq:
amount: 128
rate-limit:
rateLimit:
average: 128
period: 1m
burst: 256
retry-attempts:
retry:
attempts: 4
initialInterval: 100ms
default:
chain:
middlewares:
- default-whitelist
- https-redirectscheme
- default-headers
- security-headers
- hsts-headers
- inflight-req
- rate-limit
- retry-attempts
- compress-all
varnish.vcl
[Fichier]
vcl 4.1;
import std;
#
# backend definitions
#
# no default backend
backend default none;
backend subdomain {
.host = "[service ip]";
.port = "[service port]";
#
.connect_timeout = 6s;
.first_byte_timeout = 6s;
.between_bytes_timeout = 2s;
.max_connections = 128;
}
#
# configurations
#
# call for every start request
sub vcl_recv {
if(req.http.host ~ "subdomain.domain.com") {
set req.backend_hint = subdomain;
}
# set initial grace period usage status
set req.http.grace = "none";
if (req.restarts == 0) {
if (req.http.X-Forwarded-For && !req.http.X-Real-IP) {
set req.http.X-Real-IP = regsub\(req.http.X-Forwarded-For, \".\*\b\(?!10|127|172)\(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).\*\", \"\");
}else{
set req.http.X-Forwarded-For = req.http.X-Real-IP;
}
}
if (req.restarts > 0) {
set req.hash_always_miss = true;
}
if (req.http.upgrade ~ "(?i)websocket") {
return (pipe);
}
# non-RFC2616 or CONNECT which is weird.
if (req.method != "GET" &&
req.method != "HEAD" &&
req.method != "PUT" &&
req.method != "POST" &&
req.method != "TRACE" &&
req.method != "OPTIONS" &&
req.method != "DELETE") {
return (pipe);
}
# we only deal with GET and HEAD by default
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
if \(req.url ~ \"^/api\(\$|/.\*)\" \|| req.url ~ \"^/admin\(\$|/.\*)\") {
return (pass);
}
return (hash);
}
sub vcl_backend_response {
set beresp.grace = 60m;
set beresp.ttl = 5s;
if (bereq.url ~ "^/static/") {
set beresp.ttl = 1d;
}
# keep the response in cache for 4 hours if the response has
# validating headers.
if \(beresp.http.ETag \|| beresp.http.Last-Modified) {
set beresp.keep = 4h;
}
if \(bereq.url ~ \"\.js\$\" \|| beresp.http.content-type ~ \"text\") {
set beresp.do_gzip = true;
}
# cache only successfully responses and 404s
if (beresp.status != 200 && beresp.status != 404) {
set beresp.ttl = 0s;
set beresp.uncacheable = true;
return (deliver);
} elsif (beresp.http.Cache-Control ~ "private") {
set beresp.uncacheable = true;
set beresp.ttl = 86400s;
return (deliver);
}
# validate if we need to cache it and prevent from setting cookie
if \(beresp.ttl > 0s && \(bereq.method == \"GET\" \|| bereq.method == \"HEAD\")) {
unset beresp.http.set-cookie;
}
# if page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass
if \(beresp.ttl <= 0s \||
beresp.http.Surrogate-control ~ \"no-store\" \||
(!beresp.http.Surrogate-Control &&
beresp.http.Cache-Control ~ \"no-cache|no-store\") \||
beresp.http.Vary == "*") {
# Mark as Hit-For-Pass for the next 2 minutes
set beresp.ttl = 120s;
set beresp.uncacheable = true;
}
return (deliver);
}
# called is page's hash found (= page already cached)
sub vcl_hit {
if (obj.ttl >= 0s) {
return (deliver);
}
if (std.healthy(req.backend_hint)) {
if (obj.ttl + 300s > 0s) {
# hit after TTL expiration, but within grace period
set req.http.grace = "normal (healthy server)";
return (deliver);
} else {
# hit after TTL and grace expiration
return (restart);
}
} else {
# server is not healthy, retrieve from cache
set req.http.grace = "unlimited (unhealthy server)";
return (deliver);
}
return (restart);
}
# called at every response end (cached or not)
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.V-Cache = "HIT";
}
else {
set resp.http.V-Cache = "MISS";
}
set resp.http.V-Cache-Hits = obj.hits;
return (deliver);
}
sub vcl_pipe {
if (req.http.upgrade) {
set bereq.http.upgrade = req.http.upgrade;
set bereq.http.connection = req.http.connection;
}
}
Mise en place
Je partirai du principe que vous utilisez Portainer pour gérer vos stack Docker.
Il vous faudra créer les répertoires suivants (ou adapter au besoin) :
- /mnt/varnish
- /opt/docker/traefik/work/
Et une fois ceci fait, vous copiez les fichiers présentés ci-dessus dans les chemins tels qu’indiqué dans le docker-compose.yml.
Lorsque vous aurez mis en place les fichiers et adapter, éventuellement, les chemins ou les noms, il vous faudra modifier les informations que j’ai mises entre crochets dans les différents fichiers.
Ces informations doivent refléter votre propre besoin. Lorsque c’est fait, vous créez une nouvelle stack sous Portainer et copier/coller le contenu du fichier docker-compose.yml dans l’éditeur et vous lancez sa création.
Précisions
Avec cette méthode, vous disposerez des fonctionnalités suivantes :
- Équilibrage distribué de charge
- Cache de contenu
- SSL (HTTPS) automatique
- Certificats SSL génériques (wildcard certificates)
- Compatibilité HTTP/3
- Protection CORS
- Limitation des pics de charge
NB :
Le middleware Traefik cors-all est à utiliser avec parcimonie, car il est relativement laxiste…
Conclusion
Ce tutoriel vous donne une bonne base pour commencer dans le monde de Traefik et de Varnish.
Dans le cadre d’un HomeLab, il permet de mettre des services web à disposition, tout en disposant d’une bonne sécurité de base (ssl/http, gestion de la charge).
A vous de l’adapter en fonction de vos besoin !