Solving anything in VCL

Solving anything in VCL

Presented at Fastly Altitude 2016

Fd1af6cc88403788ae1e5710871bbf62?s=128

Andrew Betts

July 21, 2016
Tweet

Transcript

  1. Solving anything in VCL Andrew Betts, Financial Times

  2. 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.
  3. Nikkei 1. Largest business newspaper in Japan 2. Globally better

    known for the Nikkei 225 stock index 3. Around 3 million readers
  4. Coding on the edge 4

  5. Benefits of edge code 5 1. Smarter routing 2. Faster

    authentication 3. Bandwidth management 4. Higher cache hit ratio
  6. Edge side includes 6 <esi:include src="http://example.com/1.html" alt="http://bak.example.com/2.html" onerror="continue"/> index.html my-news.html

    Cache-control: max-age=86400 Cache-control: private Server
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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); }
  14. 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');
  15. 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
  16. 16 Polyfill screenshot

  17. 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
  18. 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
  19. 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); }
  20. 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
  21. Magic circa 2001 21 <?php echo $_SERVER['PHP_AUTH_USER']; ?> http://intranet/my/example/app

  22. New magic circa 2016 22 app.get('/', (req, res) => {

    res.end(req.get('Nikkei-UserID')); });
  23. 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
  24. 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
  25. 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 ...
  26. 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"; }
  27. 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
  28. 28

  29. 29 Now you see it...

  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 37 The VCL flow

  38. 38 The VCL flow

  39. 39 The VCL flow

  40. 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; }
  41. 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)
  42. 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
  43. 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) ;
  44. 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); } }
  45. 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!
  46. Crunch the data 46

  47. 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
  48. Everyone does UTF-8 now, right? 48 synthetic {"Responsive Nikkeiアルファプログラムのメンバーの皆様、アル ファバージョンのサイトにアクセスできない場合、rnfeedback@nex.nikkei.co.jp

    までその旨連絡ください。"};
  49. 49

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

    128 ? char : "&#"+char.codePointAt(0)+";" ) .join('') ;
  51. "Fixed" 51 synthetic {"Responsive Nikkei&#12450;&#12523;&#12501; &#12449;&#12503;&#12525;&#12464;&#12521;&#12512;&#12398;&#12513; &#12531;&#12496;&#12540;&#12398;&#30342;&#27096;&#12289;&#12450; &#12523;&#12501;&#12449;&#12496;&#12540;&#12472;&#12519;&#12531; &#12398;&#12469;&#12452;&#12488;&#12395;&#12450;&#12463;&#12475; &#12473;&#12391;&#12365;&#12394;&#12356;&#22580;&#21512;&#12289;

    rnfeedback@nex.nikkei.co.jp &#12414;&#12391;&#12381;&#12398; &#26088;&#36899;&#32097;&#12367;&#12384;&#12373;&#12356; &#12290;"};
  52. "Fixed" 52 synthetic digest.base64decode( {"IlJlc3BvbnNpdmUgTmlra2Vp44Ki44Or44OV44Kh44OX44Ot44Kw44Op44Og44 Gu44Oh44Oz44OQ44O844Gu55qG5qeY44CB44Ki44Or44OV44Kh44OQ44O844K444 On44Oz44Gu44K144Kk44OI44Gr44Ki44Kv44K744K544Gn44GN44Gq44GE5aC05Z CI44CBcm5mZWVkYmFja0BuZXgubmlra2VpLmNvLmpwIOOBvuOBp+OBneOBruaXqO mAo+e1oeOBj+OBoOOBleOBhOOAgiI="});

  53. Wishlist 53

  54. I have 68 backends 54

  55. 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 ...
  56. Thanks for listening 56 Andrew Betts andrew.betts@ft.com @triblondon Get the

    slides bit.ly/ft-fastly-altitude-2016