Kyle Mo
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
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