Skip to content

Commit

Permalink
ESPP-195 HTTP cache config
Browse files Browse the repository at this point in the history
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
FriendsOfSymfony/FOSHttpCache#495
api-platform/core#1856
api-platform/api-platform#1947
  • Loading branch information
rbayet committed Feb 14, 2022
1 parent 1510abe commit 340cfdb
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 21 deletions.
3 changes: 3 additions & 0 deletions api/.env
Expand Up @@ -40,3 +40,6 @@ ELASTICSEARCH_HOST=elasticsearch
ELASTICSEARCH_PORT=9200
ELASTICSEARCH_SCHEME=http
ELASTICSEARCH_URL=http://elasticsearch:9200/

# Varnish URL for purging HTTP cache
VARNISH_URL=varnish
9 changes: 9 additions & 0 deletions api/config/packages/api_platform.yaml
Expand Up @@ -8,8 +8,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
77 changes: 59 additions & 18 deletions api/docker/caddy/Caddyfile
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
95 changes: 95 additions & 0 deletions 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;
}
109 changes: 109 additions & 0 deletions 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;
}
18 changes: 18 additions & 0 deletions 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 "$@"
32 changes: 29 additions & 3 deletions docker-compose.yml
Expand Up @@ -29,8 +29,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:
Expand All @@ -41,7 +41,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
Expand All @@ -62,7 +65,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:
Expand Down

0 comments on commit 340cfdb

Please sign in to comment.