Kyle Mo

Coding Your CDN 🎃

X

=

Lambda@Edge

Let's Go Serverless !

I believe that Serverless is the future.

What is Serverless ?

 Serverless !== No Server

#

 Faas (Function As A Service)

#

 Serverless !== No Server

 Deployment、Scaling、Management

#

#

Computing

#

Application Intergration

#

Data Storage

 AWS Lambda

 AWS API Gateway

 AWS S3, DynamoDB

Features Of Serverless

No servers to manage or provision

Consumption  priced

Auto Scaling

Availability and fault tolerance built in

Event Triggered

Enterprise's Point Of View

Cheaper

A cloud infrastructure that takes less engineering effort to design, build, and maintain

And Us Developer...

Today, we will focus on...

AWS Lambda

import json
import re


def validate(event, context):
    event_body = json.loads(event['body'])
    email_regex = re.compile('^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
    matches = email_regex.match(event_body['email']) != None

    response = {
        'statusCode': 200,
        'body': json.dumps({ 'result': matches })
    }

    return response

Limitations of AWS Lambda

Stateless

AWS CloudFront CDN

 Performance

#

 Availability

#

 Security

#

 Failover

#

Heavy Traffic

#

Can we add some Intelligence to it ?

X

=

Lambda@Edge

What is AWS Lambda@Edge ?

Running Lambda On the CDN Edge Server

Bringing Some Magic 😎 

Triggering Conditions

What can Lambda@Edge Do ?

Performance

Dynamic Content Generation

Security

User tracking and analysis

Performance

Cache Miss %

Cache Hit %

Dynamic Content Generation

Resize Image Based On Request Attributes 

A/B Testing 

Redirect On Some Conditions

Return Images of Different Resolutions or Types

Prettified URL

#  Visitor Prioritization

Failover

Security

Access Control

Add Some HTTP headers (CSP, HSTS) 

Bot Detection

User Tracking & Analysis

SEO Problem Of SPA

SEO Problem Of SPA

SEO Problem Of SPA

'use strict';

const customUserAgentHeaderName = 'x-is-bot';
exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  const userAgent = headers['user-agent'][0].value;
  
  headers[customUserAgentHeaderName] = [{
    key: 'x-is-bot',
    value: checkIfBot(userAgent).toString()
  }]

  callback(null, request);
};

function checkIfBot(userAgent) {
  if (userAgent) {

    const botUserAgents = [
      'googlebot',
      'Yahoo! Slurp',
      'bingbot',
      'yandex',
      'baiduspider',
      'facebookexternalhit',
      'twitterbot',
      'rogerbot',
      'linkedinbot',
      'embedly',
      'quora link preview',
      'showyoubot',
      'outbrain',
      'pinterest/0.',
      'developers.google.com/+/web/snippet',
      'slackbot',
      'vkShare',
      'W3C_Validator',
      'redditbot',
      'Applebot',
      'WhatsApp',
      'flipboard',
      'tumblr',
      'bitlybot',
      'SkypeUriPreview',
      'nuzzel',
      'Discordbot',
      'Google Page Speed',
      'Qwantify',
      'pinterestbot',
      'Bitrix link preview',
      'XING-contenttabreceiver',
      'Chrome-Lighthouse'
    ];


    if (botUserAgents.some(function (botUserAgent) {
        return userAgent.toLowerCase().indexOf(botUserAgent) !== -1;
      })) {
      return true;
    }
    return false;
  }
  return false;
}

Viewer Request 

Separate the user-agent of the crawler and increase the cache hit-rate of Cloud Front

SEO Problem Of SPA

'use strict';

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;
  let SHOULD_PRERENDER = false;
  let IsBot = request.headers['x-is-bot'][0].value === 'true';
    
  SHOULD_PRERENDER = IsBot;

  request.headers['x-prerender-uri'] = [{
    key: 'x-prerender-uri',
    value: request.uri
  }];
  
  request.headers['x-should-prerender'] = [{
    key: 'x-should-prerender',
    value: SHOULD_PRERENDER.toString()
  }];
  callback(null, request);
};

Origin Request 

The important point here is to write down the url of the current prerender and stuff it into the header

SEO Problem Of SPA

'use strict';
const https = require('https');
const BASE_URL_RENDERER = 'YOUR_PRERENDER_SERVER?url=';

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const response = event.Records[0].cf.response;
    const headers = response.headers;


    const SHOULD_PRERENDER = request.headers['x-should-prerender'][0].value === 'true';
    const PRERENDER_URL = request.headers['x-prerender-uri'][0].value;

    if (SHOULD_PRERENDER && PRERENDER_URL) {
        const URL = BASE_URL_RENDERER + PRERENDER_URL;

        downloadContent(URL, (body) => {
            callback(null, {
                status: '200',
                statusDescription: 'OK',
                headers: response.headers,
                body
            });

        });
    } else {
        callback(null, response);
    }
};


