Server-less apps
powered by
Web Components
Sébastien Cevey
JSConf EU, 14 September 2014
Sébastien Cevey
@theefer
Senior Engineer on Editorial tools
Composer CMS
Media Service
3-tier architecture
Presentation
Middle tier
Data storage
Front-end
Back-end
Database
(or Application, Domain logic)
Meh
Rigid
- Single data source (Graphite)
- Configuration mixed with code
- Hard to add/edit graphs
Complex
- Only available via VPN
- Hard to release changes
- Bad PHP code
Let’s build a better one
AWS
CloudWatch
Graphite
?
Presentation
Middle tier
Data storage
What is the middle tier for?
-
Auth
-
Query
-
Render
Also: serve, cache
// IAM credentials with read-only access to CloudWatch metrics
var awsConfig = {
region: 'eu-west-1',
accessKeyId: 'AK9DU7JD72OXDDWI123Z',
secretAccessKey: 'D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7'
};
Auth
var awsConfig = { ... };
var cloudWatch = new AWS.CloudWatch(awsConfig);
cloudWatch.getMetricStatistics({
Namespace: 'AWS/ELB',
MetricName: 'RequestCount',
Dimensions: [{
Name: 'LoadBalancerName', Value: 'server-MyApp-XD8DEAW3Q1VY'
}],
StartTime: '2014-09-01T12:51:23Z',
EndTime: '2014-09-01T13:51:23Z',
Period: 60, // seconds
Statistics: ['Sum']
}, function(err, data) {
console.log(data);
});
Query
{
Label: "RequestCount",
ResponseMetadata: { ... },
Datapoints: [
{Timestamp: Date(2014-09-02T13:15:17Z), Sum: 7, Unit: "Count"},
{Timestamp: Date(2014-09-02T13:16:18Z), Sum: 11, Unit: "Count"},
...
]
}
var awsConfig = { ... };
var cloudWatch = new AWS.CloudWatch(awsConfig);
cloudWatch.getMetricStatistics({
...
Statistics: ['Sum']
}, function(err, data) {
var data = ['Sum'].reduce(function(all, stat) {
all[stat.toLowerCase()] = data.Datapoints.map(function(point) {
return [point.Timestamp, point[stat]];
}).sort(function(a, b) {
// data may come out of chronological order :-(
return a[0] - b[0];
});
return all;
}, {});
console.log(data);
});
{
sum: [
[Date(2014-09-02T13:15:17Z), 7],
[Date(2014-09-02T13:16:18Z), 11],
...
]
}
var cloudWatchData = ...;
google.load("visualization", "1", {packages:["corechart"]});
google.setOnLoadCallback(drawChart);
function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('date', 'Time');
data.addColumn('number', 'Requests');
data.addRows(cloudWatchData.sum);
var options = {
title: 'Request count',
vAxis: {minValue: 0}
};
var chartEl = document.getElementById('chart');
var chart = new google.visualization.AreaChart(chartEl);
chart.draw(data, options);
}
Render
<!-- main.html -->
<div id="chart" style="width: 500px; height: 150px;"></div>
Not “better” enough
Complex
Only available via VPNHard to release changesBad PHP code
Rigid
- Single data source (AWS)
- Configuration mixed with code
- Hard to add/edit graphs
The Dream
<aws-requests></aws-requests>
Web Components
<google-map lat="37.790" long="-122.390"></google-map>
<link rel="import" href="google-map.html">
<script src="platform.js"></script>
API Polyfills
HTML Import
Custom elements
Polymer
Build on top of platform.js
See also:
<!-- Define in my-greeter.html -->
<link rel="import" href="../polymer/polymer.html">
<polymer-element name="my-greeter" attributes="name">
<template>
<p>Hello {{name}}</p>
<p>Rename: <input type="text" value="{{name}}"></p>
</template>
<script>
Polymer('my-greeter', {
// definition ...
});
</script>
</polymer-element>
<!-- Import and use -->
<link rel="import" href="my-greeter.html"/>
<my-greeter name="Séb"></my-greeter>
Element scaffolding
Bi-directional data-binding
as seen in Angular, Knockout, etc.
<aws-config region="eu-west-1"
key="AK9DU7JD72OXDDWI123Z"
secret="D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7">
</aws-config>
Auth
aws-config
region
"eu-west-1"
"AK9..."
"D8b..."
key
secret
<aws-config region="eu-west-1"
key="AK9DU7JD72OXDDWI123Z"
secret="D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7"
sink="{{awsConfig}}">
</aws-config>
Auth
awsConfig => {
region: "eu-west-1",
accessKeyId: "AK9DU7JD72OXDDWI123Z",
secretAccessKey: "D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7"
}
awsConfig
aws-config
region
"eu-west-1"
"AK9..."
"D8b..."
key
secret
sink
<!-- aws-config.html -->
<link rel="import" href="../polymer/polymer.html">
<polymer-element name="aws-config" attributes="region key secret sink">
<script>
Polymer('aws-config', {
observe: { 'region': 'update',
'key': 'update',
'secret': 'update' },
ready: function() { this.update(); },
update: function() {
if (this.region && this.key && this.secret) {
this.sink = {
region: this.region,
accessKeyId: this.key,
secretAccessKey: this.secret
};
} else {
delete this.sink;
}
}
});
</script>
</polymer-element>
Auth: implementation
<aws-cloudwatch config="{{awsConfig}}"
namespace="AWS/ELB"
metricname="RequestCount"
starttime="2014-09-01T12:51:23Z"
endtime="2014-09-01T13:51:23Z"
resolution="60"
statistics="sum"
sink="{{awsMetricsRequests}}">
<aws-dimension name="LoadBalancerName"
value="server-MyApp-XD8DEAW3Q1VY" />
</aws-cloudwatch>
Query
awsMetricsRequests => {
sum: [
[Date(2014-09-02T13:15:17Z), 7],
[Date(2014-09-02T13:16:18Z), 11],
...
]
}
awsMetricsRequests
aws-cloudwatch
config
"AWS/ELB"
...
namespace
...
sink
awsConfig
<google-chart type="area"
height="150px"
width="500px"
options='{
"title": "Request rate",
"vAxes": [{ "viewWindow": { "min": 0 }}]
}'
cols='[{"label": "Time", "type": "datetime"},
{"label": "200s", "type": "number"}]'
rows="{{awsMetricsRequests.sum}}">
</google-chart>
Render
google-chart
rows
...
...
awsMetricsRequests
awsConfig
aws-config
region
"eu-west-1"
"AK9..."
"D8b..."
key
secret
sink
awsMetricsRequests
aws-cloudwatch
config
...
sink
google-chart
rows
...
<html>
<head>
<script src="bower_components/platform/platform.js"></script>
<link rel="import" href="bower_components/aws-config/aws-config.html">
<link rel="import" href="bower_components/aws-cloudwatch/aws-cloudwatch.html">
<link rel="import" href="bower_components/google-chart/google-chart.html">
</head>
<body>
<template is="auto-binding">
<aws-config region="eu-west-1"
key="AK9DU7JD72OXDDWI123Z"
secret="D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7"
sink="{{awsConfig}}">
</aws-config>
<aws-cloudwatch config="{{awsConfig}}"
namespace="AWS/ELB"
metricname="RequestCount"
starttime="2014-09-01T12:51:23Z"
endtime="2014-09-01T13:51:23Z"
resolution="60"
statistics="sum"
sink="{{awsMetricsRequests}}">
<aws-dimension name="LoadBalancerName" value="server-MyApp-XD8DEAW3Q1VY" />
</aws-cloudwatch>
<google-chart type="area" height="120px" width="100%"
options='{"title": "Request rate",
"vAxes": [{ "viewWindow": { "min": 0 }}]}'
cols='[{"label": "Time", "type": "datetime"},
{"label": "200s", "type": "number"}]'
rows="{{awsMetricsRequests.sum}}">
</google-chart>
</template>
</body>
</html>
<aws-cloudwatch config="{{awsConfig}}"
...
sink="{{awsMetricsRequests}}">
<aws-dimension name="LoadBalancerName" value="server-MyApp-XD8DEAW3Q1VY" />
</aws-cloudwatch>
<google-chart type="area" height="120px" width="100%"
options='{"title": "Request rate",
"vAxes": [{ "viewWindow": { "min": 0 }}]}'
cols='[{"label": "Time", "type": "datetime"},
{"label": "200s", "type": "number"}]'
rows="{{awsMetricsRequests.sum}}">
</google-chart>
<aws-cloudwatch config="{{awsConfig}}"
...
sink="{{awsMetricsOtherRequests}}">
<aws-dimension name="LoadBalancerName" value="server-Other-QA4R5U99UZVA" />
</aws-cloudwatch>
<google-chart type="area" height="120px" width="100%"
options='{"title": "Request rate",
"vAxes": [{ "viewWindow": { "min": 0 }}]}'
cols='[{"label": "Time", "type": "datetime"},
{"label": "200s", "type": "number"}]'
rows="{{awsMetricsOtherRequests.sum}}">
</google-chart>
Boilerplate alert!
<polymer-element name="aws-requests"
attributes="config loadBalancerName"
noscript>
<template>
<aws-cloudwatch config="{{config}}"
namespace="AWS/ELB"
metricname="RequestCount"
starttime="2014-09-01T12:51:23Z"
endtime="2014-09-01T13:51:23Z"
resolution="60"
statistics="sum"
sink="{{requestsData}}">
<aws-dimension name="LoadBalancerName"
value="{{loadBalancerName}}" />
</aws-cloudwatch>
<google-chart type="area"
height="120px"
width="100%"
options='{
"title": "Request rate",
"vAxes": [{ "viewWindow": { "min": 0 }}]
}'
cols='[{"label":"Time", "type":"datetime"},
{"label":"200s", "type":"number"}]'
rows="{{requestsData.sum}}">
</google-chart>
</template>
</polymer-element>
<aws-requests config="{{awsConfig}}"
loadBalancerName="server-MyApp-XD8DEAW3Q1VY">
</aws-requests>
<aws-requests config="{{awsConfig}}"
loadBalancerName="server-Other-QA4R5U99UZVA">
</aws-requests>
Dream unlocked!
<aws-latency config="{{awsConfig}}"
loadBalancerName="server-MyApp-XD8DEAW3Q1VY">
</aws-latency>
<aws-requests config="{{awsConfig}}"
loadBalancerName="server-MyApp-XD8DEAW3Q1VY">
</aws-requests>
<aws-counts config="{{awsConfig}}"
namespace="PROD/MyApp" metricname="SomeCount">
</aws-counts>
<graphite-metrics baseUri="https://graphite.example.com"
target="ganglia.__SummaryInfo__.requests_latency-MyApp.sum"
since="2014-09-01T12:51:23Z"
until="2014-09-01T13:51:23Z"
resolution="60"
sink="{{graphiteLatencyAvg}}">
</graphite-metrics>
<google-chart type="line"
...
rows="{{graphiteLatencyAvg}}">
</google-chart>
Graphite data source
<aws-cloudwatch starttime="2014-09-01T12:51:23Z"
endtime="2014-09-01T13:51:23Z"
... >
...
</aws-cloudwatch>
Time: we cheated...
aws-cloudwatch
starttime
"2014-..."
endtime
...
"2014-..."
<aws-cloudwatch starttime="2014-09-01T12:51:23Z"
endtime="{{now}}"
... >
...
</aws-cloudwatch>
<time-now sink="{{now}}"></time-now>
<time-now sink="{{now}}" refresh="60000"></time-now>
aws-cloudwatch
starttime
endtime
...
"2014-..."
now
time-now
refresh
sink
?
60000
sink
awsMetricsRequests
<time-now sink="{{now}}" refresh="60000"></time-now>
<time-range start="{{start}}" end="{{now}}" hours="1"></time-range>
<aws-cloudwatch starttime="{{start}}"
endtime="{{now}}"
... >
...
</aws-cloudwatch>
aws-cloudwatch
starttime
endtime
...
now
time-now
refresh
sink
60000
start
time-range
hours
1
end
start
aws-cloudwatch
starttime
endtime
...
now
time-now
refresh
sink
60000
start
time-range
hours
end
start
paper-radio-group
selected
hours
?
sink
awsMetricsRequests
<!-- in the <head> -->
<link rel="import"
href="bower_components/paper-radio-group/paper-radio-group.html">
<link rel="import"
href="bower_components/paper-radio-button/paper-radio-button.html">
...
<time-now sink="{{now}}" refresh="60000"></time-now>
<time-range start="{{start}}" end="{{now}}" hours="{{hours}}"></time-range>
<paper-radio-group selected="{{hours}}">
<paper-radio-button name="1" label="1h"></paper-radio-button>
<paper-radio-button name="3" label="3h"></paper-radio-button>
<paper-radio-button name="12" label="12h"></paper-radio-button>
<paper-radio-button name="24" label="1d"></paper-radio-button>
<paper-radio-button name="168" label="1w"></paper-radio-button>
</paper-radio-group>
<aws-cloudwatch starttime="{{start}}"
endtime="{{now}}"
... >
...
</aws-cloudwatch>
<aws-config region="eu-west-1"
key="AK9DU7JD72OXDDWI123Z"
secret="D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7"
sink="{{awsConfig}}">
</aws-config>
Config: hardcoded secrets...
<aws-identity accountId="9488595038548"
roleName="webIdentityRole"
region="eu-west-1"
sink="{{awsConfig}}">
<google-signin clientId="4832948-djsaj390d.apps.googleusercontent.com"
scopes="https://www.googleapis.com/auth/plus.login
https://www.googleapis.com/auth/userinfo.email">
</google-signin>
</aws-identity>
AWS Identity Federation!
Demo time!
Caveats
Limited browser support (platform.js)
Low-traffic only
no caching, many requests
Server must provide auth
e.g. AWS policies, IP-based
Coarse permissions
no server to enforce business logic
Future
- Deploy lines on graphs
- KPI graphs (Mixpanel, etc)
- JS errors (Sentry)
- Server instances & health (EC2, Prism)
Lessons
Removing tiers (servers) can reduce complexity
Encapsulation of logic into simple (web) components
Flexibility through composition
Sébastien Cevey
@theefer
Server-less applications powered by Web Components [JSConf EU '14]
By Sébastien Cevey
Server-less applications powered by Web Components [JSConf EU '14]
- 7,906