Server-less apps
powered by
Web Components

 

Sébastien Cevey

LondonJS, 4 September 2014

Sébastien Cevey

@theefer

inso.cc

Senior Engineer on Editorial tools

Composer CMS

Media Service

3-tier architecture

Presentation

Middle tier

Data storage

http://en.wikipedia.org/wiki/Multitier_architecture

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
  • Hacky live updating

Let’s build a better one

AWS

CloudWatch

Graphite

?

Presentation

Middle tier

Data storage

What is the middle tier for?

  • Auth

  • Query

  • Transform

  • 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

https://github.com/aws/aws-sdk-js

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

Transform

{
    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 VPN
  • Hard to release changes
  • Bad PHP code
  • Hacky live updating

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

http://www.polymer-project.org/

See also:

Mozilla X-Tags

<!-- Define in my-greeter.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

https://github.com/guardian/element-aws-config

<aws-config region="eu-west-1"
            key="AK9DU7JD72OXDDWI123Z"
            secret="D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7"
            sink="{{awsConfig}}">
</aws-config>

Auth

awsConfig => {
    region:          "eu-west-1",
    accessKeyId:     "AK9DU7JD72OXDDWI123Z",
    secretAccessKey: "D8bX0K3fd9kflqSXK8dGD022XASKD3ldKXPQX9x7"
}
<!-- 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

https://github.com/guardian/element-aws-cloudwatch

awsMetricsRequests => {
    sum: [
        [Date(2014-09-02T13:15:17Z),  7],
        [Date(2014-09-02T13:16:18Z), 11],
        ...
    ]
}
<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

https://github.com/GoogleWebComponents/google-chart

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

The Dream!

<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

https://github.com/guardian/element-graphite-metrics

<aws-cloudwatch starttime="2014-09-01T12:51:23Z"
                endtime="2014-09-01T13:51:23Z"
                ... >
    ...
</aws-cloudwatch>

Time: we cheated...

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

https://github.com/guardian/element-time-now

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

https://github.com/guardian/element-time-range

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

http://www.polymer-project.org/docs/elements/paper-elements.html

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

https://github.com/guardian/element-aws-identity

Demo time!

https://github.com/guardian/element-radiator

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

Thank you!

 

 

Any questions?

Made with Slides.com