Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Solving anything in VCL

Solving anything in VCL

Presented at Fastly Altitude 2016

Andrew Betts

July 21, 2016
Tweet

More Decks by Andrew Betts

Other Decks in Technology

Transcript

  1. Who is this guy? 1. Helped build the original HTML5

    web app for the FT 2. Created our Origami component system 3. Ran FT Labs for 3 years 4. Now working with Nikkei to rebuild nikkei.com 5. Also W3C Technical Architecture Group 6. Live in Tokyo, Japan 2 Pic of me.
  2. Nikkei 1. Largest business newspaper in Japan 2. Globally better

    known for the Nikkei 225 stock index 3. Around 3 million readers
  3. Benefits of edge code 5 1. Smarter routing 2. Faster

    authentication 3. Bandwidth management 4. Higher cache hit ratio
  4. The VCL way 1. Request and response bodies are opaque

    2. Everything happens in metadata 3. Very restricted: No loops or variables 4. Extensible: some useful Fastly extensions include geo-ip and crypto 5. Incredibly powerful when used creatively 7
  5. SOA Routing Send requests to multiple microservice backends This is

    great if... • You have a microservice architecture • Many backends, one domain • You add/remove services regularly 1
  6. SOA Routing in VCL 9 Front page Article page Timeline

    Content API Choose a backend based on a path match of the request URL /article/123
  7. SOA Routing in VCL 10 [ { name, paths, host,

    useSsl, }, … ] {{#each backends}} backend {{name}} { .port = "{{p}}"; .host = "{{h}}"; } {{/each}} let vclContent = vclTemplate(data); fs.writeFileSync( vclFilePath, vclContent, 'UTF-8' ); services.json Defines all the backends and paths that they control. routing.vcl.handlebars VCL template with Handlebars placeholders for backends & routing build.js Task script to merge service data into VCL template
  8. SOA Routing: key tools and techniques • Choose a backend:

    set req.backend = {{backendName}}; • Match a route pattern: if (req.url ~ "{{pattern}}") • Remember to set a Host header: set req.http.Host = "{{backendhost}}"; • Upload to Fastly using FT Fastly tools ◦ https://github.com/Financial-Times/fastly-tools 11
  9. service-registry.json 12 [ { "name": "front-page", "paths": [ "/(?qs)", "/.resources/front/(**)(?qs)"

    ], "hosts": [ "my-backend.ap-northeast-1.elasticbeanstalk.com" ] }, { "name": "article-page", ... } ] Common regex patterns simplified into shortcuts
  10. routing.vcl.handlebars 13 {{#each backends}} backend {{name}} { .port = "{{port}}";

    .host = "{{host}}"; .ssl = {{use_ssl}}; .probe = { .request = "GET / HTTP/1.1" "Host: {{host}}" "Connection: close"; } } {{/each}} sub vcl_recv { {{#each routes}} if (req.url ~ "{{pattern}}") { set req.backend = {{backend}}; {{#if target}} set req.url = regsub(req.url, "{{pattern}}", "{{target}}"); {{/if}} {{!-- Fastly doesn't support the host_header property in backend definitions --}} set req.http.Host = "{{backendhost}}"; } {{/each}} return(lookup); }
  11. build.js 14 const vclTemplate = handlebars.compile(fs.readFileSync('routing.vcl.handlebars'), 'UTF-8')); const services =

    require('services.json'); // ... transform `services` into `viewData` let vclContent = vclTemplate(viewData); fs.writeFileSync(vclFilePath, vclContent, 'UTF-8');
  12. UA Targeting Return user-agent specific responses without destroying your cache

    hit ratio This is great if... • You have a response that is tailored to different device types • There are a virtually infinite number of User-Agent values 2
  13. UA Targeting 17 /normalizeUA /polyfill.js?ua=ie/11 /polyfill.js Add the normalised User-

    Agent to the URL and restart the original request Add a Vary: User-Agent header to the response before sending it back to the browser We call this a preflight request
  14. UA targeting: key tools and techniques • Remember something using

    request headers: set req.http.tmpOrigURL = req.url; • Change the URL of the backend request: set req.url = "/api/normalizeUA?ua=" req.http.User-Agent; • Reconstruct original URL adding a backend response header: set req.url = req.http.tmpOrigURL "?ua=" resp.http.NormUA; • Restart to send the request back to vcl_recv: restart; 18
  15. ua-targeting.vcl 19 sub vcl_recv { if (req.url ~ "^/v2/polyfill\." &&

    req. url !~ "[\?\&]ua=") { set req.http.X-Orig-URL = req.url; set req.url = "/v2/normalizeUa?ua=" urlencode(req.http.User-Agent); } } sub vcl_deliver { if (req.url ~ "^/v\d/normalizeUa" && resp.status == 200 && req.http.X-Orig-URL) { set req.http.Fastly-force-Shield = "1"; if (req.http.X-Orig-URL ~ "\?") { set req.url = req.http.X-Orig-URL "&ua=" resp.http.UA; } else { set req.url = req.http.X-Orig-URL "?ua=" resp.http.UA; } restart; } else if (req.url ~ "^/v\d/polyfill\..*[\?\&]ua=" && req. http.X-Orig-URL && req.http.X-Orig-URL !~ "[\?\&]ua=") { add resp.http.Vary = "User-Agent"; } return(deliver); }
  16. Authentication Implement integration with your federated identity system entirely in

    VCL This is great if... • You have a federated login system using a protocol like OAuth • You want to annotate requests with a simple verified authentication state 3
  17. New magic circa 2016 22 app.get('/', (req, res) => {

    res.end(req.get('Nikkei-UserID')); });
  18. Authentication 23 /article/123 Decode+verify auth cookie! Nikkei-UserID: andrew.betts Nikkei-UserRank: premium

    Vary: Nikkei-UserRank Article Cookie: Auth=a139fm24... Cache-control: private
  19. Authentication: key tools and techniques • Get a cookie by

    name: req.http.Cookie:MySiteAuth • Base64 normalisation: digest.base64url_decode(), digest.base64_decode • Extract the parts of a JSON Web Token (JWT): regsub({{cookie}}, "(^[^\.]+)\.[^\.]+\.[^\.]+$", "\1"); • Check JWT signature: digest.hmac_sha256_base64() • Set trusted headers for backend use: req.http.Nikkei-UserID = regsub({{jwt}}, {{pattern}}, "\1"); 24
  20. authentication.vcl 25 if (req.http.Cookie:NikkeiAuth) { set req.http.tmpHeader = regsub(req.http.Cookie:NikkeiAuth, "(^[^\.]+)\.[^\.]+\.[^\.]+$",

    "\1"); set req.http.tmpPayload = regsub(req.http.Cookie:NikkeiAuth, "^[^\.]+\.([^\.]+)\.[^\.]+$", "\1"); set req.http.tmpRequestSig = digest.base64url_decode( regsub(req.http.Cookie:NikkeiAuth, "^[^\.]+\.[^\.]+\.([^\.]+)$", "\1") ); set req.http.tmpCorrectSig = digest.base64_decode( digest.hmac_sha256_base64("{{jwt_secret}}", req.http.tmpHeader "." req.http.tmpPayload) ); if (req.http.tmpRequestSig != req.http.tmpCorrectSig) { error 754 "/login; NikkeiAuth=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT"; } ... continues ...
  21. authentication.vcl (cont) 26 set req.http.tmpPayload = digest.base64_decode(req.http.tmpPayload); set req.http.Nikkei-UserID =

    regsub(req.http.tmpPayload, {"^.*?"sub"\s*:\s*"(\w+)".*?$"}, "\1"); set req.http.Nikkei-Rank = regsub(req.http.tmpPayload, {"^.*?"ds_rank"\s*:\s*"(\w+)".*?$"}, "\1"); unset req.http.base64_header; unset req.http.base64_payload; unset req.http.signature; unset req.http.valid_signature; unset req.http.payload; } else { set req.http.Nikkei-UserID = "anonymous"; set req.http.Nikkei-Rank = "anonymous"; }
  22. Feature flags Dark deployments and easy A/B testing without reducing

    front end perf or cache efficiency This is great if... • You want to serve different versions of your site to different users • Test new features internally on prod before releasing them to the world 4
  23. 28

  24. Feature flags parts 30 • A flags registry - a

    JSON file will be fine ◦ Include all possible values of each flag and what percentage of the audience it applies to ◦ Publish it statically - S3 is good for that • A flag toggler tool ◦ Reads the JSON, renders a table, writes an override cookie with chosen values • An API ◦ Reads the JSON, responds to requests by calculating a user's position number on a 0-100 line and matches them with appropriate flag values • VCL ◦ Merges flag data into requests
  25. Feature flags 31 Flags API Article Merge the flags response

    with the override cookie, set as HTTP header, restart original request... Decode+verify auth cookie! /article/123 Cookie: Flgs-Override= Foo=10; /api/flags?userid=6453 Flgs: highlights=true; Foo=42; Flgs: highlights=true; Foo=42; Foo=10 Vary: Flgs
  26. ExpressJS flags middleware 32 app.get('/', (req, res) => { if

    (req.flags.has('highlights')) { // Enable highlights feature } }); HTTP/1.1 200 OK Vary: Nikkei-Flags ... Middleware provides convenient interface to flags header Invoking the middleware on a request automatically applies a Vary header to the response
  27. Dynamic backends Override backend rules at runtime without updating your

    VCL This is great if... • You have a bug you can't reproduce without the request going through the CDN • You want to test a local dev version of a service with live integrations 5
  28. Dynamic backends 34 Developer laptop Dynamic backend proxy (node-http-proxy) Check

    forwarded IP is whitelisted or auth header is also present GET /article/123 Backend-Override: article -> fc57848a.ngrok.io Detect override header, if path would normally be routed to article, change it to override proxy instead. ngrok fc57848a .ngrok.io Normal production backends
  29. Dynamic backends: key tools and techniques • Extract backend to

    override: set req.http.tmpORBackend = regsub(req.http.Backend-Override, "\s*\-\>.*$", ""); • Check whether current backend matches if (req.http.tmpORBackend == req.http.tmpCurrentBackend) { • Use node-http-proxy for the proxy app ◦ Remember res.setHeader('Vary', 'Backend-Override'); ◦ I use {xfwd: false, changeOrigin: true, hostRewrite: true} 35
  30. Debug headers Collect request lifecycle information in a single HTTP

    response header This is great if... • You find it hard to understand what path the request is taking through your VCL • You have restarts in your VCL and need to see all the individual backend requests, not just the last one 6
  31. Debug journey 40 vcl_recv { set req.http.tmpLog = if (req.restarts

    == 0, "", req.http.tmpLog ";"); # ... routing ... set req.http.tmpLog = req.http.tmpLog " {{backend}}:" req.url; } vcl_fetch { set req.http.tmpLog = req.http.tmpLog " fetch"; ... } vcl_hit { set req.http.tmpLog = req.http.tmpLog " hit"; ... } vcl_miss { set req.http.tmpLog = req.http.tmpLog " miss"; ... } vcl_pass { set req.http.tmpLog = req.http.tmpLog " pass"; ... } vcl_deliver { set resp.http.CDN-Process-Log = req.http.tmpLog; }
  32. Debug journey 41 CDN-Process-Log: apigw:/flags/v1/rnikkei/allocate? output=diff&segid=foo&rank=X HIT (hits=2 ttl=1.204/5.000 age=4

    swr=300.000 sie=604800.000); rnikkei_front_0:/ MISS (hits=0 ttl=1.000/1.000 age=0 swr=300.000 sie=86400.000)
  33. RUM++ Resource Timing API + data Fastly exposes in VCL.

    And no backend. This is great if... • You want to track down hotspots of slow response times • You'd like to understand how successfully end users are being matched to their nearest PoPs 7
  34. Resource timing on front end 43 var rec = window.performance.getEntriesByType("resource")

    .find(rec => rec.name.indexOf('[URL]') !== -1) ; (new Image()).src = '/sendBeacon'+ '?dns='+(rec.domainLookupEnd-rec.domainLookupStart)+ '&connect='+(rec.connectEnd-rec.connectStart)+ '&req='+(rec.responseStart-rec.requestStart)+ '&resp='+(rec.responseEnd-rec.responseStart) ;
  35. Add CDN data in VCL & respond with synthetic 44

    sub vcl_recv { if (req.url ~ "^/sendBeacon") { error 700 "GIF"; } } sub vcl_error { if (obj.status == 700) { set obj.status = 200; set obj.response = "OK"; set obj.http.Content-Type = "image/gif"; synthetic digest.base64_decode("R0lGODlhAQABAIAAAA..."); return (deliver); } }
  36. RUM++ 45 /sendBeacon?foo=42&... No backend request! 200 OK Write logs

    in 1 minute batches to Amazon S3 Use an 'error' response to return a 200!
  37. Beyond ASCII Use these encoding tips to embed non-ASCII content

    in your VCL file. This is great if... • Your users don't speak English, but you can only write ASCII in VCL files 8
  38. 49

  39. Quick conversion 50 "string" .split('') .map( char => char.codePointAt(0) <

    128 ? char : "&#"+char.codePointAt(0)+";" ) .join('') ;
  40. Varnishlog to the rescue A way to submit a varnish

    transaction ID to the API, and get all varnishlog events relating to that transaction, including related (backend) transactions 55 > fastly log 1467852934 17 SessionOpen c 66.249.72.22 47013 :80 17 ReqStart c 66.249.72.22 47013 1467852934 17 RxRequest c GET 17 RxURL c /articles/123 17 RxProtocol c HTTP/1.1 17 RxHeader c Host: www.example.com ...