From e602135cc8a348e7a80fd858e6656f404e390aa2 Mon Sep 17 00:00:00 2001 From: Richard BAYET Date: Wed, 9 Feb 2022 10:58:57 +0100 Subject: [PATCH] ESPP-195 HTTP cache config Includes now defunct API Platform Varnish configuration. From https://github.com/api-platform/api-platform/blob/v2.5.7/api/docker/varnish/conf/default.vcl Please note that is a BAN based purge (which is kinda meh ?) Please see also https://github.com/FriendsOfSymfony/FOSHttpCache/issues/495 https://github.com/api-platform/core/issues/1856 https://github.com/api-platform/api-platform/issues/1947 --- api/.env | 3 + api/config/packages/api_platform.yaml | 9 ++ api/docker/caddy/Caddyfile | 77 ++++++++++--- api/docker/varnish/api.vcl | 95 +++++++++++++++ api/docker/varnish/default.vcl | 109 ++++++++++++++++++ .../varnish/docker-varnish-entrypoint.sh | 18 +++ docker-compose.yml | 32 ++++- 7 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 api/docker/varnish/api.vcl create mode 100644 api/docker/varnish/default.vcl create mode 100644 api/docker/varnish/docker-varnish-entrypoint.sh diff --git a/api/.env b/api/.env index 9d2720eeb..98a1f14de 100644 --- a/api/.env +++ b/api/.env @@ -40,3 +40,6 @@ ELASTICSEARCH_HOST=127.0.0.1 ELASTICSEARCH_PORT=9200 ELASTICSEARCH_SCHEME=http ELASTICSEARCH_URL=http://127.0.0.1:9200/ + +# Varnish URL for purging HTTP cache +VARNISH_URL=varnish diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index da8c665e8..93157c3c7 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -9,8 +9,17 @@ api_platform: versions: [3] # Mercure integration, remove if unwanted # mercure: ~ + http_cache: + invalidation: + enabled: true + varnish_urls: ['%env(VARNISH_URL)%'] + max_age: 3600 + shared_max_age: 7200 + public: true # Good cache defaults for REST APIs defaults: stateless: true cache_headers: vary: ['Content-Type', 'Authorization', 'Origin'] + max_age: 0 + shared_max_age: 3600 diff --git a/api/docker/caddy/Caddyfile b/api/docker/caddy/Caddyfile index c1f57b792..3951e3e03 100644 --- a/api/docker/caddy/Caddyfile +++ b/api/docker/caddy/Caddyfile @@ -3,29 +3,26 @@ {$DEBUG} # HTTP/3 support servers { - protocol { - experimental_http3 - } + protocol { + experimental_http3 + } } } -{$SERVER_NAME} - -log +(snippet) { + # Matches requests for HTML documents, for static files and for Next.js files, + # except for known API paths and paths with extensions handled by API Platform + @pwa expression `( + {header.Accept}.matches("\\btext/html\\b") + && !{path}.matches("(?i)(?:^/docs|^/graphql|^/bundles/|^/_profiler|^/_wdt|\\.(?:json|html$|csv$|ya?ml$|xml$))") + ) + || {path} == "/favicon.ico" + || {path} == "/manifest.json" + || {path} == "/robots.txt" + || {path}.startsWith("/_next") + || {path}.startsWith("/sitemap")` -# Matches requests for HTML documents, for static files and for Next.js files, -# except for known API paths and paths with extensions handled by API Platform -@pwa expression `( - {header.Accept}.matches("\\btext/html\\b") - && !{path}.matches("(?i)(?:^/docs|^/graphql|^/bundles/|^/_profiler|^/_wdt|\\.(?:json|html$|csv$|ya?ml$|xml$))") - ) - || {path} == "/favicon.ico" - || {path} == "/manifest.json" - || {path} == "/robots.txt" - || {path}.startsWith("/_next") - || {path}.startsWith("/sitemap")` -route { root * /srv/api/public # mercure { # # Transport to use (default to Bolt) @@ -57,3 +54,47 @@ route { encode zstd gzip file_server } + +# {$SERVER_NAME} + +# log + +# @health_check { +# host {$SERVER_NAME} +# path /health-check +# } + +{$SERVER_NAME} { + log + + # named matchers + @do_varnish_pass { + header !X-Caddy-Forwarded + # method GET + # protocol https + not { + header Upgrade websocket + } + } + + # do the cache pass + reverse_proxy @do_varnish_pass http://{$VARNISH_UPSTREAM} { + # request_header +X-Caddy-Forwarded 1 + header_up X-Caddy-Forwarded 1 + # header_up Host {http.reverse_proxy.upstream.hostport} + } + + import snippet +} + +http://{$SERVER_NAME}:8080 { + log + + @do_redirect_https { + header !X-Caddy-Forwarded + } + + redir @do_redirect_https https://{$SERVER_NAME}{uri} permanent + + import snippet +} diff --git a/api/docker/varnish/api.vcl b/api/docker/varnish/api.vcl new file mode 100644 index 000000000..a26a2ab55 --- /dev/null +++ b/api/docker/varnish/api.vcl @@ -0,0 +1,95 @@ +vcl 4.0; + +import std; + +backend default { + .host = "api"; + .port = "80"; + # Health check + #.probe = { + # .url = "/"; + # .timeout = 5s; + # .interval = 10s; + # .window = 5; + # .threshold = 3; + #} +} + +# Hosts allowed to send BAN requests +acl invalidators { + "localhost"; + "php"; + # local Kubernetes network + "10.0.0.0"/8; + "172.16.0.0"/12; + "192.168.0.0"/16; +} + +sub vcl_recv { + if (req.restarts > 0) { + set req.hash_always_miss = true; + } + + # Remove the "Forwarded" HTTP header if exists (security) + unset req.http.forwarded; + + # To allow API Platform to ban by cache tags + if (req.method == "BAN") { + if (client.ip !~ invalidators) { + return (synth(405, "Not allowed")); + } + + if (req.http.ApiPlatform-Ban-Regex) { + ban("obj.http.Cache-Tags ~ " + req.http.ApiPlatform-Ban-Regex); + + return (synth(200, "Ban added")); + } + + return (synth(400, "ApiPlatform-Ban-Regex HTTP header must be set.")); + } + + # For health checks + if (req.method == "GET" && req.url == "/healthz") { + return (synth(200, "OK")); + } +} + +sub vcl_hit { + if (obj.ttl >= 0s) { + # A pure unadulterated hit, deliver it + return (deliver); + } + + if (std.healthy(req.backend_hint)) { + # The backend is healthy + # Fetch the object from the backend + return (restart); + } + + # No fresh object and the backend is not healthy + if (obj.ttl + obj.grace > 0s) { + # Deliver graced object + # Automatically triggers a background fetch + return (deliver); + } + + # No valid object to deliver + # No healthy backend to handle request + # Return error + return (synth(503, "API is down")); +} + +sub vcl_deliver { + # Don't send cache tags related headers to the client + unset resp.http.url; + # Comment the following line to send the "Cache-Tags" header to the client (e.g. to use CloudFlare cache tags) + unset resp.http.Cache-Tags; +} + +sub vcl_backend_response { + # Ban lurker friendly header + set beresp.http.url = bereq.url; + + # Add a grace in case the backend is down + set beresp.grace = 1h; +} diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl new file mode 100644 index 000000000..28d1a9ee0 --- /dev/null +++ b/api/docker/varnish/default.vcl @@ -0,0 +1,109 @@ +vcl 4.0; +import std; + +backend default { + .host = "caddy"; + .port = "8080"; + .max_connections = 300; + .first_byte_timeout = 300s; # How long to wait before we receive a first byte from our backend? + .connect_timeout = 5s; # How long to wait for a backend connection? + .between_bytes_timeout = 2s; # How long to wait between bytes received from our backend? + + # Health check +# .probe = { +# .request = +# "HEAD /health-check HTTP/1.1" +# "Host: caddy-probe.local" +# "Connection: close" +# "User-Agent: Varnish Health Probe"; +# .timeout = 5s; +# .interval = 5s; +# .window = 4; +# .threshold = 2; +# } +} + +# Hosts allowed to send BAN requests +acl invalidators { + "localhost"; + "php"; + # local Kubernetes network + # "10.0.0.0"/8; + # "172.16.0.0"/12; + # "192.168.0.0"/16; +} + +sub vcl_recv { + if (req.restarts > 0) { + set req.hash_always_miss = true; + } + + # Remove the "Forwarded" HTTP header if exists (security) + unset req.http.forwarded; + + # To allow API Platform to ban by cache tags + if (req.method == "BAN") { + if (client.ip !~ invalidators) { + return (synth(405, "Not allowed")); + } + + if (req.http.ApiPlatform-Ban-Regex) { + ban("obj.http.Cache-Tags ~ " + req.http.ApiPlatform-Ban-Regex); + + return (synth(200, "Ban added")); + } + + return (synth(400, "ApiPlatform-Ban-Regex HTTP header must be set.")); + } + + if (req.method != "GET" && req.method != "HEAD") { + return (pass); + } + + # For health checks + # if (req.method == "GET" && req.url == "/healthz") { + # return (synth(200, "OK")); + # } + + return (hash); +} + +sub vcl_hit { + if (obj.ttl >= 0s) { + # A pure unadulterated hit, deliver it + return (deliver); + } + + if (std.healthy(req.backend_hint)) { + # The backend is healthy + # Fetch the object from the backend + return (restart); + } + + # No fresh object and the backend is not healthy + if (obj.ttl + obj.grace > 0s) { + # Deliver graced object + # Automatically triggers a background fetch + return (deliver); + } + + # No valid object to deliver + # No healthy backend to handle request + # Return error + return (synth(503, "API is down")); +} + +sub vcl_deliver { + # Don't send cache tags related headers to the client + unset resp.http.url; + # Comment the following line to send the "Cache-Tags" header to the client (e.g. to use CloudFlare cache tags) + # unset resp.http.Cache-Tags; +} + +sub vcl_backend_response { + # Ban lurker friendly header + set beresp.http.url = bereq.url; + + # Add a grace in case the backend is down + set beresp.grace = 1h; +} diff --git a/api/docker/varnish/docker-varnish-entrypoint.sh b/api/docker/varnish/docker-varnish-entrypoint.sh new file mode 100644 index 000000000..54efa98c1 --- /dev/null +++ b/api/docker/varnish/docker-varnish-entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# this will check if the first argument is a flag +# but only works if all arguments require a hyphenated flag +# -v; -SL; -f arg; etc will work, but not arg1 arg2 +if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then + set -- varnishd \ + -F \ + -f /etc/varnish/default.vcl \ + -a http=:80,HTTP \ + -a proxy=:8443,PROXY \ + -p feature=+http2 \ + -s malloc,$VARNISH_SIZE \ + "$@" +fi + +exec "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 3c52e626e..e78f32c0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,8 +28,8 @@ services: context: ./pwa target: api_platform_pwa_prod environment: - API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT: http://caddy - NEXT_PUBLIC_ENTRYPOINT: http://caddy + API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT: https://caddy + NEXT_PUBLIC_ENTRYPOINT: https://caddy caddy: build: @@ -40,7 +40,10 @@ services: - pwa environment: PWA_UPSTREAM: pwa:3000 - SERVER_NAME: ${SERVER_NAME:-localhost, caddy:80} + # SERVER_NAME: ${SERVER_NAME:-localhost, caddy:80} + SERVER_NAME: ${SERVER_NAME:-localhost} + # VARNISH_UPSTREAM: varnish:80 + VARNISH_UPSTREAM: varnish:80 # MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeMe!} # MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeMe!} restart: unless-stopped @@ -61,7 +64,30 @@ services: - target: 443 published: ${HTTP3_PORT:-443} protocol: udp + - target: 8080 + published: 8080 + protocol: tcp + varnish: + # image: varnish:fresh-alpine + image: varnish:fresh + depends_on: + - caddy + restart: unless-stopped + volumes: + - './api/docker/varnish/default.vcl:/etc/varnish/default.vcl:ro' + environment: + - VARNISH_SIZE=512M + # - SERVERNAME=${SERVER_NAME:-localhost, caddy:80} + # If http2 not already supported out of the box + # command: "-p default_keep=300 -p feature=+http2" + command: "-p default_keep=300" + # restart: unless-stopped + # ports: + # - target: 80 + # published: 8080 + # protocol: tcp + database: image: postgres:${POSTGRES_VERSION:-13}-alpine environment: