Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Solving anything in VCL

9,156 views

Published on

Andrew Betts Web Developer, The Financial Times at Fastly Altitude 2016

Running custom code at the Edge using a standard language is one of the biggest advantages of working with Fastly’s CDN. Andrew gives you a tour of all the problems the Financial Times and Nikkei solve in VCL and how their solutions work.

Published in: Technology

Solving anything in VCL

  1. 1. Solving anything in VCL Andrew Betts, Financial Times
  2. 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. 3. Nikkei 1. Largest business newspaper in Japan 2. Globally better known for the Nikkei 225 stock index 3. Around 3 million readers
  4. 4. 4
  5. 5. 5
  6. 6. 6
  7. 7. 7
  8. 8. 8
  9. 9. Coding on the edge 9
  10. 10. Benefits of edge code 10 1. Smarter routing 2. Faster authentication 3. Bandwidth management 4. Higher cache hit ratio
  11. 11. Edge side includes 11 <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
  12. 12. 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 12
  13. 13. 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
  14. 14. SOA Routing in VCL 14 Front page Article page Timeline Content API Choose a backend based on a path match of the request URL /article/123
  15. 15. SOA Routing in VCL 15 [ { 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
  16. 16. 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 16
  17. 17. service-registry.json 17 [ { "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
  18. 18. routing.vcl.handlebars 18 {{#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); }
  19. 19. build.js 19 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');
  20. 20. 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
  21. 21. 21 Polyfill screenshot
  22. 22. UA Targeting 22 /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
  23. 23. 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; 23
  24. 24. ua-targeting.vcl 24 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 ~ "^/vd/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 ~ "^/vd/polyfill..*[?&]ua=" && req.http.X-Orig-URL && req.http.X-Orig-URL !~ "[?&]ua=") { add resp.http.Vary = "User-Agent"; } return(deliver); }
  25. 25. 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
  26. 26. Magic circa 2001 26 <?php echo $_SERVER['PHP_AUTH_USER']; ?> http://intranet/my/example/app
  27. 27. New magic circa 2016 27 app.get('/', (req, res) => { res.end(req.get('Nikkei-UserID')); });
  28. 28. Authentication 28 /article/123 Nikkei-UserID: andrew.betts Nikkei-UserRank: premium Vary: Nikkei-UserRank Article Cookie: Auth=a139fm24... Cache-control: private
  29. 29. 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"); 29
  30. 30. authentication.vcl 30 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 ...
  31. 31. authentication.vcl (cont) 31 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"; }
  32. 32. 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
  33. 33. 33
  34. 34. 34 Now you see it...
  35. 35. Feature flags parts 35 ● 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
  36. 36. Feature flags 36 Flags API Article Merge the flags response with the override cookie, set as HTTP header, restart original request... /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
  37. 37. ExpressJS flags middleware 37 app.get('/', (req, res) => { if (req.flags.has('highlights')) { // Enable highlights feature } }); HTTP/1.1 200 OK Vary: Nikkei-Flags ...
  38. 38. 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
  39. 39. Dynamic backends 39 Developer laptopDynamic 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 ngrok fc57848a .ngrok.io
  40. 40. 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} 40
  41. 41. 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
  42. 42. 42 The VCL flow
  43. 43. 43 The VCL flow
  44. 44. 44 The VCL flow
  45. 45. Debug journey 45 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; }
  46. 46. Debug journey 46 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)
  47. 47. 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
  48. 48. Resource timing on front end 48 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) ;
  49. 49. Add CDN data in VCL & respond with synthetic 49 sub vcl_recv { if (req.url ~ "^/sendBeacon") { error 204 "No content"; } }
  50. 50. RUM++ 50 /sendBeacon?foo=42&... 204 No Content
  51. 51. 51
  52. 52. Crunch the data 52
  53. 53. 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
  54. 54. Everyone does UTF-8 now, right? 54 synthetic {"Responsive Nikkeiアルファプログラムのメンバーの皆様、ア ルファバージョンのサイトにアクセスできない場合、 rnfeedback@nex.nikkei.co.jp までその旨連絡ください。"};
  55. 55. 55
  56. 56. Quick conversion 56 "string" .split('') .map( char => char.codePointAt(0) < 128 ? char : "&#"+char.codePointAt(0)+";" ) .join('') ;
  57. 57. "Fixed" 57 synthetic {"Responsive Nikkeiアルファプログ&# 12521;ムのメンバーの&# 30342;様、アルファバ&# 12540;ジョンのサイト&# 12395;アクセスできな&# 12356;場合、rnfeedback@nex.nikkei.co.jp までその旨連絡く ださい。"};
  58. 58. "Fixed" 58 synthetic digest.base64decode( {"IlJlc3BvbnNpdmUgTmlra2Vp44Ki44Or44OV44Kh44OX44Ot44Kw44Op44Og44 Gu44Oh44Oz44OQ44O844Gu55qG5qeY44CB44Ki44Or44OV44Kh44OQ44O844K444 On44Oz44Gu44K144Kk44OI44Gr44Ki44Kv44K744K544Gn44GN44Gq44GE5aC05Z CI44CBcm5mZWVkYmFja0BuZXgubmlra2VpLmNvLmpwIOOBvuOBp+OBneOBruaXqO mAo+e1oeOBj+OBoOOBleOBhOOAgiI="});
  59. 59. 59
  60. 60. I have 68 backends 60
  61. 61. 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 61 > 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 ...
  62. 62. Thanks for listening 62 Andrew Betts andrew.betts@ft.com @triblondon Get the slides bit.ly/ft-fastly-altitude-2016

×