const downloadContent = function (url, callback) {
    https
        .get(url, function (res) {
            var body = '';

            res.on('data', function (chunk) {
                body += chunk.toString();
            });

            res.on('end', function () {
                callback(body, null);
            });
        })
        .on('error', function (e) {
            callback(null, e);
        });
};

Origin Response 

Get the url that needs prerender from the header and send the prerender request, and then send it back after getting the response back.

Advantages Of Prerendering In The Cloud

#  No need to modify the program drastically

Make it simple 

#  Don't worry about high traffic

Demo A/B Testing

Demo A/B Testing

// Origin Request
 import json
 import random
 
 def lambda_handler(event, context):
     request = event['Records'][0]['cf']['request']
     headers = request['headers']
 
     cookieExperimentA, cookieExperimentB = 'site=a', 'site=b'
     pathExperimentA, pathExperimentB = 'Your site a s3 domain', 'Your site b s3 domain'
 
 
     experimentUri = ""
 
     for cookie in headers.get('cookie', []):
         if cookieExperimentA in cookie['value']:
             print("Experiment A cookie found")
             experimentUri = pathExperimentA
             break
         elif cookieExperimentB in cookie['value']:
             print("Experiment B cookie found")
             experimentUri = pathExperimentB
             break
 
     if not experimentUri:
         print("Experiment cookie has not been found. Throwing dice...")
         if random.random() < 0.75:
             experimentUri = pathExperimentA
         else:
             experimentUri = pathExperimentB
 
     request['uri'] = experimentUri
     print(f"Request uri set to {experimentUri}")
     return request

Demo A/B Testing : Forward Cookies

Limitations Of Lambda@Edge

Only deploy in US East (N. Virginia) Region

#  Only Node.js & Python now

#  Not support environment variables

#  Some http headers are in blacklist

#  Not support Lambda dead letter queue

Infrastructure as Code Is A Trend

# Cloudformation example

Parameters: 
  ExistingSecurityGroup: 
    Description: An existing security group ID (optional).
    Default: NONE
    Type: String
    AllowedValues: 
      - default
      - NONE
Conditions: 
  CreateNewSecurityGroup: !Equals [!Ref ExistingSecurityGroup, NONE]
Resources: 
  MyInstance: 
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: "ami-0ff8a91507f77f867"
      SecurityGroups: !If [CreateNewSecurityGroup, !Ref NewSecurityGroup, !Ref ExistingSecurityGroup]
  NewSecurityGroup: 
    Type: "AWS::EC2::SecurityGroup"
    Condition: CreateNewSecurityGroup
    Properties: 
      GroupDescription: Enable HTTP access via port 80
      SecurityGroupIngress: 
        - 
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
Outputs: 
  SecurityGroupId:
    Description: Group ID of the security group used.
    Value: !If [CreateNewSecurityGroup, !Ref NewSecurityGroup, !Ref ExistingSecurityGroup]

IaC Providers

But...wait, I hate AWS, I am not its user.

Ok, that's see another cloud provider

Special Features Of Cloudflare Worker

Using Service Worker API

Benchmark Comparison

Sample : Aggregate Requests

/**
 * someHost is set up to return JSON responses
 * Replace url1 and url2 with the hosts you wish to send requests to
 * @param {string} url the URL to send the request to
 */
const someHost = "https://examples.cloudflareworkers.com/demos"
const url1 = someHost + "/requests/json"
const url2 = someHost + "/requests/json"
const type = "application/json;charset=UTF-8"
/**
 * gatherResponse awaits and returns a response body as a string.
 * Use await gatherResponse(..) in an async function to get the response body
 * @param {Response} response
 */
async function gatherResponse(response) {
  const { headers } = response
  const contentType = headers.get("content-type") || ""
  if (contentType.includes("application/json")) {
    return JSON.stringify(await response.json())
  }
  else if (contentType.includes("application/text")) {
    return await response.text()
  }
  else if (contentType.includes("text/html")) {
    return await response.text()
  }
  else {
    return await response.text()
  }
}

async function handleRequest() {
  const init = {
    headers: {
      "content-type": type,
    },
  }
  const responses = await Promise.all([fetch(url1, init), fetch(url2, init)])
  const results = await Promise.all([
    gatherResponse(responses[0]),
    gatherResponse(responses[1]),
  ])
  return new Response(results.join(), init)
}

addEventListener("fetch", event => {
  return event.respondWith(handleRequest())
})

Wrap up - What do I want to tell you ?

Serverless's endless goal is...

There will be more solutions in the future

Lambda@Edge & Cloudflare Worker are only two of them

But...actually, I am a frontend developer....

Serverless & BFF are two oppturnities

Thank You

Made with Slides.com