$30 off During Our Annual Pro Sale. View Details »

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. Solving anything in VCL
    Andrew Betts, Financial Times

    View Slide

  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.

    View Slide

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

    View Slide

  4. Coding on the edge
    4

    View Slide

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

    View Slide

  6. Edge side includes
    6
    alt="http://bak.example.com/2.html" onerror="continue"/>
    index.html
    my-news.html
    Cache-control: max-age=86400
    Cache-control: private
    Server

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  16. 16
    Polyfill screenshot

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  21. Magic circa 2001
    21
    echo $_SERVER['PHP_AUTH_USER'];
    ?>
    http://intranet/my/example/app

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  28. 28

    View Slide

  29. 29
    Now you see it...

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  37. 37
    The VCL
    flow

    View Slide

  38. 38
    The VCL
    flow

    View Slide

  39. 39
    The VCL
    flow

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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!

    View Slide

  46. Crunch the data
    46

    View Slide

  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

    View Slide

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

    View Slide

  49. 49

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  53. Wishlist
    53

    View Slide

  54. I have 68 backends
    54

    View Slide

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

    View Slide

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

    View Slide