Slide 1

Slide 1 text

Solving anything in VCL Andrew Betts, Financial Times

Slide 2

Slide 2 text

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.

Slide 3

Slide 3 text

Nikkei 1. Largest business newspaper in Japan 2. Globally better known for the Nikkei 225 stock index 3. Around 3 million readers

Slide 4

Slide 4 text

Coding on the edge 4

Slide 5

Slide 5 text

Benefits of edge code 5 1. Smarter routing 2. Faster authentication 3. Bandwidth management 4. Higher cache hit ratio

Slide 6

Slide 6 text

Edge side includes 6 index.html my-news.html Cache-control: max-age=86400 Cache-control: private Server

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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); }

Slide 14

Slide 14 text

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');

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

16 Polyfill screenshot

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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); }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Magic circa 2001 21 http://intranet/my/example/app

Slide 22

Slide 22 text

New magic circa 2016 22 app.get('/', (req, res) => { res.end(req.get('Nikkei-UserID')); });

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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 ...

Slide 26

Slide 26 text

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"; }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

28

Slide 29

Slide 29 text

29 Now you see it...

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

37 The VCL flow

Slide 38

Slide 38 text

38 The VCL flow

Slide 39

Slide 39 text

39 The VCL flow

Slide 40

Slide 40 text

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; }

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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) ;

Slide 44

Slide 44 text

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); } }

Slide 45

Slide 45 text

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!

Slide 46

Slide 46 text

Crunch the data 46

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Everyone does UTF-8 now, right? 48 synthetic {"Responsive Nikkeiアルファプログラムのメンバーの皆様、アル ファバージョンのサイトにアクセスできない場合、[email protected] までその旨連絡ください。"};

Slide 49

Slide 49 text

49

Slide 50

Slide 50 text

Quick conversion 50 "string" .split('') .map( char => char.codePointAt(0) < 128 ? char : ""+char.codePointAt(0)+";" ) .join('') ;

Slide 51

Slide 51 text

"Fixed" 51 synthetic {"Responsive Nikkeiアルフ ァプログラムのメ ンバーの皆様、ア ルファバージョン のサイトにアクセ スできない場合、 [email protected] までその 旨連絡ください 。"};

Slide 52

Slide 52 text

"Fixed" 52 synthetic digest.base64decode( {"IlJlc3BvbnNpdmUgTmlra2Vp44Ki44Or44OV44Kh44OX44Ot44Kw44Op44Og44 Gu44Oh44Oz44OQ44O844Gu55qG5qeY44CB44Ki44Or44OV44Kh44OQ44O844K444 On44Oz44Gu44K144Kk44OI44Gr44Ki44Kv44K744K544Gn44GN44Gq44GE5aC05Z CI44CBcm5mZWVkYmFja0BuZXgubmlra2VpLmNvLmpwIOOBvuOBp+OBneOBruaXqO mAo+e1oeOBj+OBoOOBleOBhOOAgiI="});

Slide 53

Slide 53 text

Wishlist 53

Slide 54

Slide 54 text

I have 68 backends 54

Slide 55

Slide 55 text

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 ...

Slide 56

Slide 56 text

Thanks for listening 56 Andrew Betts [email protected] @triblondon Get the slides bit.ly/ft-fastly-altitude-2016