Post

Traefik & Varnish

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.

The Traefik Maintainer Team

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.

Wikipédia

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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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}).*", "\1");
		}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 !

Cet article est sous licence CC BY 4.0 par l'auteur.

© 2022- Olivier. Certains droits réservés.

Propulsé par τζ avec le thème Χ