[John Graham-Cumming] Coding on the Edge

[John Graham-Cumming] Coding on the Edge

Presentation from GDG DevFest Ukraine 2018 - the biggest community-driven Google tech conference in the CEE.

Learn more at: https://devfest.gdg.org.ua

__

The popularity of Edge Computing platforms like Cloudflare Workers and Lambda@Edge means there's now a third place to run code: at the edge. Applications are liberated from having to run on device or on server and can now live inside the Internet. In this talk I'll look at the architecture and performance of Edge Computing and show real-world examples of apps running on the Edge.

3a6de6bc902de7f75c0e753b3202ed52?s=128

Google Developers Group Lviv

October 12, 2018
Tweet

Transcript

  1. // Coding on the Edge John Graham-Cumming

  2. There’s a problem with serverless It's not living up to

    its potential.
  3. Minicomputer Units of Compute Granularity

  4. Minicomputer Commodity Server Units of Compute Granularity 10x

  5. Minicomputer Commodity Server Virtual Machine Units of Compute Granularity 10x

    10x
  6. Minicomputer Commodity Server Virtual Machine Container Units of Compute Granularity

    10x 10x 10x
  7. Minicomputer Commodity Server Virtual Machine Container Event Units of Compute

    Granularity 10x 10x 10x
  8. Minicomputer Commodity Server Virtual Machine Container Event Units of Compute

    Granularity 10x 10x 10x 500 req/sec… 86400 sec/day… 30 days/month...
  9. Minicomputer Commodity Server Virtual Machine Container Event Units of Compute

    Granularity 10x 10x 10x 1,000,000,000x 500 req/sec… 86400 sec/day… 30 days/month...
  10. But… First-gen serverless is just containers. Serverless Container "There is

    no cloud" design by Chris Watterston (chriswatterston.com)
  11. None
  12. What if handling just one event was OK?

  13. Your code would run "everywhere"

  14. Colocated Services

  15. onSms(event) { ... stripe.charge(...) ... }) onInvoice(event) { ... email.send(...)

    ... })
  16. onSms(event) { ... stripe.charge(...) ... }) onInvoice(event) { ... email.send(...)

    ... })
  17. onSms(event) { ... stripe.charge(...) ... }) onInvoice(event) { ... email.send(...)

    ... }) Many services One server
  18. Geographic Affinity

  19. None
  20. None
  21. None
  22. None
  23. None
  24. None
  25. None
  26. Speed of Light Speed of Light (m/s) year

  27. None
  28. None
  29. 18 62 Major Cloud Provider #1 32 Major Cloud Provider

    #2 4 Major Cloud Provider #3 Cloudflare Workers 154
  30. New Server(less) Technology Required • Instantaneous startup for one-off invocation.

    • Thousands of simultaneous tenants. • Secure sandboxing.
  31. None
  32. None
  33. One thing missing...

  34. Storage

  35. User Cache App DB

  36. User Cache App DB

  37. 6 to 45 minutes RTT ☹ User Cache App DB

  38. 6 to 45 minutes RTT ☹ User Cache App DB

  39. =

  40. Central DB User Cache App User DB User Cache App

    User DB User Cache App User DB
  41. User Cache App User DB User Cache App User DB

    User Cache App User DB Central DB
  42. None
  43. None
  44. // The Real World

  45. Online Maps

  46. Factorio Maps

  47. None
  48. // Race GCS and B2 let gcsReq = new Request('https://storage.googleapis.com/bucketName'

    + url.pathname, event.request) let b2Req = new Request(getB2Url(request) + '/bucketName' + url.pathname, event.request); // Fetch from GCS and B2 with Cloudflare caching enabled let gcsPromise = fetch(gcsReq, cfSettings); let b2Promise = fetch(b2Req, cfSettings); let response = await Promise.race([gcsPromise, b2Promise]); if (response.ok) { return response; } // If the winner was bad, find the one that is good (if any) response = await gcsPromise; if (response.ok) { return response; }
  49. Send logs to StatHat // Fetch from GCS and B2

    with caching let reqStartTime = Date.now(); let gcsPromise = fetch(gcsReq, cfSettings); let b2Promise = fetch(b2Req, cfSettings); let response = await Promise.race([gcsPromise, b2Promise]); if (response.ok) { event.waitUntil(logResponse(event, response, (Date.now() - reqStartTime))); return response; }
  50. Data Loss Detection and Prevention

  51. Spotting a Canary if(response.headers.get('Content-Type').includes('text')){ let body = await response.text() if(body.includes('SHHHTHISISASECRET'))

    { response = new Response('Blocked.', {status: 403, headers: new Headers({'Private-block': true})}) return response } return new Response(body, {status: response.status, headers: response.headers}) }
  52. Call PagerDuty from the Worker async function createPagerDutyIncident(event) { let

    body = `{ "incident": { "type": "incident", "title": "Potential data breach", "service": { "id": "${PD_SERVICE_ID}", "type": "service_reference" }, } }` let PDInit = { method: 'POST', headers: new Headers({ "Content-Type": "application/json", "Accept": "application/vnd.pagerduty+json;version=2", "From": `${PD_FROM}`, "Authorization": `Token token=${PD_API_KEY}` }), body: body } event.waitUntil(fetch('https://api.pagerduty.com/inc idents', PDInit)) }
  53. Geo-targetting

  54. Redirect to country-specific page… if it exists function fetchAndApply(request) {

    const country = request.headers.get('Cf-Ipcountry').toLowerCase() let url = new URL(request.url) const target_url = 'https://' + url.hostname + '/' + country const target_url_response = await fetch(target_url) if(target_url_response.status === 200) { return new Response('', { status: 302, headers: { 'Location': target_url } }) } else { return response } }
  55. Edge-Side Includes

  56. Dynamically include external content <html lang="en"> <head> <meta charset="UTF-8"> <title>ESI

    Demonstration</title> <script src="/cdn-cgi/apps/head/FRz8JyVlvChltLiN7yAq762_k_A.js"></script></head> <body> <h1>Cloudflare Edge-Side-Include demonstration</h1> <h3>How to ESI with Cloudflare</h3> <p>ESI (or Edge-Side-Include consist in fetching a globally static page with some dynamic fragment, this can be done directly from our EDGE with the workers, where the HTML page would be like that one:</p> <p>Dynamic portion coming from https://httpbin.org/headers:</p> <esi:include src="https://httpbin.org/headers" onerror="continue" /> </body> </html>
  57. Stream content and replace esi: async function fetchAndStream(request) { let

    response = await fetch(request) let contentType = response.headers.get('content-type') if (!contentType || !contentType.startsWith("text/")) { return response } let { readable, writable } = new TransformStream() let newResponse = new Response(readable, response) newResponse.headers.set('cache-control', 'max-age=0') streamTransformBody(response.body, writable) return newResponse }
  58. Find the esi:include and make sub-request async function handleTemplate(encoder, templateKey)

    { const linkRegex = /(esi:include.*src="(.*?)".*\/)/gm let result = linkRegex.exec(templateKey); let esi if (!result) { return encoder.encode(`<${templateKey}>`); } if (result[2]) { esi = await subRequests(result[2]); } return encoder.encode( `${esi}` ); }
  59. Get content via a sub-request async function subRequests(target){ const init

    = { method: 'GET', headers: { 'user-agent': 'cloudflare' } } let response = await fetch(target, init) let text = await response.text() return text }
  60. DroneDeploy

  61. None
  62. Verify JWT on the edge function handleFetch(request) { if (!(await

    isValidJwt(request))) { return new Response('Invalid JWT', { status: 403 }) } const gsBaseUrl = createGoogleStorageUrl(request); const gsHeaders = new Headers(); gsHeaders.set('Date', new Date().toUTCString()); // Required by Google for HMAC signed URLs const signature = await hmacSignature(gsBaseUrl, gsHeaders); gsHeaders.set('Authorization', 'AWS ' + HMAC_KEY + ':' + signature); return fetch(new Request(gsBaseUrl, {headers: gsHeaders})) }
  63. JWT Handling async function isValidJwt(request) { const encodedToken = getJwt(request);

    if (encodedToken === null) { return false } const token = decodeJwt(encodedToken); return isValidJwtSignature(token) } function getJwt(request) { const authHeader = request.headers.get('Authorization'); if (!authHeader || authHeader.substring(0, 6) !== 'Bearer') { return null } return authHeader.substring(6).trim() }
  64. // Thank you John Graham-Cumming