<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[meBooks]]></title><description><![CDATA[Thoughts from the intersection of coding and ebooks]]></description><link>https://blog.mebooks.co.nz/</link><image><url>https://blog.mebooks.co.nz/favicon.png</url><title>meBooks</title><link>https://blog.mebooks.co.nz/</link></image><generator>Ghost 1.9</generator><lastBuildDate>Mon, 21 Sep 2026 12:23:35 GMT</lastBuildDate><atom:link href="https://blog.mebooks.co.nz/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Using Splunk with Traefik]]></title><description><![CDATA[<div class="kg-card-markdown"><p>In this post we'll look at what it takes to set up Splunk with the Traefik reverse-proxy, such that we can send log events using Splunks HTTP Event Collector, and see and query the results in Splunk's admin dashboard.</p>
<p>Splunk is a great tool for capturing and analysing application logs.</p></div>]]></description><link>https://blog.mebooks.co.nz/using-splunk-with-traefik/</link><guid isPermaLink="false">5c96c118a0cf980001f78e3c</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Sun, 24 Mar 2019 01:15:57 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1553176772-493c732932da?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://images.unsplash.com/photo-1553176772-493c732932da?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Using Splunk with Traefik"><p>In this post we'll look at what it takes to set up Splunk with the Traefik reverse-proxy, such that we can send log events using Splunks HTTP Event Collector, and see and query the results in Splunk's admin dashboard.</p>
<p>Splunk is a great tool for capturing and analysing application logs.<br>
Although Splunk is reasonably pricey, they do allow you to use Splunk Enterprise self-hosted for up to 500MB of data per day.</p>
<p>Given that <a href="https://docs.splunk.com/Documentation/Splunk/7.2.4/Installation/DeployandrunSplunkEnterpriseinsideDockercontainers">Splunk Enterprise is available as a Docker image</a>, and I've been using <a href="https://traefik.io/">Traefik</a> as a reverse proxy for my projects recently, I was curious if I could get the two to work nicely together. Looking for clues as to how others had done this in Google, I came up empty-handed, hence this post.</p>
<h2 id="splunkconfiguration">Splunk configuration</h2>
<p>Given that I'm developing on a Mac (though using Linux in production), I ran into the <a href="https://answers.splunk.com/answers/306998/why-am-i-getting-homepathoptsplunkvarlibsplunkaudi.html?childToView=578312#answer-578312">issue that Splunk doesn't yet have robust support for Mac OS High Sierra</a>.<br>
Thankfully there's a workaround that involves adding the <code>OPTIMISTIC_ABOUT_FILE_LOCKING</code> setting to the <code>splunk-launch.conf</code>.</p>
<p>Therefore, when we run Splunk, we'll share an edited version of the <code>splunk-launch.conf</code> via a volume.</p>
<p>Set up the local directory containing an empty <code>splunk-launch.conf</code>:</p>
<pre><code>mkdir -p /opt/splunk
touch /opt/splunk/splunk-launch.conf
</code></pre>
<p>The <code>splunk-launch.conf</code> will look like:</p>
<pre><code>#   Version 7.2.4

# Modify the following line to suit the location of your Splunk install.
# If unset, Splunk will use the parent of the directory containing the splunk
# CLI executable.
#
# SPLUNK_HOME=/opt/splunk-home

# By default, Splunk stores its indexes under SPLUNK_HOME in the
# var/lib/splunk subdirectory.  This can be overridden
# here:
#
# SPLUNK_DB=/opt/splunk-home/var/lib/splunk
# Splunkd daemon name
SPLUNK_SERVER_NAME=Splunkd

# Splunkweb daemon name
SPLUNK_WEB_NAME=splunkweb

# If SPLUNK_OS_USER is set, then Splunk service will only start
# if the 'splunk [re]start [splunkd]' command is invoked by a user who
# is, or can effectively become via setuid(2), $SPLUNK_OS_USER.
# (This setting can be specified as username or as UID.)
#
# SPLUNK_OS_USER
# https://answers.splunk.com/answers/306998/why-am-i-getting-homepathoptsplunkvarlibsplunkaudi.html?childToView=578312#answer-578312
OPTIMISTIC_ABOUT_FILE_LOCKING=1
</code></pre>
<h2 id="runningasplunkdockercontainer">Running a Splunk docker container</h2>
<p>Before we setup our <code>docker-compose.yml</code>, we'll run a Splunk container directly, as we need to check that it works, and also set up the <a href="https://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector">HTTP Event Collector</a>.</p>
<p>In order to use Splunk to capture our logs, splunk exposes the <code>HTTP Event Collector</code> service via an https endpoint, which we can ping with our log data.</p>
<p>Setting up the the <code>HTTP Event Collector</code> basically involves generating an authorization token in the Splunk instance, which can then be used to authenticate each request to send data to Splunk.</p>
<pre><code># Create the local folders
mkdir -p /opt/splunk/etc/
mkdir -p /opt/splunk/var/

# Run the Splunk docker container
docker run \
    -p 8000:8000 \
    -p 8088:8088 \
    -e 'SPLUNK_START_ARGS=--accept-license --no-prompt --answer-yes' \
    -e 'SPLUNK_USERNAME=admin' \
    -e 'SPLUNK_PASSWORD=CHANGEMENOW' \
    -v /opt/splunk/splunk-launch.conf:/opt/splunk/etc/splunk-launch.conf \
    -v /opt/splunk/etc:/opt/splunk/etc \
    -v /opt/splunk/var:/opt/splunk/var \
    splunk/splunk:latest
</code></pre>
<p>Presumably you'll want to change the password from <code>CHANGEMENOW</code> to something more appropriate -- note that Splunk enforces a password policy of a length of at least eight ASCII characters.</p>
<p>Given that this is a test, and given that Splunk takes some time to start up, I've not deamonised the above command, so I can follow the progress easily at the terminal. You'll now that Splunk is ready when you see the following:</p>
<pre><code>Ansible playbook complete, will begin streaming var/log/splunk/splunkd_stderr.log
</code></pre>
<p>You should now be able to see the Splunk admin interface at <a href="http://localhost:8000">http://localhost:8000</a>:</p>
<p><img src="https://blog.mebooks.co.nz/content/images/2019/03/splunk-admin.png" alt="Using Splunk with Traefik"></p>
<h2 id="settingupthehttpeventcollector">Setting up the HTTP Event Collector</h2>
<p>We need to <a href="https://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector">set up the HTTP Event Collector as per the Splunk documentation</a>.</p>
<p>Once done, we should have a <code>Token Value</code> at <a href="http://localhost:8000/en-US/manager/search/http-eventcollector:">http://localhost:8000/en-US/manager/search/http-eventcollector:</a></p>
<p><img src="https://blog.mebooks.co.nz/content/images/2019/03/splunk-http-event-collector.png" alt="Using Splunk with Traefik"></p>
<p>We can test that this works by pinging the Splunk endpoint with some example data:</p>
<pre><code>curl -ki  https://localhost:8088/services/collector/event \
    -H &quot;Authorization: Splunk a6cb2c21-7dd1-4028-bbee-257f5a5e17db&quot; \
    -d '{&quot;event&quot;: &quot;hello splunk&quot;}'
    
HTTP/1.1 200 OK
Content-Length: 27
Content-Type: application/json; charset=UTF-8
Date: Sun, 24 Mar 2019 00:13:53 GMT
Server: Splunkd
Vary: Authorization
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

{&quot;text&quot;:&quot;Success&quot;,&quot;code&quot;:0}
</code></pre>
<p>Note a couple of things about the call:</p>
<ul>
<li>We need to use <code>https</code></li>
<li>Because we probably don't have a valid cert, we need to specify the <code>-k</code> Curl option.</li>
</ul>
<h2 id="traefik">Traefik</h2>
<p>As Splunk demands that we call the HTTP Event Collector using an https endpoint, we need to ensure that we set up the Splunk backend service in Traefik as HTTPS.</p>
<p>However, this leads to the problem that Traefik will not be able to call the Splunk backend service via HTTPS as it doesn't have access to the cert.</p>
<p>We could extract the certs from the Splunk container (they live at <code>$SPLUNK_HOME/etc/auth/</code>) and supply these to Traefik, but for a non-production set up, we can use the Traefik <code>insecureSkipVerify</code> global configuration value.</p>
<p>We'll create a local directory in which our <code>traefik.toml</code>  will live:</p>
<pre><code>mkdir -p /opt/etc/traefik
touch /opt/etc/traefik/traefik.toml
</code></pre>
<p>At a minimum, our <code>traefik.toml</code> will look something like:</p>
<pre><code>################################################################
# Global configuration
################################################################

defaultEntryPoints = [&quot;http&quot;, &quot;https&quot;]
insecureSkipVerify = true

################################################################
# Entrypoints configuration
################################################################

# Entrypoints definition
#
# Optional
# Default:
[entryPoints]
  # http should be redirected to https
  [entryPoints.http]
    address = &quot;:80&quot;
    [entryPoints.http.redirect]
      entryPoint = &quot;https&quot;

  # https is now the default
  [entryPoints.https]
    address = &quot;:443&quot;

  # Traefik status page
  [entryPoints.traefik]
     address = &quot;:8080&quot;
     [entryPoints.traefik.auth.basic]
        usersFile = &quot;/usr/local/share/.auth&quot;

# Enable ACME (Let's Encrypt): automatic SSL
# https://docs.traefik.io/user-guide/examples/#basic-example-with-http-challenge
[acme]
  storage = &quot;/etc/traefik/acme.json&quot;
  caServer = &quot;https://acme-v02.api.letsencrypt.org/directory&quot;
  entryPoint = &quot;https&quot;
  acmeLogging = true
  onDemand = false
  onHostRule = true
  [acme.tlsChallenge]

################################################################
# Traefik logs configuration
################################################################

[traefikLog]

filePath = &quot;/var/log/traefik/traefik.log&quot;

################################################################
# Access logs configuration
################################################################

[accessLog]

filePath = &quot;/var/log/traefik/access.log&quot;
</code></pre>
<h2 id="dockercomposeyml">docker-compose.yml</h2>
<p>We'll assume the following environment variables:</p>
<pre><code>BASIC_AUTH_USERNAME=admin
BASIC_AUTH_PASSWORD=whatever
DOMAIN=localhost.example.com
EMAIL=whoever@example.com
SPLUNK_USERNAME=admin
SPLUNK_PASSWORD=changemenow
CERT=example.com.pem
KEY=example.com-key.pem
</code></pre>
<p>We'll assume that we've added the following entries to our <code>/etc/hosts</code> file:</p>
<pre><code>127.0.0.1	splunk.localhost.example.com
127.0.0.1	splunk-api.localhost.example.com
</code></pre>
<p>We'll also assume that we've generated a cert for <code>*.localhost.example.com</code> -- you can use something like <a href="https://blog.filippo.io/mkcert-valid-https-certificates-for-localhost/">mkcert</a> to generate this cert and key, and store the results at <code>/opt/etc/traefik/</code>.</p>
<p>We'll add basic auth to Traefik by using a <code>Dockerfile</code> based on <code>traefik:alpine</code>:</p>
<pre><code># Dockerfile
FROM traefik:alpine

ARG BASIC_AUTH_USERNAME
ARG BASIC_AUTH_PASSWORD

ENV BASIC_AUTH_USERNAME $BASIC_AUTH_USERNAME
ENV BASIC_AUTH_PASSWORD $BASIC_AUTH_PASSWORD

RUN apk add --update apache2-utils \
  &amp;&amp; rm -rf /var/cache/apk/* \
  &amp;&amp; mkdir -p /var/log/traefik \
  &amp;&amp; mkdir -p /etc/traefik \
  &amp;&amp; touch /etc/traefik/acme.json \
  &amp;&amp; chmod 0600 /etc/traefik/acme.json \
  &amp;&amp; htpasswd -cBb /usr/local/share/.auth $BASIC_AUTH_USERNAME $BASIC_AUTH_PASSWORD
</code></pre>
<p>Our <code>docker-compose.yml</code> ends up looking like the following:</p>
<pre><code>version: '2'

services:
  traefik:
    build:
      context: .
      args:
        - BASIC_AUTH_USERNAME=$BASIC_AUTH_USERNAME
        - BASIC_AUTH_PASSWORD=$BASIC_AUTH_PASSWORD
    container_name: &quot;traefik.${DOMAIN}&quot;
    restart: unless-stopped
    command: -c /dev/null --api --docker --logLevel=DEBUG --acme.email=$EMAIL \
      --configFile=/etc/traefik/traefik.toml \
      --entryPoints='Name:https Address::443 TLS:/etc/traefik/${CERT},/etc/traefik/${KEY}'
    ports:
      - 80:80
      - 443:443
      - 8025:8025
    expose:
      - 8080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/etc/traefik:/etc/traefik
    labels:
      traefik.enable: &quot;true&quot;
      traefik.backend: traefik
      traefik.frontend.rule: &quot;Host:traefik.${DOMAIN}&quot;
      traefik.port: &quot;8080&quot;

  splunk:
    # https://www.splunk.com/blog/2018/10/24/announcing-splunk-on-docker.html
    image: splunk/splunk:latest
    container_name: &quot;splunk.${DOMAIN}&quot;
    environment:
      SPLUNK_START_ARGS: --accept-license --no-prompt --answer-yes
      SPLUNK_USERNAME: $SPLUNK_USERNAME
      SPLUNK_PASSWORD: $SPLUNK_PASSWORD
    restart: unless-stopped
    labels:
      # Use the following yaml formatting to allow our basic auth
      # variables to be properly escaped
      - &quot;traefik.ui.backend=splunk&quot;
      - &quot;traefik.ui.port=8000&quot;
      - &quot;traefik.ui.frontend.rule=Host:splunk.${DOMAIN}&quot;
      - 'traefik.ui.frontend.auth.basic=${BASIC_AUTH_USERNAME}:${BASIC_AUTH_PASSWORD_ENCRYPTED}'
      - &quot;traefik.api.backend=splunk-api&quot;
      - &quot;traefik.api.protocol=https&quot;
      - &quot;traefik.api.port=8088&quot;
      - &quot;traefik.api.frontend.rule=Host:splunk-api.${DOMAIN}&quot;
    volumes:
      # We need to override splunk-launch.conf on Mac OS Sierra:
      # https://answers.splunk.com/answers/306998/why-am-i-getting-homepathoptsplunkvarlibsplunkaudi.html?childToView=578312#answer-578312
      - /opt/splunk/splunk-launch.conf:/opt/splunk/etc/splunk-launch.conf
      - /opt/splunk/etc:/opt/splunk/etc
      - /opt/splunk/var:/opt/splunk/var
    expose:
      - 8000
      - 8088
    depends_on:
      - traefik
</code></pre>
<p>We should now be able to run up our stack. Again, we don't daemonise, so we can easily see the logs messages in the terminal, given that Splunk takes some time to start:</p>
<pre><code>docker-compose up --build
</code></pre>
<p>We should then be able to see our admin at <a href="https://splunk.localhost.example.com">https://splunk.localhost.example.com</a>.</p>
<p>We should also be able to ping the HTTP Event Collector:</p>
<pre><code>curl -ki  https://splunk-api.localhost.example.com/services/collector/event -H &quot;Authorization: Splunk a6cb2c21-7dd1-4028-bbee-257f5a5e17db&quot; -d '{&quot;event&quot;: &quot;hello splunk&quot;}'

HTTP/1.1 200 OK
Content-Length: 27
Content-Type: application/json; charset=UTF-8
Date: Sun, 24 Mar 2019 00:13:53 GMT
Server: Splunkd
Vary: Authorization
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

{&quot;text&quot;:&quot;Success&quot;,&quot;code&quot;:0}
</code></pre>
<h2 id="usingthesplunkservice">Using the Splunk service</h2>
<p>In order to easily send logs to Splunk from docker containers, we'll want to install the <a href="https://github.com/splunk/docker-logging-plugin#install-the-plugin-from-docker-store">docker-logging-plugin</a>:</p>
<pre><code>docker plugin install splunk/docker-logging-plugin:latest --alias splunk-logging-plugin
docker plugin enable splunk-logging-plugin
</code></pre>
<p>If we want another service in our <code>docker-compose.yml</code> to direct logs to Splunk, we'll need to ensure it contains an appropriate <code>logging</code> section, specifying the <code>splunk-token</code> and <code>splunk-url</code> we've set up previously:</p>
<pre><code>    logging:
      driver: splunk-logging-plugin
      options:
        splunk-token: a6cb2c21-7dd1-4028-bbee-257f5a5e17db
        splunk-url: https://splunk-api.${DOMAIN}
        splunk-insecureskipverify: 'true'
        labels: whatever
</code></pre>
<p>And that should be all we need to do to get Splunk running with Traefik in Docker, at least on a local Mac OS machine.</p>
<p>A production Splunk instance will require significantly more setup than this though, as you'll want to ensure Splunk is secure and robust.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Edge Side Includes (ESIs) in React using higher-order components]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Although React is a great way of doing component-based UI development, when creating web applications that are somewhat complicated or need to perform at scale, we may find ourselves running into situations where React seems to be less than helpful.</p>
<p>One such situation that I've been working with recently is</p></div>]]></description><link>https://blog.mebooks.co.nz/react-edge-server-includes/</link><guid isPermaLink="false">5a095579c7843e0001a1003b</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Mon, 13 Nov 2017 08:42:19 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1493725017694-269175c9816a?ixlib=rb-0.3.5&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;s=7be9f2a9351ca08638314421b81031fd" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://images.unsplash.com/photo-1493725017694-269175c9816a?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&s=7be9f2a9351ca08638314421b81031fd" alt="Edge Side Includes (ESIs) in React using higher-order components"><p>Although React is a great way of doing component-based UI development, when creating web applications that are somewhat complicated or need to perform at scale, we may find ourselves running into situations where React seems to be less than helpful.</p>
<p>One such situation that I've been working with recently is getting our React application to support <a href="https://en.wikipedia.org/wiki/Edge_Side_Includes">Edge Side Includes</a>.</p>
<p>However, as I've discovered, with a little bit of work we can ensure our React application is able to gain the benefits of ESIs in an elegant and maintainable fashion.</p>
<h2 id="edgesideincludesesis">Edge Side Includes (ESIs)</h2>
<p>Edge Side Includes (aka ESIs) are a way of caching parts of the page so that effort is offloaded from your application to an edge server, using a service such as Akamai.</p>
<p>For example, consider a <code>&lt;CustomerList /&gt;</code> React component whose job is to render a list of customer firstnames; the rendered HTML for such a a component might look like the following:</p>
<pre><code>&lt;div&gt;
  &lt;ul&gt;
    &lt;li&gt;Jane&lt;/li&gt;
    &lt;li&gt;John&lt;/li&gt;
    &lt;li&gt;Abigail&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>
<p>The component JavaScript could look like the following:</p>
<pre><code>// customerList.js
export default class CustomerList extends React.Component {
  constructor(props) {
    super(props);

    return fetch('https://data.example.com/customer_list')
    .then((res) =&gt; res.json())
    .then((data) =&gt; {
      this.state = {
        data        
      };
    })
    .catch((error) =&gt; {
      console.log('Error: ', error)
    })
  }

  static name = 'CustomerList';

  render() {
    return (
      &lt;div&gt;
        &lt;ul&gt;
          { this.state.data.map((customer) =&gt; (
            &lt;li&gt;{ customer.firstname }&lt;/li&gt;
          )) }
        &lt;/ul&gt;
      &lt;/div&gt;
    );
  }
}
</code></pre>
<p>The important thing to grasp here is that such a list would require our application to fetch the customer firstnames each time it was called, and if our web application receives a lot of interest, this could result in many thousands of calls being made each minute to the data source to obtain this information.</p>
<p>Although one could argue that the application itself could cache such data internally, this is not really the job of the application, and can even become problematic when we run a cluster with multiple copies of our application (e.g. using Docker containers), as different nodes in the cluster could end up having different cached copies of the data, and the result rendered to the user would therefore depend on which node in the cluster served the page to them.</p>
<p>ESIs are very useful in large-scale web applications as not only can they reduce the load on the backend data source, they can also result in lower costs for serving traffic, as the cost of pages served via Edge Servers can be very cheap.</p>
<p>In order to implement an ESI, instead of the above HTML, our application would render an <code>esi:include</code> tag instead, e.g.</p>
<pre><code>&lt;esi:include src-&quot;https//api.example.com/CustomerList?field=html&quot; onerror=&quot;continue&quot; /&gt;
</code></pre>
<p>Then, when the Edge Server sees this tag, it will call to <a href="https://blog.mebooks.co.nz/react-edge-server-includes/http/api.example.com/CustomerList?field=html">http//api.example.com/CustomerList?field=html</a> to obtain the content, and replace the <code>&lt;esi:include /&gt;</code> with the retrieved content.</p>
<p>In terms of reducing the load on our data source, this of course would be pointless if the Edge Server had to perform this data fetch for every page that it processes; typically, the content being ESIed (<a href="https://blog.mebooks.co.nz/react-edge-server-includes/https/api.example.com/CustomerList?field=html">https//api.example.com/CustomerList?field=html</a>) will be served with a cache header indicating that the Edge Server can cache it for a certain period of time, and need not refetch it until this cache period expires.</p>
<h2 id="react">React</h2>
<p>Although it may not be obvious at first, ESIs pose a few problems for our React application.</p>
<h3 id="weneedacomponentapi">We need a component API</h3>
<p>The first is that our <code>&lt;CustomerList /&gt;</code> component content has to be able to be served as a separate bit of content, and not simply as part of the entire page that our application is rendering.</p>
<p>This means that our application needs to provide a separate API that can be queried for a given component, returning just the HTML (and possibly CSS) for this component.</p>
<p>This is actually reasonably simple to create, as the following illustrates:</p>
<pre><code>// componentServer.js
'use strict';

import React              from 'react';
import { renderToString } from 'react-dom/server';
import Helmet             from 'react-helmet';
import jp                 from 'jsonpath';

// create webpack context containing all components
let requireComponent = require.context(
  './', // context folder
  true, // include subdirectories
  /\/[^\.]+\.js$/ // RegExp
);

export default function(req, res) {
  let sendJson        = req.headers.accepts === 'application/json';
  let componentName   = req.params.componentName;
  let body            = req.body;
  let componentProps  = body.props || {};
  let returnField     = body.field;
  let Component;

  if (req.method === 'GET') {
    componentProps = req.query.props
      ? JSON.parse(req.query.props) 
      : componentProps;

    returnField = req.query.field;
  }

  if (typeof returnField !== 'string' &amp;&amp; returnField != void 0) {
    return sendError(res, 400, '&quot;field&quot; prop was not a string.', sendJson);
  }

  try {
    Component = requireComponent(`./${componentName}.js`);
  } catch (e) {
    console.error(`Component ${componentName} not found`); // eslint-disable-line no-console
  }

  if (!Component) {
    return sendError(res, 404, 'A component with that name does not exist.', sendJson);
  } else {
    let html;

    try {
      html = renderToString(
        &lt;Component {...componentProps} /&gt;
      );
    } catch (e) {
      console.log('Error:', e.message); // eslint-disable-line no-console
    }

    let header = Helmet.rewind();

    // payload header should only contain strings
    Object.keys(header)
    .forEach((attributeName) =&gt; {
      header[attributeName] = header[attributeName].toString();
    });

    let payload = {
      html,
      css: Component.css,
      header,
    };

    const contentTypes = {
      css: 'text/css',
    };

    if (returnField) {
      let field = jp.query(payload, returnField)[0];
      return res
      .type(contentTypes[returnField] || 'text/html')
      .status(200)
      .send(field)
      .end();
    }

    if (sendJson) {
      return res
      .status(200)
      .json(payload);
    }

    // return rendered HTML if JSON was not requested
    payload.state = JSON.stringify(payload.state);
    res
    .status(200)
    .render('component_api_layout', payload);
  }
};
</code></pre>
<p>Note that we can ask separately for either the html (<a href="https://blog.mebooks.co.nz/react-edge-server-includes/https/api.example.com/CustomerList?field=html">https//api.example.com/CustomerList?field=html</a>) or the css (<a href="https://blog.mebooks.co.nz/react-edge-server-includes/https/api.example.com/CustomerList?field=css">https//api.example.com/CustomerList?field=css</a>) of our component.</p>
<h3 id="weneedtothinkaboutourrendering">We need to think about our rendering</h3>
<p>Now that we have a means of allowing our Edge Server to fetch the content to replace the <code>&lt;esi:include /&gt;</code> tag, we have to ensure that our component renders appropriately (i.e. differently) in the various contexts in which it will be called:</p>
<ul>
<li>
<p>when called via our component API, we expect the component to render the full HTML and / or CSS</p>
</li>
<li>
<p>when called to render serverside via a call to our main application (i.e. when it's constructing the page of which our <code>&lt;CustomerList /&gt;</code> is one part), we expect our component to render an <code>&lt;esi:include /&gt;</code> tag, e.g <code>&lt;esi:include src-&quot;https//api.example.com/CustomerList&quot; onerror=&quot;continue&quot; /&gt;</code>, rather than rendering the actual HTML of the <code>&lt;CustomerList /&gt;</code></p>
</li>
<li>
<p>when called to render clientside once our page hits the browser, we expect out component to render the full HTML and / or CSS of our <code>&lt;CustomerList /&gt;</code></p>
</li>
</ul>
<p>The last of these requires some thinking about; we <em>WILL</em> be re-rendering clientside whether we like it or not, as the <code>data-react-checksum</code> generated during the server render (i.e. which took into account the serverside-rendered <code>&lt;esi:include /&gt;</code>) will be invalid clientside, as the <code>&lt;esi:include /&gt;</code> has been replaced on the page's journey to the browser with the real markup of our <code>&lt;CustomerList /&gt;</code> by the Edge Server.</p>
<p>As React detects that the <code>data-react-checksum</code> is no longer valid, it will re-render the DOM, and will call our <code>&lt;CustomerList /&gt;</code> to re-render clientside.</p>
<p>It's worth noting that, in the recently-released React 16, <code>data-react-checksum</code> <a href="https://reactjs.org/blog/2017/09/26/react-v16.0.html#better-server-side-rendering">is no longer used</a>, and therefore it <em>may</em> be possible to avoid the clientside re-render immediately that the page is loaded, but you'll still want to be able to cope with clientside re-rendering of the <code>&lt;CustomerList /&gt;</code> at some point.</p>
<h3 id="anexerciseinfutility">An exercise in futility?</h3>
<p>So, given that clientside our <code>&lt;CustomerList /&gt;</code> needs to render the full HTML, this makes the whole of the article to this point sound like an exercise in futility; what is the point of using an Edge Server to reduce the load on our data source from serverside calls by our application, if we simply end up calling the data source clientside from the browser?</p>
<p>The answer is that, clientside, we have to be a little smarter, in that we don't get our <code>&lt;CustomerList /&gt;</code> component to render the full HTML, but rather we get it to re-use the DOM that was injected by the Edge Server.</p>
<p>Thus, the only time that our <code>&lt;CustomerList /&gt;</code> component actually calls the data source and renders fresh HTML containing the firstnames of our customers is when the Edge Server calls our component via our new component API (i.e. <a href="https://blog.mebooks.co.nz/react-edge-server-includes/https/api.example.com/CustomerList?field=html">https//api.example.com/CustomerList?field=html</a>).</p>
<p>If we set caching headers on the <a href="https://blog.mebooks.co.nz/react-edge-server-includes/https/api.example.com/CustomerList?field=html">https//api.example.com/CustomerList?field=html</a> response for, say, 180 seconds, then our component will only be asked to do a data fetch every three minutes or so, rather than on every request.</p>
<h2 id="anesihigherordercomponent">An ESI higher-order component</h2>
<p>So, now that we've worked out the mechansim to get ESI content to appear, we need to implement it.</p>
<p>One naive way would be to amend each component we wish to ESI, such that it renders in different contexts according to the logic described above.</p>
<p>However, this means that, should we have many components that we wish to ESI, we will have to make similar changes in each component, and be prepared to change all of the components whenever we wish to make modifications.</p>
<p>A better approach is to use a <a href="https://reactjs.org/docs/higher-order-components.html">higher-order component</a>, which allows us to wrap our <code>&lt;CustomerList /&gt;</code> with the ESI logic, while not having to modify <code>&lt;CustomerList /&gt;</code> at all.</p>
<p>Our main <code>app</code> component would look somewhat like follows where, instead of rendering <code>&lt;CustomerList /&gt;</code>, it would now be rendering <code>&lt;WrappedCustomerList /&gt;</code>:</p>
<pre><code>// app.js 
import React               from 'react';
import WithEdgeSideInclude from 'withEdgeSideInclude';
import CustomerList        from 'customerList';

const WrappedCustomerList = WithEdgeSideInclude(CustomerList),

class Page extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      &lt;WrappedCustomerList /&gt;
    );
  })
}
</code></pre>
<p>In the above, we are using <code>&lt;WithEdgeSideInclude /&gt;</code> to wrap our <code>&lt;CustomerList /&gt;</code> component, and in doing so <code>&lt;WithEdgeSideInclude /&gt;</code> will be responsible for deciding whether <code>&lt;CustomerList /&gt;</code> renders its full HTML, or whether something else should be rendered instead.</p>
<p>Our <code>&lt;WithEdgeSideInclude /&gt;</code> higher-order component will look something like the following:</p>
<pre><code>// withEdgeSideInclude.js
'use strict';

import React  from 'react';
import config from 'config';

let SUPPRESS_ESI = config.suppressEsi;
let BUNDLE_TYPE = config.bundleType;

module.exports = function withEdgeServerInclude(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
    }

    static name = 'WithEdgeServerInclude';

    shouldComponentUpdate() {
      // Only allow rerendering if we're not using an ESI
      return ! SUPPRESS_ESI
        ? false
        : true;
    }

    getExistingHtml(selector, property) {
      const element = global.document.querySelector(selector);
      return element
        ? element[property]
        : null;
    }

    render() {
      // Don't re-render the included ESI client-side, but instead
      // re-use the edge-server-rendered DOM.
      if (BUNDLE_TYPE === 'client') {
        const css = this.getExistingHtml(`#${WrappedComponent.name}__styles`, 'innerHTML');
        const html = this.getExistingHtml(`.${WrappedComponent.name}`, 'outerHTML');

        if (html &amp;&amp; css) {
          return (
            &lt;div&gt;
              &lt;style
                id={`${WrappedComponent.name}__styles`}
                dangerouslySetInnerHTML={{ __html: css }}
              /&gt;
              &lt;div dangerouslySetInnerHTML={{ __html: html }} /&gt;
            &lt;/div&gt;
          );
        }
      }

      // Render an esi:include unless we're configured to suppress
      if (! SUPPRESS_ESI) {
        return (
          &lt;div&gt;
            &lt;style
              id={`${WrappedComponent.name}__styles`}
              dangerouslySetInnerHTML={{ __html: `&lt;esi:include src=&quot;https//api.example.com/${WrappedComponent.name}?field=css&quot; onerror=&quot;continue&quot; /&gt;` }}
            /&gt;
            &lt;div
              dangerouslySetInnerHTML={{ __html: `&lt;esi:include src=&quot;https//api.example.com/${WrappedComponent.name}?field=html&quot; onerror=&quot;continue&quot; /&gt;` }}
            /&gt;
          &lt;/div&gt;
        );
      }

      // Render the full component
      return (
        &lt;WrappedComponent {...this.props} /&gt;
      );
    }
  }
};
</code></pre>
<p>Note the following about <code>withEdgeSideInclude.js</code>:</p>
<ul>
<li>
<p>We use a config boolean, <code>SUPPRESS_ESI</code>, to determine whether we wish to render the full HTML of our wrapped component; this is helpful for local development, where we want to see the rendered HTML and not the <code>&lt;esi:include /&gt;</code> (as requests to localhost will not be routed via our Edge Server, and therefore any <code>&lt;esi:include /&gt;</code> will not be replaced by the full HTML).<br>
In this case, the relevant part of the render function is below:</p>
<pre><code>  // Render the full component
  return (
    &lt;WrappedComponent {...this.props} /&gt;
  );
</code></pre>
</li>
<li>
<p>If we are rendering serverside, we render the <code>&lt;esi:include &gt;s</code>. In this case, the relevant part of the render function is below; note that in this case we use two <code>&lt;esi:include &gt;s</code>, one for the CSS and one for the HTML.</p>
<pre><code>  // Render an esi:include unless we're configured to suppress
  if (! SUPPRESS_ESI) {
    return (
      &lt;div&gt;
        &lt;style
          id={`${WrappedComponent.name}__styles`}
          dangerouslySetInnerHTML={{ __html: `&lt;esi:include src=&quot;https//api.example.com/${WrappedComponent.name}?field=css&quot; onerror=&quot;continue&quot; /&gt;` }}
        /&gt;
        &lt;div
          dangerouslySetInnerHTML={{ __html: `&lt;esi:include src=&quot;https//api.example.com/${WrappedComponent.name}?field=html&quot; onerror=&quot;continue&quot; /&gt;` }}
        /&gt;
      &lt;/div&gt;
    );
  }
</code></pre>
</li>
<li>
<p>If we are rendering clientside, we re-use the HTML and CSS that was injected by the Edge Server.<br>
In this case, the relevant code is below:</p>
<pre><code>  getExistingHtml(selector, property) {
    const element = global.document.querySelector(selector);
    return element
      ? element[property]
      : null;
  }

  ...

  // Don't re-render the included ESI client-side, but instead
  // re-use the edge-server-rendered DOM.
  if (BUNDLE_TYPE === 'client') {
    const css = this.getExistingHtml(`#${WrappedComponent.name}__styles`, 'innerHTML');
    const html = this.getExistingHtml(`.${WrappedComponent.name}`, 'outerHTML');

    if (html &amp;&amp; css) {
      return (
        &lt;div&gt;
          &lt;style
            id={`${WrappedComponent.name}__styles`}
            dangerouslySetInnerHTML={{ __html: css }}
          /&gt;
          &lt;div dangerouslySetInnerHTML={{ __html: html }} /&gt;
        &lt;/div&gt;
      );
    }
  }
</code></pre>
</li>
</ul>
<p>Note that in the clientside rendering, we grab the DOM elements that were injected by the Edge Server, and simply render these using <code>dangerouslySetInnerHTML</code>, rather than render them fresh.<br>
In this way we avoid a clientside call to our data source.</p>
<h2 id="conclusion">Conclusion</h2>
<p>As the above examples have outlined, we can gain the substantial benefits of ESIs in React applications with two particular modifications to our application:</p>
<ul>
<li>
<p>a component API, to allow the Edge Server to retrieve the HTML and CSS for a specific component</p>
</li>
<li>
<p>a <code>&lt;WithEdgeSideInclude /&gt;</code> higher-order component, which wraps the component we wish to ESI, and determines whether the wrapped component should be allowed to do the data fetch and render its HTML, or whether an <code>&lt;esi:include /&gt;</code> should be rendered (serverside) or whether the Edge Server injected DOM should be re-used (clientside).</p>
</li>
</ul>
<p>Although, at least in React 15, we can't easily avoid the clientside rerender caused by the invalid <code>data-react-checksum</code>, we can ensure that we don't lose the benefits of ESIs during the clientside rerender.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Running Wordpress using Kubernetes locally]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Our aim with this post is to describe the process to run <a href="https://kubernetes.io/">Kubernetes</a> locally, by demonstrating running MySQL and Wordpress on the Kubernetes cluster.</p>
<p>We won't explain what Kubernetes is, suffice to say it is the container orchestration solution that everyone seems to be leaning towards; normally you'd want to</p></div>]]></description><link>https://blog.mebooks.co.nz/running-wordpress-using-kubernetes/</link><guid isPermaLink="false">59d99bfcc7843e0001a10038</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Sun, 08 Oct 2017 04:58:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1465844880937-7c02addc633b?ixlib=rb-0.3.5&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;s=fb819ffb4b19817d0ec5b37f89d13265" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="https://images.unsplash.com/photo-1465844880937-7c02addc633b?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&s=fb819ffb4b19817d0ec5b37f89d13265" alt="Running Wordpress using Kubernetes locally"><p>Our aim with this post is to describe the process to run <a href="https://kubernetes.io/">Kubernetes</a> locally, by demonstrating running MySQL and Wordpress on the Kubernetes cluster.</p>
<p>We won't explain what Kubernetes is, suffice to say it is the container orchestration solution that everyone seems to be leaning towards; normally you'd want to run up a Kubernetes cluster on a cloud provider such as AWS or GCE, however for the purposes of training / evaluation, we run it up locally using a cluster of Vagrant virtual machines.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>We assume we're on Mac OSX, though the <code>Vagrantfile</code> is OS-agnostic, so you should be able to get things working on both Windows and Linux relatively easily.</p>
<p>We need to install a few things, and we'll do so using the <a href="https://brew.sh/">brew package manager</a>:</p>
<pre><code class="language-bash">brew install vagrant
brew install kubectl
brew install wget
</code></pre>
<h2 id="creatingthekubernetescluster">Creating the Kubernetes cluster</h2>
<p>Our first step is to clone the <a href="https://github.com/pires/kubernetes-vagrant-coreos-cluster">kubernetes-vagrant-coreos-cluster</a> repo, which bills itself as a:</p>
<blockquote>
<p>Turnkey Kubernetes cluster setup with Vagrant 1.8+ and CoreOS.</p>
</blockquote>
<p>This project provides a comprehensive <code>Vagrantfile</code> which will, by default, run up a cluster of three CoreOS Vagrant virtual machines.</p>
<pre><code class="language-bash"># clone the repo
git clone https://github.com/pires/kubernetes-vagrant-coreos-cluster.git
cd kubernetes-vagrant-coreos-cluster
</code></pre>
<p>We can then <code>vagrant up</code> the cluster:</p>
<pre><code class="language-bash"># remove Virtualbox's DHCP server
VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0

# run up the cluster, specifying that we want the web ui
USE_KUBE_UI=true vagrant up
</code></pre>
<p>Note that we specify that we want the web-based user interface; other settings are described in the project's <a href="https://github.com/pires/kubernetes-vagrant-coreos-cluster">README.md</a>, but do take a look through the <a href="https://github.com/pires/kubernetes-vagrant-coreos-cluster/blob/master/Vagrantfile">Vagrantfile</a> to get a better idea about what values can be supplied.</p>
<p>The cluster startup will likely take some time, as vagrant will most likely need to pull a number of images; the Vagrantfile does allow the following providers, though we're using Virtualbox here:</p>
<ul>
<li><strong><a href="https://www.virtualbox.org">Virtualbox</a></strong> (the default)</li>
<li><strong><a href="http://www.parallels.com/eu/products/desktop/">Parallels Desktop</a></strong></li>
<li><strong><a href="http://www.vmware.com/products/fusion">VMware Fusion</a></strong> or <strong><a href="http://www.vmware.com/products/workstation">VMware Workstation</a></strong></li>
</ul>
<p>Once Vagrant has pulled the necessary VM images, you should be able to inspect them at:</p>
<pre><code class="language-bash"># display the virtualbox image directories
ls -la ~/VirtualBox\ VMs/


# display the uncompressed sizes of the virtualbox image directories
du -sh ~/VirtualBox\ VMs/*

2.8G	/Users/jasondarwin/VirtualBox VMs/kubernetes-vagrant-coreos-cluster_master_1507421853719_25399
4.4G	/Users/jasondarwin/VirtualBox VMs/kubernetes-vagrant-coreos-cluster_node-01_1507422227192_50843
2.7G	/Users/jasondarwin/VirtualBox VMs/kubernetes-vagrant-coreos-cluster_node-02_1507422346442_20718
</code></pre>
<h2 id="inspectingthecluster">Inspecting the cluster</h2>
<p>Once the cluster has been created, we can then get some information about it.</p>
<p>We'll be using <code>kubectl</code>, and there's <a href="https://kubernetes.io/docs/user-guide/kubectl-cheatsheet/">a cheatsheet</a> that lists the useful <code>kubectl</code> commands.</p>
<pre><code class="language-bash"># view the three nodes using vagrant
vagrant status

Current machine states:

master                    running (virtualbox)
node-01                   running (virtualbox)
node-02                   running (virtualbox)


# get info about our cluster
kubectl cluster-info

Kubernetes master is running at https://172.17.8.101
KubeDNS is running at https://172.17.8.101/api/v1/namespaces/kube-system/services/kube-dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.


# display nodes
kubectl get nodes

NAME           STATUS                     AGE       VERSION
172.17.8.101   Ready,SchedulingDisabled   3h        v1.7.5
172.17.8.102   Ready                      3h        v1.7.5
172.17.8.103   Ready                      3h        v1.7.5
</code></pre>
<p>At this point we might like to ssh into our cluster nodes and have a look around; we can either use <code>vagrant ssh</code> or we can <code>ssh</code> directly by referring to the vagrant <code>insecure_private_key</code>:</p>
<pre><code class="language-bash"># ssh into the master
vagrant ssh master

# ssh directly into a machine
ssh -i ~/.vagrant.d/insecure_private_key core@172.17.8.101
</code></pre>
<h2 id="thekubernetesdashboard">The Kubernetes dashboard</h2>
<p>During the creation of the cluster, you'll see messages regarding the creation of the <code>kubernetes-dashboard</code>:</p>
<pre><code class="language-bash">==&gt; master: Configuring Kubernetes dashboard...
deployment &quot;kubernetes-dashboard&quot; created
service &quot;kubernetes-dashboard&quot; created
==&gt; master: Kubernetes dashboard will be available at http://172.17.8.101:8080/ui
</code></pre>
<p>Visting the specified URL (in our case <a href="http://172.17.8.101:8080/ui">http://172.17.8.101:8080/ui</a>) will present you with the dashboard:</p>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1507435152/kubernetes-ui_wygi6s.png" alt="Running Wordpress using Kubernetes locally"></p>
<p>Note that visiting the base URL (<a href="http://172.17.8.101:8080">http://172.17.8.101:8080</a>) will present a JSON file listing all exposed endpoints, including some very handy ones:</p>
<pre><code class="language-json">{
    &quot;paths&quot;: [
        &quot;/api&quot;,
        &quot;/api/v1&quot;,
        &quot;/apis&quot;,
        &quot;/apis/&quot;,
        &quot;/apis/admissionregistration.k8s.io&quot;,
        &quot;/apis/admissionregistration.k8s.io/v1alpha1&quot;,
        &quot;/apis/apiextensions.k8s.io&quot;,
        &quot;/apis/apiextensions.k8s.io/v1beta1&quot;,
        &quot;/apis/apiregistration.k8s.io&quot;,
        &quot;/apis/apiregistration.k8s.io/v1beta1&quot;,
        &quot;/apis/apps&quot;,
        &quot;/apis/apps/v1beta1&quot;,
        &quot;/apis/authentication.k8s.io&quot;,
        &quot;/apis/authentication.k8s.io/v1&quot;,
        &quot;/apis/authentication.k8s.io/v1beta1&quot;,
        &quot;/apis/authorization.k8s.io&quot;,
        &quot;/apis/authorization.k8s.io/v1&quot;,
        &quot;/apis/authorization.k8s.io/v1beta1&quot;,
        &quot;/apis/autoscaling&quot;,
        &quot;/apis/autoscaling/v1&quot;,
        &quot;/apis/batch&quot;,
        &quot;/apis/batch/v1&quot;,
        &quot;/apis/batch/v2alpha1&quot;,
        &quot;/apis/certificates.k8s.io&quot;,
        &quot;/apis/certificates.k8s.io/v1beta1&quot;,
        &quot;/apis/extensions&quot;,
        &quot;/apis/extensions/v1beta1&quot;,
        &quot;/apis/networking.k8s.io&quot;,
        &quot;/apis/networking.k8s.io/v1&quot;,
        &quot;/apis/policy&quot;,
        &quot;/apis/policy/v1beta1&quot;,
        &quot;/apis/rbac.authorization.k8s.io&quot;,
        &quot;/apis/rbac.authorization.k8s.io/v1alpha1&quot;,
        &quot;/apis/rbac.authorization.k8s.io/v1beta1&quot;,
        &quot;/apis/settings.k8s.io&quot;,
        &quot;/apis/settings.k8s.io/v1alpha1&quot;,
        &quot;/apis/storage.k8s.io&quot;,
        &quot;/apis/storage.k8s.io/v1&quot;,
        &quot;/apis/storage.k8s.io/v1beta1&quot;,
        &quot;/healthz&quot;,
        &quot;/healthz/autoregister-completion&quot;,
        &quot;/healthz/ping&quot;,
        &quot;/healthz/poststarthook/apiservice-registration-controller&quot;,
        &quot;/healthz/poststarthook/apiservice-status-available-controller&quot;,
        &quot;/healthz/poststarthook/bootstrap-controller&quot;,
        &quot;/healthz/poststarthook/ca-registration&quot;,
        &quot;/healthz/poststarthook/extensions/third-party-resources&quot;,
        &quot;/healthz/poststarthook/generic-apiserver-start-informers&quot;,
        &quot;/healthz/poststarthook/kube-apiserver-autoregistration&quot;,
        &quot;/healthz/poststarthook/start-apiextensions-controllers&quot;,
        &quot;/healthz/poststarthook/start-apiextensions-informers&quot;,
        &quot;/healthz/poststarthook/start-kube-aggregator-informers&quot;,
        &quot;/healthz/poststarthook/start-kube-apiserver-informers&quot;,
        &quot;/logs&quot;,
        &quot;/metrics&quot;,
        &quot;/swagger-2.0.0.json&quot;,
        &quot;/swagger-2.0.0.pb-v1&quot;,
        &quot;/swagger-2.0.0.pb-v1.gz&quot;,
        &quot;/swagger.json&quot;,
        &quot;/swaggerapi&quot;,
        &quot;/ui&quot;,
        &quot;/ui/&quot;,
        &quot;/version&quot;
    ]
}
</code></pre>
<h2 id="installingmysqlandwordpress">Installing MySQL and Wordpress</h2>
<p>Once our cluster is running, we then need to use it; for this demonstration we'll run up MySQL and Wordpress as two separate services.</p>
<p>We're working from the <a href="https://github.com/kubernetes/kubernetes/tree/master/examples/mysql-wordpress-pd">Kubernetes example using MySQL and Wordpress</a>, which is described in detail <a href="https://kubernetes.io/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/">on the Kubernetes site as a tutorial</a>, though we did find we needed to make a few tweaks to get everything working.</p>
<p>Create our directory and download the yaml files:</p>
<pre><code class="language-bash">mkdir mysql-wordpress
cd mysql-wordpress

wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/examples/mysql-wordpress-pd/local-volumes.yaml
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/examples/mysql-wordpress-pd/mysql-deployment.yaml
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/examples/mysql-wordpress-pd/wordpress-deployment.yaml
</code></pre>
<p>Amend local-volumes.yaml to add <code>annotations</code>, otherwise we get problems with Kubernetes not being able to connect the persistent volumes:</p>
<pre><code class="language-yaml"># Before:
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
labels:
    app: wordpress


# After:
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
annotations:
    volume.beta.kubernetes.io/storage-class: &quot;&quot;
labels:
    app: wordpress
</code></pre>
<p>Create our local persistent volume mounts.</p>
<p>Note: Any other Type of PersistentVolume would allow you to recreate the Deployments and Services at this point without losing data, but <code>hostPath</code> loses the data as soon as the Pod stops running. However, for the purposes of this demonstration, we're not worried about persisting data.</p>
<pre><code class="language-bash">mkdir -p /tmp/data/pv-1
mkdir -p /tmp/data/pv-2
</code></pre>
<p>Create two PersistentVolumes from the <code>local-volumes.yaml</code> file:</p>
<pre><code class="language-bash">kubectl create -f local-volumes.yaml

# display the persistent volumes:
kubectl get pv

NAME         CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                    STORAGECLASS   REASON    AGE
local-pv-1   20Gi       RWO           Retain          Bound     default/wp-pv-claim                               1h
local-pv-2   20Gi       RWO           Retain          Bound     default/mysql-pv-claim                            1h
</code></pre>
<h3 id="creatingsecretsformysqlpassword">Creating secrets for MySQL Password</h3>
<p>The Kubernetes site has <a href="https://kubernetes.io/docs/concepts/configuration/secret/">documentation about handling secrets</a>; in our case we need to handle the MySQL password.</p>
<p>One option is to create a secret in a password file:</p>
<pre><code class="language-bash">echo -n &quot;WHATEVER&quot; &gt; ./password.txt
kubectl create secret generic mysql-pass --from-file=password.txt

# Display the existence of the secret
kubectl get secrets
</code></pre>
<p>In the <code>mysql-deployment.yaml</code> we'll then need to ensure the container spec is set accordingly:</p>
<pre><code class="language-yaml"># mysql-deployment.yaml
- name: MYSQL_ROOT_PASSWORD
  valueFrom:
    secretKeyRef:
      name: mysql-pass
      key: password.txt
</code></pre>
<p>Alternatively, we can create a literal secret, which is supplied directly on the command line:</p>
<pre><code class="language-bash">
# kubectl create secret generic mysql-pass --from-literal=password=WHATEVER

# Display the existence of the secret
kubectl get secrets
</code></pre>
<pre><code class="language-yaml"># mysql-deployment.yaml
- name: MYSQL_ROOT_PASSWORD
  valueFrom:
    secretKeyRef:
      name: mysql-pass
      key: password
</code></pre>
<h2 id="deploymysql">Deploy MySQL</h2>
<p>At this point we're ready to deploy MySQL:</p>
<pre><code class="language-bash"># deploy MySQL
kubectl create -f mysql-deployment.yaml

# Show the pod as deployed
kubectl get pods

NAME                               READY     STATUS    RESTARTS   AGE
wordpress-mysql-2917821887-m4cgc   1/1       Running   0          1h

# Describe the deployment, including the assigned persistent volume
kubectl describe -f mysql-deployment.yaml
</code></pre>
<p>Find the node running MySQL via the pods in the Kubernetes UI; amongst other things, it will indicate which node the service is running on (172.17.8.103 in this case):</p>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1507436906/kubernetes-ui-mysql_srxgve.png" alt="Running Wordpress using Kubernetes locally"></p>
<h2 id="inspectmysql">Inspect MySQL</h2>
<pre><code class="language-bash"># ssh into the node
ssh -i ~/.vagrant.d/insecure_private_key core@172.17.8.103


# find the running mysql container
docker ps -l

CONTAINER ID        IMAGE                                             COMMAND                  CREATED             STATUS              PORTS               NAMES
0c24ee28b333        mysql                                             &quot;docker-entrypoint...&quot;   29 seconds ago      Up 29 seconds                           k8s_mysql_wordpress-mysql-2917821887-r67gk_default_c269b585-abcb-11e7-bd0b-08002751ec84_0


# docker exec into the running mysql container
# and run a interactive bash shell
docker exec -it $(docker ps -ql) /bin/bash


# login to mysql with the previously-configured password
mysql -u root -p
</code></pre>
<h3 id="rollingback">Rolling back</h3>
<p>If you encounter problems, it's quite easy to rollback the deployment:</p>
<pre><code class="language-bash"># if you need to rollback:
kubectl delete service wordpress-mysql
kubectl delete deployment wordpress-mysql
kubectl delete pvc mysql-pv-claim
</code></pre>
<p>If you get really stuck, you can also delete the local persistant volume mounts, though you'll need to then recreate them if you want to re-deploy:</p>
<pre><code class="language-bash"># Delete the local pv mounts
kubectl delete pv local-pv-1 local-pv-2

# Receate the pv mounts using local-volumes.yaml file:
kubectl create -f local-volumes.yaml

# Redeploy MySQL
kubectl create -f mysql-deployment.yaml
</code></pre>
<h2 id="deploywordpress">Deploy Wordpress</h2>
<p>Deploying Wordpress is quite similar</p>
<pre><code class="language-bash"># deploy wordpress
kubectl create -f wordpress-deployment.yaml


# Show the pod as deployed
kubectl get pods

NAME                               READY     STATUS    RESTARTS   AGE
wordpress-559664747-h22qq          1/1       Running   0          1h
wordpress-mysql-2917821887-m4cgc   1/1       Running   0          1h


# Describe the deployment, including the assigned persistent volume
kubectl describe -f wordpress-deployment.yaml
</code></pre>
<p>Check the Kubernetes UI to ensure that the Wordpress pod has deployed successfully.</p>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1507437520/kubernetes-ui-wordpress_eoz2cg.png" alt="Running Wordpress using Kubernetes locally"></p>
<p>We need to take note of the <code>Cluster IP</code> assigned to Wordpress, which can be seen on the UI under <code>Services</code>, or can be found via <code>kubectl</code>:</p>
<pre><code class="language-bash"># find the internal ip
kubectl get services wordpress

NAME        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
wordpress   10.100.111.132   &lt;pending&gt;     80:32367/TCP   5m
</code></pre>
<p>Note that the Wordpress service will be shown as <code>pending</code>; this is nothing to worry about.</p>
<p>We'll also need to take note from the Kubernetes UI of Node that it's been assigned to; in our case it was <code>172.17.8.102</code>.</p>
<h2 id="inspectwordpress">Inspect Wordpress</h2>
<pre><code class="language-bash"># ssh into the node
ssh -i ~/.vagrant.d/insecure_private_key core@172.17.8.102

# then curl the page using the CLUSTER-IP from
# 'kubectl get services wordpress'.
# Note we follow the redirect
curl -i -L 10.100.111.132

# lots of HTML then follows...
</code></pre>
<p>Use the node ip, and the port reported in <code>kubectl get services wordpress</code> to get to the wordpress install screen in a browser; in our case the URL is <a href="http://172.17.8.102:32367">http://172.17.8.102:32367</a></p>
<p>You should then see the familiar Wordpress install screen:</p>
<p><img src="https://raw.githubusercontent.com/kubernetes/examples/master/mysql-wordpress-pd/WordPress.png" alt="Running Wordpress using Kubernetes locally"></p>
<h3 id="rollingback">Rolling back</h3>
<p>If you encounter problems, it's quite easy to rollback the deployment:</p>
<pre><code class="language-bash"># if you need to rollback:
kubectl delete deployment wordpress
kubectl delete service wordpress
kubectl delete pvc wp-pv-claim
</code></pre>
<p>If you get really stuck, you can also delete the MySQL deployment and the local persistent volume mounts, though you'll need to then recreate them if you want to re-deploy:</p>
<pre><code class="language-bash"># Delete the MySQL deployment
kubectl delete service wordpress-mysql
kubectl delete deployment wordpress-mysql
kubectl delete pvc mysql-pv-claim

# Delete the local pv mounts
kubectl delete pv local-pv-1 local-pv-2

# Receate the pv mounts using local-volumes.yaml file:
kubectl create -f local-volumes.yaml

# Redeploy MySQL
kubectl create -f mysql-deployment.yaml

# Redeploy Wordpress
kubectl create -f wordpress-deployment.yaml
</code></pre>
<h2 id="cleanup">Cleanup</h2>
<p>Once you tire of your kubernetes cluster, cleanup is quite straightforward:</p>
<pre><code class="language-bash"># Destroy the vagrant cluster
vagrant destroy

# Delete the local persistent volumes
kubectl delete pv local-pv-1 local-pv-2

# Remove the local mounts
rm -rf /tmp/data/pv-1/
rm -rf /tmp/data/pv-2/
</code></pre>
<h2 id="summary">Summary</h2>
<p>There are plenty of other <a href="https://github.com/kubernetes/kubernetes/tree/master/examples">Kubernetes examples</a> to play around with and plenty of other <a href="https://kubernetes.io/docs/tasks/">tutorials on the Kubernetes site</a>, and possibly in a future post we'll look at deployment on a cloud service such as AWS.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Building Docker images on EC2 instances]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/e_brightness:-23/v1468038577/sky-sunset-clouds-blue-5644_cropped_tgv4i9.jpg) -->
<p>Building Docker images on EC2 instances rather than on a local machine makes a lot of sense.</p>
<p>You get the benefits of a consistent build environment, as well as a typically much-faster network connection than you often enjoy locally.</p>
<p>However, there are some gotchas, and we look at these below.</p></div>]]></description><link>https://blog.mebooks.co.nz/building-docker-images-on-ec2-instances/</link><guid isPermaLink="false">59ceded9c7843e0001a0ffc3</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Sat, 09 Jul 2016 00:01:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1495277493816-4c359911b7f1?ixlib=rb-0.3.5&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;s=0807db907d6d13257cb3d7087442eab6" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/e_brightness:-23/v1468038577/sky-sunset-clouds-blue-5644_cropped_tgv4i9.jpg) -->
<img src="https://images.unsplash.com/photo-1495277493816-4c359911b7f1?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&s=0807db907d6d13257cb3d7087442eab6" alt="Building Docker images on EC2 instances"><p>Building Docker images on EC2 instances rather than on a local machine makes a lot of sense.</p>
<p>You get the benefits of a consistent build environment, as well as a typically much-faster network connection than you often enjoy locally.</p>
<p>However, there are some gotchas, and we look at these below.</p>
<h2 id="determiningthedockerpath">Determining the Docker path</h2>
<p>If you've been using <a href="https://docs.docker.com/engine/installation/mac/#/docker-for-mac">Docker for Mac</a>, then you've probably been using Docker at:</p>
<pre><code class="language-bash">/usr/local/bin/docker
</code></pre>
<p>However, when moving to a Linux box, Docker instead is to be found at:</p>
<pre><code class="language-bash">/usr/bin/docker
</code></pre>
<h2 id="settingdockerstorageonec2">Setting Docker storage on EC2</h2>
<p>Another particular problem that I've run across is ensuring that there's enough space for building large Docker images.</p>
<p>We use an <a href="https://github.com/jcdarwin/ansible-role-ec2">ansible role to start an EC2 instance</a>, and then push, pull and build Docker images using this EC2 instance.</p>
<p>Using our ansible role, we spin up an m3.large instance, particularly because we want the 32GB SSD that it comes with.</p>
<p>However, when we install Docker on the instance, the default setting for storage is in <code>/var/lib/docker</code>, whereas our 32GB SDD storage is mounted at <code>/mnt</code>.</p>
<p>On an <code>m3.large</code> the <code>/dev/xvda1</code> mount <code>/</code> has a size of 7.8GB, which is not enough for some of the images we'll be building: ideally, Docker images should be kept as small as possible, however, we need to build some rather large monolithic applications, so we really need the space at <code>/mnt</code>.</p>
<p>This is not a problem when running a container from an image, as we can simply map the volume:</p>
<pre><code class="language-bash">-v /mnt/docker/data:/root/monolithic-app-storage \
</code></pre>
<p>However, when building an image, although we can specify the volume in the Dockerfile, we can't map it to a location on disk, and therefore are reliant on there being enough disk space where the Docker images are stored.</p>
<p>Theoretically, we can simply change the <code>$DOCKER_OPTS</code> and restart docker:</p>
<pre><code class="language-bash">mkdir -p /mnt/docker/data
export DOCKER_OPTS=&quot;-g /mnt/docker&quot;
service docker restart
</code></pre>
<p>If this worked, we should see our <code>$DOCKER_OPTS</code> when we do the following:</p>
<pre><code class="language-bash">ps -aux | grep docker
</code></pre>
<p>However, we don't, and this has been a problem that others have run across, such as <a href="http://stackoverflow.com/questions/24309526/how-to-change-the-docker-image-installation-directory">here</a> and <a href="http://stackoverflow.com/questions/30127580/docker-opts-in-etc-default-docker-ignored/30219552#30219552">here</a>.</p>
<p>One answer (though possibly not the best), is to edit <code>/lib/systemd/system/docker.service</code> and add our desired storage location to the <code>ExecStart</code> variable:</p>
<pre><code class="language-bash">ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS --graph /mnt/docker
</code></pre>
<p>When we look, we then see our mount point used for Docker:</p>
<pre><code class="language-bash">ps -aux | grep docker

root     16939 11.0  0.5 540896 42388 ?        Ssl  04:18   2:40 /usr/bin/docker daemon -H fd:// --graph /mnt/docker
</code></pre>
<p>Note that if you've already pulled or built images, you'll either need to re-do this, or <code>save</code> and then <code>load</code> them.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Accelerated Mobile Pages: Under the Hood]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1460029931/water-yellow-cold-plant_cropped_llmzaf.jpg) -->
<h2 id="ampmakingthewebgreatagain">AMP: Making the web great again</h2>
<p>Since launching in October 2015, Google's <a href="https://www.ampproject.org/">Accelerated Mobile Pages</a> (AMP) has quickly been building up momentum, with many of the major news publishers now on board.</p>
<p>AMP is an approach for making content-based websites fast to load, typically less than a second, and for</p></div>]]></description><link>https://blog.mebooks.co.nz/accelerated-mobile-pages-under-the-hood/</link><guid isPermaLink="false">59d05f84c7843e0001a0ffda</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Mon, 04 Jul 2016 04:22:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/water-yellow-cold-plant_cropped_llmzaf.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1460029931/water-yellow-cold-plant_cropped_llmzaf.jpg) -->
<h2 id="ampmakingthewebgreatagain">AMP: Making the web great again</h2>
<img src="https://blog.mebooks.co.nz/content/images/2017/10/water-yellow-cold-plant_cropped_llmzaf.jpg" alt="Accelerated Mobile Pages: Under the Hood"><p>Since launching in October 2015, Google's <a href="https://www.ampproject.org/">Accelerated Mobile Pages</a> (AMP) has quickly been building up momentum, with many of the major news publishers now on board.</p>
<p>AMP is an approach for making content-based websites fast to load, typically less than a second, and for many AMP-powered pages, almost instantly.</p>
<p>AMP can be regarded as similar in performance to <a href="https://instantarticles.fb.com/">Facebook Instant Articles</a>, though with the important distinction of allowing publishers implementing AMP to control the publishing of their own content, unlike Facebook Instant Articles.</p>
<p>AMP is <a href="https://github.com/ampproject/amphtml">open-source</a>, and the AMP team have been working with many of the third-party providers typically found on content sites (i.e. advertising, analytics, video etc) to ensure that third-party content loads fast on pages that employ the AMP approach.</p>
<p>For this post, we'll assume that you have a general understanding of how AMP works; if you haven't yet seen AMP in action, check the <a href="https://www.ampproject.org/">video on the AMP project site</a>, the animated gif below, or <a href="http://bit.ly/amp-obama">see the real thing in action</a> (you'll need to view this link on a mobile device, or in a mobile-sized viewport in dev tools).</p>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1459844865/amp_pd1ijg.gif" alt="Accelerated Mobile Pages: Under the Hood"></p>
<h2 id="weneeddiscipline">We need Discipline</h2>
<p>For web developers, AMP can be thought of as a  way of enforcing discipline for good practice in making sites fast to load.</p>
<p>If you're a web developer, you can probably empathise with the image below.</p>
<p><img src="https://cdn-images-1.medium.com/max/800/1*XHyAO1Rd7Ged9NXRIarFtQ.png" alt="Accelerated Mobile Pages: Under the Hood"></p>
<p><em>Familiar with this?</em></p>
<p><a href="https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/">Source</a> has a great article providing reactions from web developers in the online news space, which includes some interesting allusions to the problems of guaranteeing good performance (in terms of speed) of news websites (my italics).</p>
<blockquote>
<p>“I’m not optimistic that there is a business conversation around performance. I think we talk about a lot, as journalists, because we’re plugged into the trends and we love talking about ourselves. But I’m not convinced that news management is paying attention, or if they’re willing to make it a priority. I still have a lot of conversations where speed gets lip service, but we still cut “just this one” corner for business benefit X. <em>Cut enough corners, and eventually you’re right back where you started.</em></p>
</blockquote>
<blockquote>
<p>Like so many things that we think are technical problems, <em>these are actually questions of political will and support</em>. If AMP succeeds at all ... it’ll be because its hardline stance on performance and third-party code can provide arguments for that leadership to advance.”</p>
</blockquote>
<p><a href="https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/">https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/</a></p>
<p>Importantly, AMP enforces discipline in two respects.</p>
<p>Firstly, development teams implementing AMP sites have to work according to the restrictions that AMP specifies. We'll look at those shortly, but suffice to say that AMP limits the options for teams to make make decisions that adversely affect performance, and provides an easy way to implement many of the measures that should be considered good practice when looking to make fast loading websites.</p>
<blockquote>
<p>“AMP is basically web performance best practices dressed up as a file format.</p>
</blockquote>
<blockquote>
<p>That’s a very clever solution to what is, at heart, a cultural problem: when management (in one form or another) comes to the CMS team at a news organization and asks to add more junk to the site, saying “we can’t do that because AMP” is a much more powerful argument than trying to explain why a pop-over “Like us on Facebook!” modal is driving our readers to drink.”</p>
</blockquote>
<p><a href="https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/">https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/</a></p>
<p>Secondly, AMP enforces discipline regarding the performance of third-party code and content; development teams trying to produce fast websites are often hamstrung because of their lack of control over the performance of third-party code and content that the business mandates for inclusion in their pages. Lack-lustre performance from online advertisers, analytics providers, and video providers can often significantly contribute to the less than stellar loading peformance of many web pages, and can be at least partly blamed for the rise in interest in ad-blockers.</p>
<p>The AMP project have been working with many of the leading third-party providers to get them to serve their content via AMP-friendly mechanisms, and these third-party providers include the big names such as Adobe, Brightcove, Comscore, Chartbeat, Double-click. This improvement in performance of content from these providers can probably be regarded as the single most important achievement of the AMP project to date, as it addresses the major area that is beyond the control of the development teams producing news sites.</p>
<p>As one of the AMP team members, Malte Ubl, <a href="https://medium.com/@cramforce/2016-will-be-the-year-of-concurrency-on-the-web-c39b1e99b30f#.h3tvs71e5">expressed it</a>:</p>
<blockquote>
<p>For AMP I was like: “What if we instrument pages just once and allow configuring the collected data to be sent to N analytics providers?”</p>
</blockquote>
<blockquote>
<p>So, we went and talked to many of them. And every single one was like: “That sounds like a great idea”.</p>
</blockquote>
<blockquote>
<p>With this launching really soon in AMP, if you have 15 analytics providers your page will be exactly as heavy as if you only had one.</p>
</blockquote>
<h2 id="whatampisnot">What AMP is not</h2>
<h3 id="notasolutionforeverysite">Not a solution for every site</h3>
<p>AMP is not a universal panacea: it's only really applicable to templated content-based sites, and not really suitable for hand-crafted sites, which could be actually faster than AMP if done correctly.</p>
<p>Interestingly, the AMP team found that, of the major news organisations they partnered with to launch AMP, all but one reported that the AMP version of their content loaded in the browser significantly faster than the non-AMP version, with load times often improved by 50%-100% or more.</p>
<p>The one news organisaton whose AMP site actually performed 15% slower than their normal site was The Guardian, who everyone I think agrees were already doing a pretty good job of optimising their site before AMP appeared.</p>
<h3 id="notagreatmechanismforcustomcontent">Not a great mechanism for custom content</h3>
<p>AMP's focus is on producing a visual mobile experience that's leaner in some respects than it could be otherwise, and this means that it's not well suited to producing graphic one-off content, such as snow-falls, interactives and infographics.</p>
<p>As <a href="https://twitter.com/thomaswilburn">Thomas Wilburn</a> from the Seattle Times <a href="https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/">mentioned in the Source article</a>:</p>
<blockquote>
<p>“The interactives team at the Seattle Times probably won’t use AMP directly at all. Our whole mission is to take advantage of everything a browser can do to tell stories–we’re already using best practices for fast pages, and almost everything we do requires custom JavaScript. But our stuff can’t go in our existing CMS either: we’ve had to find workarounds like responsive iframes or just hosting our own pages wholesale.</p>
</blockquote>
<blockquote>
<p>AMP isn’t really aimed at content like ours, which is a splashy but tiny percentage of everything the news industry puts out. It’s aimed at the article pages that IT departments write, where all our standard text content goes. Because generally, those pages suck.”</p>
</blockquote>
<p>That said, there is at <a href="https://amphtml.wordpress.com/2016/02/25/rolling-out-the-red-carpet-for-interactives-in-amp/">least one example from Google</a> where an interactive treatment was achieved within the AMP restrictions, making heavy use of iframes to present the interactive elements.</p>
<h3 id="ampisgearedtowardscontentgenerators">AMP is geared towards content generators</h3>
<p>Given their function in being a large producer of the sort of templated content that the AMP team are targetting, it should be no surprise that there is <a href="http://wptavern.com/automattic-adds-amp-support-to-wordpress-com-releases-plugin-for-self-hosted-sites">already support for AMP on Wordpress.com</a>, as well as a <a href="http://www.techrepublic.com/article/how-to-speed-mobile-page-load-times-with-amp-and-wordpress/">plugin allowing those self-hosting their Wordpress sites</a> to easily produce AMP-powered pages.</p>
<p>Medium have also <a href="https://medium.com/the-story/making-medium-more-powerful-for-publishers-39663413a904#.bpfbfimsf">announced that they will be supporting AMP</a>, and we can probably expect other content publishing platforms to adopt AMP over the next few months.</p>
<p>AMP can also be seen as a good fit for those building <a href="https://developers.google.com/web/progressive-web-apps?hl=en">progressive web apps</a>, another recent technology paradigm that is also being heavily promoted by Google.</p>
<p>Progressive web apps offer some of the advantages enjoyed up to now by mobile apps, including fast load times, offline caching of content, and easy addition to the devices home screen, and allow web developers to better compete with mobile apps.</p>
<p>And if you think that mobile web apps are gaining advantages to the detriment of their app-based cousins, it should be noted that the convergence of mobile and web apps is happening from both directions, with <a href="https://developers.google.com/app-indexing/">deep-linking of app content</a> offering mobile developers access to one of the web's characteristic advantages.</p>
<h2 id="whatsgoodforgoogleisgoodfortheweb">What's good for Google is good for the web</h2>
<p>We shouldn't consider that Google are being solely altruistic with AMP — Google's business model is largely predicated on a healthy web economy, and although they control a number of aspects of the mobile app economy, they are in an on-going battle for advertising revenue with the likes of Facebook, who are successfully building up their share of the mobile app advertising market.</p>
<p>So, ensuring that the mobile web works fast and as seamlessly as possible with mobile apps can only be regarded as good business sense on Google's part.</p>
<h2 id="amprestrictions">AMP restrictions</h2>
<p>In order to produce an AMP-powered site, you'll have to limit the way in which you create your pages, and major AMP restrictions include the following:</p>
<ul>
<li>CSS must be inlined in the head, and must be no larger than 50K (i.e. uncompressed)</li>
<li>The only JavaScript directly allowed on the page (i.e. not in iframes) is the AMP JavaScript library</li>
<li>Certain performance-expensive CSS selectors and HTML tags are not allowed, with AMP providing custom HTML tags in place of some, for example the &lt;amp-img&gt; tag instead of the normal &lt;img&gt; tag</li>
<li>Third-party content is typically iframed in AMP-conformant components</li>
<li>As a consequence, third-party code is much more restricted in what it can do; for example interstitals or take-over ads are not easily acheiveable on AMP-powered pages.</li>
</ul>
<h2 id="thegoodnews">The good news</h2>
<p>It's not all bad news though, and a number of the features that we consider essential on the modern web have good support under AMP:</p>
<ul>
<li>Web fonts are privileged, in that they are treated as the first HTTP request to be actioned on the page</li>
<li><a href="https://www.ampproject.org/docs/guides/responsive/style_pages.html#disallowed-styles">Most CSS selectors are allowed</a>, and of those not allowed, the important (!) qualifier is probably going to be the one that developers miss the most</li>
<li>AMP provides plenty of AMP-specific components for media including video, slideshows, audo and images</li>
<li>Third-party JavaScript is allowed within the confines of iframes</li>
</ul>
<h2 id="ampperformanceobjectives">AMP performance objectives</h2>
<p>AMP considers performance not only in terms of speed, but also in terms of a better user experience when presenting content to the user as the page is loading.</p>
<p>This includes allowing the browser to determine the layout geometry of page components without having to fetch the external assets, such that the content doesn't jump around as the page is loading, and recommending the use of a CSS animation that fades in the page content only when it's ready for viewing.</p>
<blockquote>
<p>“Predictable performance is a key design goal for AMP HTML. Primarily we are aiming at reducing the time until the content of a page can be consumed / used by the user. In concrete terms this means that:<br>
HTTP requests necessary to render and fully layout the document should be minimized.<br>
Resources such as images or ads should only be downloaded if they are likely to be seen by the user.</p>
</blockquote>
<blockquote>
<p>Browsers should be able to calculate the space needed by every resource on the page without fetching that resource.”</p>
</blockquote>
<h2 id="learningfromamp">Learning from AMP</h2>
<p>A number of the techniques that AMP employs can be thought of as simply good practice, and can be used without necessarily having to adopt AMP itself.</p>
<h3 id="theeasystuff">The easy stuff</h3>
<ul>
<li>Ensure that there are zero HTTP requests until fonts start downloading</li>
<li>Inline critical CSS, and keep overall size small</li>
<li>Use <a href="http://caniuse.com/#search=resource%20hints">resource hints</a>: preload / preconnect / prefetch</li>
<li>Allow browsers to calculate size of all components before fetching</li>
<li>Lazy-loading of images and ads, such that only those that are in the viewport, or shortly to be in the viewport, are loaded</li>
<li>Restrict CSS animations to GPU-friendly opacity and transform</li>
<li>Make use of service workers to control caching</li>
</ul>
<h3 id="theharderstuff">The harder stuff</h3>
<p>There's a number of other techniques we can cherry-pick from AMP, though these may be harder to achieve than they would if we were directly using AMP:</p>
<ul>
<li>Keep third-party JS out of the critical path: async / defer loading of scripts</li>
<li>Iframe expensive third-party JS, so that document.write doesn’t block overall page rendering</li>
<li>Minimise style / layout calculations, using fastdom</li>
</ul>
<p>Happily, implementing <a href="https://en.wikipedia.org/wiki/HTTP/2">HTTP2</a> will go some way to promoting faster loading of pages, expecially if you use server-push and take advantage of pipelining assets through a single TCP connection.</p>
<h2 id="specifictechniques">Specific techniques</h2>
<p>Looking through the AMP code, there's a few techniques that stand out as being particularly useful:</p>
<h3 id="preconnectpolyfill">Preconnect polyfill</h3>
<p>AMP implements a simple preconnect polyfill in the form of an intentional 404:</p>
<pre><code>const url = origin + '/amp_preconnect_polyfill?' + Math.random();
const xhr = new XMLHttpRequest();
xhr.open('HEAD', url, true);
xhr.send();
</code></pre>
<blockquote>
<p>“Preconnecting executes DNS lookup, TCP and SSL handshake. This saves 100+ms even on WIFI and can be seconds faster on a crappy mobile connection.”</p>
</blockquote>
<h3 id="prefetchpolyfill">Prefetch Polyfill</h3>
<pre><code>const prefetch = document.createElement('link');
prefetch.setAttribute('rel', 'prefetch');
prefetch.setAttribute('href', url);
document.head.appendChild(prefetch);
</code></pre>
<p>The above doesn’t work on Safari iOS; so alternatively (but wastefully):</p>
<pre><code>new Image().src = urlToPrefetch
</code></pre>
<p>This will prefetch the URL and when it is actually fetched, it is served from cache. However, next time, when the resource is cached, but it wasn’t an image, it will fetch the resource again which wastes bandwidth.</p>
<h3 id="reduceuseofes6polyfills">Reduce Use of ES6 Polyfills</h3>
<p>This may give developers piling into ES6 some pause for thought:</p>
<blockquote>
<p>“We love ES6 and we love Babel, but unfortunately it turns out that some of its polyfills are heavy with respect to JavaScript size. Quite reluctantly we forked the core-js-shim (just one file) and only kept Array.from, Promise and Math.sign out of all the ES6 goodness.</p>
</blockquote>
<blockquote>
<p>Additionally we limited the syntactical ES6 features we use in the project to those that can be transpiled efficiently by Babel and created a custom Babel helpers file for that purpose”</p>
</blockquote>
<h3 id="thepracticallyimpossible">The (practically) impossible</h3>
<p>There are some techniques that AMP use which are probably very difficult to implement in large scale content publishing sites, including the coordiantion between iframes:</p>
<blockquote>
<p>“AMP sandboxes all third party JS in cross-origin iframes.</p>
</blockquote>
<blockquote>
<p>Whichever is the first third party sandbox iframe on a page declare themselves as “master iframe” and then the second and further frames try to find that master in the parent.</p>
</blockquote>
<blockquote>
<p>This way we always have one special iframe that is the “master iframe”. This allows easy sharing of resources across iframes. Work can be done only once and reused by all the iframes that need it.”</p>
</blockquote>
<h2 id="ampthepossiblefuture">AMP: the (possible) future</h2>
<p>The AMP project is ongoing, and recent commentary from the AMP has been interesting in sketching out future developments.</p>
<h3 id="javascriptgetsmoreprivileges">JavaScript gets more privileges</h3>
<p>Currently, AMP restricts custom and third-party JavaScript to second-class citizen status, only being able to operate within iframes.</p>
<p>In a <a href="https://medium.com/@cramforce/2016-will-be-the-year-of-concurrency-on-the-web-c39b1e99b30f#.q5d4ldfuf">piece about improving concurrency</a>, Matli Ubl described his vision for AMP adopting a coordination role for controlling the execution and scheduling of other JavaScript, such that smooth page performance can be maintained at all times.</p>
<p>This would then allow custom and third-party JavaScript to be libterated from their iframe ghettos, and operate once more in an environment where they have access to the full page model.</p>
<h3 id="contentperformancepolicy">Content Performance Policy</h3>
<p>In <a href="https://timkadlec.com/2016/02/a-standardized-alternative-to-amp/">a recent article</a>, Tim Kadlec was critical of AMP's role, and posited the use of browser directives indicating what performance boundaries are not negotiable, using a mechanism similar to <a href="https://en.wikipedia.org/wiki/Content_Security_Policy">Content Security Policy</a>.</p>
<p>As a result of this article, the AMP team got together with Tim Kadlec and Yoav Weiss, and drafted a proposal for something they're calling <a href="https://medium.com/google-developers/amp-and-the-sandbox-policy-adbf004ff65#.19ofhe223">Sandbox Policy</a>.</p>
<blockquote>
<p>The Sandbox Policy is inspired by AMP and shares certain concepts and goals. By having a browser-enforced mechanism of “self-control”, i.e. the ability to modify or disable certain web platform capabilities, it enables the following two scenarios:</p>
</blockquote>
<blockquote>
<ul>
<li>Developers can lock down their application against regressions caused by own and third party resources; it enables violation auditing and reporting against specified standards.</li>
</ul>
</blockquote>
<blockquote>
<ul>
<li>Third parties can validate enforced policies and use them as signals about runtime/UX/performance characteristics of the content.</li>
</ul>
</blockquote>
<p>This isn't envisaged as being a replacement for AMP, but rather a mechanism that could be used with or without AMP to enforce performance guidelines in the browser.</p>
<p>It'll be interesting to see what adoption this proposal gets, and it'd be great if we see a mechanism like this adopted across the board by the browser manufacturers.</p>
<h2 id="fastisimportant">Fast is important</h2>
<p>Although those of us living in well-connected cities in the developed world take (reasonably) fast internet access for granted, we'd still find ourselves benefiting from the use of AMP as a mechanism to combat the year-on-year page bloat that we've been seeing for a while now, and which serves to obviate any advantage that we get from having faster devices and better network speeds.</p>
<p>And for those of us living in not-so-well-connected areas of the world, or still stuck on 3G connections, AMP provides something that approaches a seamless browsing experience.</p>
<h2 id="furtherreading">Further reading</h2>
<h3 id="reactionsfromnewsmediaorganisations">Reactions from News Media Organisations</h3>
<ul>
<li><a href="https://medium.com/@Rich_Harris/don-t-cramp-our-style-9bcef09e638f#.67zblyosk">https://medium.com/@Rich_Harris/don-t-cramp-our-style-9bcef09e638f#.67zblyosk</a></li>
<li><a href="https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/">https://source.opennews.org/en-US/articles/what-amp-maybe-means-news-developers/</a></li>
<li><a href="http://www.niemanlab.org/2015/10/get-ampd-heres-what-publishers-need-to-know-about-googles-new-plan-to-speed-up-your-website/">http://www.niemanlab.org/2015/10/get-ampd-heres-what-publishers-need-to-know-about-googles-new-plan-to-speed-up-your-website/</a></li>
<li><a href="http://www.niemanlab.org/2015/12/static-is-the-new-interactive/">http://www.niemanlab.org/2015/12/static-is-the-new-interactive/</a></li>
</ul>
<h3 id="moreaboutamp">More about AMP</h3>
<ul>
<li><a href="https://medium.com/@cramforce/why-amp-is-fast-7d2ff1f48597#.e6b5smfhy">https://medium.com/@cramforce/why-amp-is-fast-7d2ff1f48597#.e6b5smfhy</a></li>
<li><a href="https://medium.com/@cramforce/why-amp-html-does-not-take-full-advantage-of-the-preload-scanner-7e7f788aa94e#.7smx1owes">https://medium.com/@cramforce/why-amp-html-does-not-take-full-advantage-of-the-preload-scanner-7e7f788aa94e#.7smx1owes</a></li>
<li><a href="https://medium.com/@cramforce/amps-and-websites-in-the-age-of-the-service-worker-8369841dc962#.ev2nwm5rq">https://medium.com/@cramforce/amps-and-websites-in-the-age-of-the-service-worker-8369841dc962#.ev2nwm5rq</a></li>
<li><a href="https://medium.com/@cramforce/not-so-micro-optimizations-f867c47b832d#.50bfl1j2m">https://medium.com/@cramforce/not-so-micro-optimizations-f867c47b832d#.50bfl1j2m</a></li>
<li><a href="https://medium.com/@cramforce/2016-will-be-the-year-of-concurrency-on-the-web-c39b1e99b30f#.q5d4ldfuf">https://medium.com/@cramforce/2016-will-be-the-year-of-concurrency-on-the-web-c39b1e99b30f#.q5d4ldfuf</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Automating Elastic Beanstalk]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/e_brightness:-12/v1458863757/norwaydock_vb8jii.jpg) -->
<h2 id="goodpracticeandscalability">Good practice and scalability</h2>
<p>In this post we're going to demonstrate running Docker containers using Amazon's Elastic Beanstalk.</p>
<p>When I was figuring out to use Docker containers on Amazon's Elastic Beanstalk, I seemed to find plenty of articles detailing how to manually setup Elastic Beanstalk using the AWS dashboard, but</p></div>]]></description><link>https://blog.mebooks.co.nz/automating-elastic-beanstalk/</link><guid isPermaLink="false">59d06022c7843e0001a0ffdb</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Sat, 02 Apr 2016 03:25:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/norwaydock_vb8jii.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/e_brightness:-12/v1458863757/norwaydock_vb8jii.jpg) -->
<h2 id="goodpracticeandscalability">Good practice and scalability</h2>
<img src="https://blog.mebooks.co.nz/content/images/2017/10/norwaydock_vb8jii.jpg" alt="Automating Elastic Beanstalk"><p>In this post we're going to demonstrate running Docker containers using Amazon's Elastic Beanstalk.</p>
<p>When I was figuring out to use Docker containers on Amazon's Elastic Beanstalk, I seemed to find plenty of articles detailing how to manually setup Elastic Beanstalk using the AWS dashboard, but I really wanted a solution that was automatable / repeatable.</p>
<p>Specifically, with this project we want to be able to:</p>
<ul>
<li>Install and use Docker on Mac OSX (though other host platforms are fine)</li>
<li>Build a custom <code>apache-php5</code> Docker image using a <code>Dockerfile</code>, fully provisioned and ready for our app</li>
<li>Build a <code>php-app</code> Docker image containing our custom app on top of this <code>apache-php5</code> Docker image</li>
<li>Push our Docker images to <a href="http://hub.docker.com">hub.docker.com</a></li>
<li>Run up our application locally from the Docker images using Elastic Beanstalk's <code>eb local run</code></li>
<li>Run up our application remotely on AWS using Elastic Beanstalk's <code>eb create</code> / <code>eb deploy</code></li>
<li>Do all of the above in an automated fashion, using a <code>Makefile</code></li>
</ul>
<p>We want to follow good practice, and this involves splitting our database and application into separate containers. This will then alow us to scale up application containers and database containers independently.</p>
<p>For the local install, we'll deploy both the application and database container on the same host, but when deploying for production, we'll use RDS to serve our database, and an EC2 instance dedicated to each application container.</p>
<p>Presuming that our application is PHP heavy but database light (say, because it only uses the database for login / personal profile storage), we can probably get away with a single<br>
database container / RDS instance, while scaling up more EC2 instances of our application container when demand increases.</p>
<p>Our app will do nothing more arduous than displaying the information generated by <code>phpinfo()</code> (on the <code>php-app</code> container) and show that it can connect to a MySQL database (on the <code>mysql</code> container if local or the using RDS if deployed on AWS).</p>
<p>However, you should easily be able to build out a much more complicated PHP application after<br>
reading through these notes.</p>
<p>Note that we build an <code>apache-php5</code> Docker image first, and then another <code>php-app</code> image containing our app on top of this image. This allows us to re-use the existing apache-php5 Docker image whenever our app changes, and thus speeds up our builds.</p>
<p>All code for this project can be found at <a href="https://github.com/jcdarwin/local-elasticbeanstalk-php-demo">https://github.com/jcdarwin/local-elasticbeanstalk-php-demo</a></p>
<h2 id="requirements">Requirements</h2>
<p>We're doing this on OSX, but it should work on typical linux variants.<br>
We're presuming that you've got installed:</p>
<ul>
<li><a href="http://brew.sh/">homebrew</a> (Mac OSX package manager)</li>
<li>GNU make</li>
<li>a public/private key pair, <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html">created and registered on AWS</a>,<br>
and stored locally, e.g.:</li>
</ul>
<pre><code class="language-bash">~/.ssh/aws-eb
~/.ssh/aws-eb.pub
</code></pre>
<p>We're also presuming that you're reasonably comfortable with the principles behind Docker, and have probably already had some exposure to AWS.</p>
<h2 id="configuration">Configuration</h2>
<p>We'll use a local shell script, <code>.env_local</code>, not versiond in git, to store our custom / sensitive details and implement them as environment variables.<br>
This should be the only place we have to regularly make changes, and is used by our <code>Makefile</code> when running build steps:</p>
<pre><code class="language-bash">#!/bin/bash

export EB_APP=local-elasticbeanstalk
export EB_ENVIRONMENT=dev-$EB_APP
export EB_SCALE_MIN=2

export DOCKER_MACHINE=default
export DOCKER_USER=mebooks
export DOCKER_EMAIL=jcdarwin@gmail.com
export DOCKER_PASSWORD=bailter

DOCKERED=eval &quot;$(docker-machine env $DOCKER_MACHINE)&quot;
</code></pre>
<p>Set execute permissions on this file:</p>
<pre><code class="language-bash">chmod a+g .env_local
</code></pre>
<p>Note that running the <code>.env_local</code> loads the environment so we can use docker (once installed) from the local OSX terminal, and does the same as:</p>
<pre><code class="language-bash">eval &quot;$(docker-machine env default)&quot;
</code></pre>
<h2 id="installation">Installation</h2>
<p>We use <code>docker-machine</code> on OSX via the <a href="https://www.docker.com/products/docker-toolbox">Docker ToolBox</a>.</p>
<p>The easiest way to do this is:</p>
<pre><code class="language-bash">brew cask install dockertoolbox
</code></pre>
<p>As we're using Max OSX, this means that we'll end up with a Virtualbox Linux VM that will be used to run Docker.</p>
<p>Start our <code>docker-machine</code>:</p>
<pre><code class="language-bash">docker-machine start default
</code></pre>
<h3 id="setupdockersowecanuseitfromourcurrentshell">Setup docker so we can use it from our current shell</h3>
<p>Port-forward in <code>VirtualBox</code>, so we can access port 80 transparently:</p>
<pre><code class="language-bash">VBoxManage list vms
VBoxManage modifyvm &quot;defaut&quot; --natpf1 &quot;guestnginx,tcp,,80,,80&quot;
</code></pre>
<p>Once installed, we can see that <code>docker</code> is at version 10.1:</p>
<pre><code class="language-bash">docker -v
$ Docker version 1.10.2, build c3959b1
</code></pre>
<p>If we apply our environment variables:</p>
<pre><code class="language-bash">. ./.env_local
</code></pre>
<p>we should then see our environment values based on <code>docker-machine config default</code> plus any extra that we've added in <code>.env_local</code>:</p>
<pre><code class="language-bash">env | grep DOCKER

    DOCKER_PASSWORD=WHATEVER
    DOCKER_HOST=tcp://192.168.99.101:2376
    DOCKER_MACHINE_NAME=default
    DOCKER_TLS_VERIFY=1
    DOCKER_MACHINE=default
    DOCKER_USER=mebooks
    DOCKER_CERT_PATH=/Users/jasondarwin/.docker/machine/machines/default
    DOCKER_EMAIL=jcdarwin@gmail.com
</code></pre>
<p>and we should be able to access docker directly</p>
<pre><code class="language-bash">docker info
</code></pre>
<h3 id="installawsebcli">Install <code>awsebcli</code></h3>
<p>We install the <code>awsebcli</code> using <code>homebrew</code>:</p>
<pre><code class="language-bash">brew install awsebcli
</code></pre>
<p>However, <a href="https://forums.aws.amazon.com/thread.jspa?threadID=225425">there's a problem with the version compatibility check</a>, meaning that awsebcli thinks that docker 1.10.2 is &lt; docker 1.6, and we receive the following message:</p>
<pre><code class="language-bash">&quot;You must install Docker version 1.6.0 to continue. If you are using Mac OS X, ensure you have boot2docker version 1.6.0. Currently, &quot;eb local&quot; does not support Windows.&quot;
</code></pre>
<p>To rectify this, currently we must edit <code>/usr/local/Cellar/aws-elasticbeanstalk/3.7.3/libexec/lib/python2.7/site-packages/ebcli/containers/compat.py</code> as follows:</p>
<pre><code class="language-python">def supported_docker_installed():
    &quot;&quot;&quot;
    Return whether proper Docker version is installed.
    :return: bool
    &quot;&quot;&quot;

    try:
        #return commands.version() &gt;= SUPPORTED_DOCKER_V
        return True
    # OSError = Not installed
    # CommandError = docker versions less than 1.5 give exit code 1
    # with 'docker --version'.
    except (OSError, CommandError):
        return False
</code></pre>
<h3 id="installcomposer">Install Composer</h3>
<pre><code class="language-bash">curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
</code></pre>
<p>Use composer to install the dependencies for our <code>php-app</code></p>
<pre><code class="language-bash">cd php-app
composer install
</code></pre>
<h2 id="appenvironmentvariables">App environment variables</h2>
<p>We need to make certain environment variables available to our PHP scripts, particularly<br>
those to do with connecting to our MySQL container.</p>
<p>To do this, we create a <code>php-app/.env</code> file, with placeholders for the expected<br>
environment variables:</p>
<pre><code class="language-php"># php-app/.env
# The variables below are replaced during container startup by init.sh

# If we're using a local mysql container, the MYSQL variables are populated
#DB_HOST=&quot;${MYSQLDB_PORT_3306_TCP_ADDR}&quot;
#DB_DATABASE=&quot;${MYSQLDB_ENV_MYSQL_DATABASE}&quot;
#DB_PASSWORD=&quot;${MYSQLDB_ENV_MYSQL_ROOT_PASSWORD}&quot;
#DB_USERNAME=&quot;${MYSQLDB_ENV_MYSQL_USERNAME}&quot;

# If we;re using an AWS RDS instance, the RDS  variables are populated
#DB_HOST=&quot;${RDS_HOSTNAME}&quot;
#DB_DATABASE=&quot;${RDS_DB_NAME}&quot;
#DB_PASSWORD=&quot;${RDS_PASSWORD}&quot;
#DB_USERNAME=&quot;${RDS_USERNAME}&quot;
</code></pre>
<p>We then use our <code>init.sh</code> script to read the environment variables during the initialisation of our <code>php-app</code> container, and replace the placeholders in <code>php-app/.env</code> with the environment variables values:</p>
<pre><code class="language-bash"># Make a copy of our .env file, as we don't want to pollute the original
cp /var/www/html/.env /tmp/

# Update the app configuration to make the service environment
# variables available.
function setEnvironmentVariable() {
    if [ -z &quot;$2&quot; ]; then
        echo &quot;Environment variable '$1' not set.&quot;
        return
    fi

    # Check whether variable already exists
    if grep -q &quot;\${$1}&quot; /tmp/.env; then
        # Reset variable
        sed -i &quot;s/\${$1}/$2/g&quot; /tmp/.env
    fi
}

# Grep for variables that look like MySQL (for local deployments)
# or RDS (for remote deployments).
for _curVar in `env | grep 'MYSQL\|RDS' | awk -F = '{print $1}'`;do
    # awk has split them by the equals sign
    # Pass the name and value to our function
    setEnvironmentVariable ${_curVar} ${!_curVar}
done

# Now that /tmp/.env is populated, we can start/restart apache
# and let our PHP scripts access them.
service apache2 restart
</code></pre>
<h2 id="createourmakefile">Create our Makefile</h2>
<p>We use a Makefile to make our builds slightly easier:</p>
<pre><code class="language-bash">include .env_local
BASE_IMAGE=mebooks/apache-php5
APP_IMAGE=mebooks/php-app
APP=php-app
VERSION=`git describe --tags`
CORE_VERSION=HEAD

all: build-base prepare

base: build-base push-base

app: prepare-app build-app push-app

environment: create-environment

#
# Our base image tasks
#
build-base:
    docker build -t $(BASE_IMAGE):$(VERSION) docker/base

push-base:
    docker login --username=$(DOCKER_USER) --email=$(DOCKER_EMAIL) --password=$(DOCKER_PASSWORD)
    docker push $(BASE_IMAGE)

#
# Our app image tasks
#
prepare-app:
    # Update Dockerrun.aws.json with the current image version
    sed -i '' &quot;s~${APP_IMAGE}\:[^\&quot;]*~${APP_IMAGE}\:$(VERSION)~g&quot; Dockerrun.aws.json
    git archive --format tgz HEAD $(APP) &gt; docker/app/$(APP).tgz

build-app:
    docker build -t $(APP_IMAGE):$(VERSION) docker/app

push-app:
    docker login --username=$(DOCKER_USER) --email=$(DOCKER_EMAIL) --password=$(DOCKER_PASSWORD)
    docker push $(APP_IMAGE)

#
# Our Elastic Beanstalk tasks
#
create-environment:
    eb create -v \
        --cfg $(EB_APP) \
        --scale $(EB_SCALE_MIN) \
        --cname $(EB_ENVIRONMENT) \
        $(EB_ENVIRONMENT)
</code></pre>
<h2 id="buildthedockerimages">Build the docker images</h2>
<p>Commit and tag our changes, e.g.:</p>
<pre><code class="language-bash">    git tag 2.3.0
</code></pre>
<p>The use of <code>git describe --tags</code> in the <code>Makefile</code> means that we'll be using the latest <code>git tag</code> to tag our Docker images.<br>
Note that, if you've made commits since your last <code>git tag</code>, we'll end up using a tag value which is a combination of the tag and the last commit hash:</p>
<pre><code class="language-bash">git describe --tags
    2.4.0-9-g8215032
</code></pre>
<h3 id="buildourmebooksapachephp5basedockerimage">Build our <code>mebooks/apache-php5</code> base Docker image</h3>
<pre><code class="language-bash">    # Create the `mebooks/apache-php5` Docker image
    make base

    # Check that the image was created
    docker images
</code></pre>
<p>Edit <code>docker/app/Dockerfile</code> and ensure that our <code>php-app</code> is refering to the same version as that that we just built:</p>
<pre><code class="language-bash">    FROM mebooks/apache-php5:2.3.0
</code></pre>
<p>Our <code>mebooks/apache-php5</code> Docker image should only need updating when we want to update the packages in the distribution, such as when there are security vulnerabilities.</p>
<h3 id="buildourappdockerimage">Build our app docker image</h3>
<p>Note that our app docker image wil be tagged with the current version of the repo and our <code>Dockerrun.aws.json</code> file will be updated accordingly.</p>
<pre><code class="language-bash"># Create our php-app image and push it to hub.docker.com
make app

# Check that we updated the image version in our `Dockerrun.aws.json`
cat Dockerrun.aws.json | grep 'Name'
    &quot;Name&quot;: &quot;mebooks/php-app:2.4.0-8-gb0eef33&quot;,
</code></pre>
<p>As we've just tagged our <code>Dockerrun.aws.json</code> file with the current version of the repo,<br>
we need to commit changes, otherwise the <code>eb create</code> / <code>eb deploy</code> (which makes use of <code>git archive</code>) will use the previous version of the <code>Dockerrun.aws.json</code> file.</p>
<p>Note that, as <code>eb create</code> / <code>eb deploy</code> uses <code>git archive</code> to create a zipfile of our application for deployment, this means that:</p>
<ul>
<li>we can use <code>.gitignore</code> to specify files that shouldn't be under version control</li>
<li>we can use <code>.gitattributes</code> to specify files that should be under version control but should be deployed in the app bundle.</li>
</ul>
<p><code>eb create</code> / <code>eb deploy</code> can also make use of a <code>.ebignore</code> file.</p>
<h3 id="checkthatourcontainersfunctionasexpected">Check that our containers function as expected</h3>
<pre><code class="language-bash"># Set our environment variable
VERSION=`git describe --tags` &amp;&amp; echo $VERSION

# Start just the PHP container
docker run -tid -p 80:80 \
    --name=php-app \
    mebooks/php-app:${VERSION}

# Start both containers linked
docker run -p 3306:3306 \
    -e MYSQL_USERNAME=root \
    -e MYSQL_ROOT_PASSWORD=password \
    -e MYSQL_DATABASE=my_db \
    -d \
    --name mysqlserver \
    mysql

docker run -tid -p 80:80 \
    --name=php-app \
    --link mysqlserver:mysqldb \
    mebooks/php-app:${VERSION}
</code></pre>
<p>We should now be able to see the PHP Info details at the address reported by <code>docker-machine ip</code>, e.g.:</p>
<pre><code class="language-bash">docker-machine ip
    http://192.168.99.101/
</code></pre>
<p>If we've started the <code>mysql</code> container, we should also be able to see a simple example of connecting to our MySQL database at <a href="http://192.168.99.101/mysql.php">http://192.168.99.101/mysql.php</a></p>
<h2 id="pushourimagestohubdocker">Push our images to hub.docker</h2>
<p>Although our <code>Makefile</code> handles the pushing of our containers to hub.docker, we'll cover it here as we need to know about AWS using the associated config file to gain access to pull our images from hub.docker.</p>
<p>Login to our docker account:</p>
<pre><code class="language-bash"># Enter the username, password and email when prompted
docker login

# Alternatively, specify username, email and password
# If any of these parameters are not supplied, you'll be prompted for them
docker login --username=mebooks --email=jcdarwin@gmail.com --password=WHATEVER
</code></pre>
<p>The login will create a config file at <code>~/.docker/config.json</code>.</p>
<p>AWS currently uses an older format of the docker config for authentication to hub.docker,<br>
so we need to change our current <code>~/.docker/config.json</code> to the required format by removing the <code>auths</code> wrapper:</p>
<pre><code class="language-json"># ~/.docker/config.json
{
    &quot;auths&quot;: {
        &quot;https://index.docker.io/v1/&quot;: {
            &quot;auth&quot;: &quot;WHATEVER&quot;,
            &quot;email&quot;: &quot;jcdarwin@gmail.com&quot;
        }
    }
}
</code></pre>
<p>becomes:</p>
<pre><code class="language-json"># ~/.docker/.dockercfg.json
{
        &quot;https://index.docker.io/v1/&quot;: {
            &quot;auth&quot;: &quot;WHATEVER&quot;,
            &quot;email&quot;: &quot;jcdarwin@gmail.com&quot;
        }
}
</code></pre>
<p>We now need to push this to a suitable s3 bucket so Elastic Beanstalk can use it to download our images from our private repository.</p>
<pre><code class="language-bash">s3cmd put ~/.docker/.dockercfg.json s3://elasticbeanstalk-ap-southeast-2-&lt;aws_account_id&gt;
</code></pre>
<p>Pushing our images to hub.docker is as simple as:</p>
<pre><code class="language-bash">docker push mebooks/apache-php5
docker push mebooks/php-app
</code></pre>
<p>Note that the <code>make app</code> task automatically does the <code>docker push mebooks/php-app</code>, while the <code>make base</code> task automatically does the <code>docker push mebooks/apache-php5</code>,</p>
<p>Once pushed, we should be able to see our images on hub.docker:</p>
<pre><code>https://hub.docker.com/r/mebooks/php-app/tags/
https://hub.docker.com/r/mebooks/apache-php5/tags/
</code></pre>
<h3 id="createourecrrespositories">Create our ECR respositories</h3>
<p>Alternatively, in this section we'll look at using Amazon's Elastic Container Registry (ECR) as a place to store our images instead of using hub.docker.</p>
<p>Authenticate Docker to an Amazon ECR registry with <a href="http://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_AWSCLI.html">get-login</a>:</p>
<pre><code class="language-bash">aws ecr --profile oregon get-login

docker login -u AWS -p BIGLONGNUMBERAPPEARSHERE -e none https://&lt;aws_account_id&gt;.dkr.ecr.us-west-2.amazonaws.com
</code></pre>
<p>Note that https://&lt;aws_account_id&gt;.dkr.ecr.us-west-2.amazonaws.com is the URL for our container registry.</p>
<p>Copy and paste the docker login command into a terminal to authenticate your Docker CLI to the registry. This command provides an authorization token that is valid for the specified registry for 12 hours.</p>
<p>Create our repositories:</p>
<pre><code class="language-bash">aws ecr create-repository --profile oregon --repository-name mebooks/apache-php5

    registryId: &lt;aws_account_id&gt;
    repositoryArn: arn:aws:ecr:us-west-2:&lt;aws_account_id&gt;:repository/mebooks/apache-php5
    repositoryName: mebooks/apache-php5

aws ecr create-repository --profile oregon --repository-name mebooks/php-app

    registryId: &lt;aws_account_id&gt;
    repositoryArn: arn:aws:ecr:us-west-2:&lt;aws_account_id&gt;:repository/mebooks/php-app
    repositoryName: mebooks/php-app
</code></pre>
<p>Tag our images and push them</p>
<pre><code class="language-bash">docker tag mebooks/apache-php5:latest &lt;aws_account_id&gt;.dkr.ecr.us-west-2.amazonaws.com/mebooks/apache-php5:latest

docker push &lt;aws_account_id&gt;.dkr.ecr.us-west-2.amazonaws.com/mebooks/apache-php5:latest

docker tag mebooks/php-app:2.4.0-8-gb0eef33 &lt;aws_account_id&gt;.dkr.ecr.us-west-2.amazonaws.com/mebooks/php-app:2.4.0-8-gb0eef33

docker push &lt;aws_account_id&gt;.dkr.ecr.us-west-2.amazonaws.com/mebooks/php-app:2.4.0-8-gb0eef33
</code></pre>
<h2 id="createourdockerrunawsjsonmulticontainerversion">Create our <code>Dockerrun.aws.json</code> (multi-container version)</h2>
<p>We can use a multi-container <code>Dockerrun.aws.json</code> if we want to use <code>eb local run</code> to spin up both our <code>php-app</code> container and an associated <code>mysql</code> container.</p>
<pre><code class="language-json">{
    &quot;AWSEBDockerrunVersion&quot;: 2,
    &quot;containerDefinitions&quot;: [
        {
            &quot;name&quot;: &quot;mysql&quot;,
            &quot;image&quot;: &quot;mysql:5.6&quot;,
            &quot;essential&quot;: true,
            &quot;portMappings&quot;: [
                {
                    &quot;hostPort&quot;: 3306,
                    &quot;containerPort&quot;: 3306
                }
            ],
            &quot;environment&quot;: [
                {
                    &quot;name&quot;: &quot;MYSQL_USERNAME&quot;,
                    &quot;value&quot;: &quot;root&quot;
                },
                {
                    &quot;name&quot;: &quot;MYSQL_PASSWORD&quot;,
                    &quot;value&quot;: &quot;password&quot;
                },
                {
                    &quot;name&quot;: &quot;MYSQL_DB_NAME&quot;,
                    &quot;value&quot;: &quot;my_db&quot;
                }
            ]
        },
        {
            &quot;name&quot;: &quot;php-app&quot;,
            &quot;image&quot;: &quot;mebooks/php-app&quot;,
            &quot;essential&quot;: true,
            &quot;memory&quot;: 128,
            &quot;portMappings&quot;: [
                {
                    &quot;hostPort&quot;: 80,
                    &quot;containerPort&quot;: 80
                }
            ],
            &quot;links&quot;: [
                &quot;mysql&quot;
            ]
        }
    ]
}
</code></pre>
<p>Note that we specify a (very) weak <code>MYSQL_ROOT_PASSWORD</code> in our <code>Dockerrun.aws.json</code> — you'll want to change this and ideally not have it under version control.</p>
<h2 id="runourcontainerslocally">Run our containers locally</h2>
<p>Finally, use <code>eb local</code> to create our docker containers locally:</p>
<pre><code class="language-bash">eb local run
</code></pre>
<p>In a second terminal:</p>
<pre><code class="language-bash">eb local status
docker ps
</code></pre>
<h2 id="accessingthecontainerslocally">Accessing the containers locally</h2>
<p>Find the appropriate container id, and start a bash shell on it:</p>
<pre><code class="language-bash">docker ps

$ CONTAINER ID        IMAGE
$ 832af3ff45d8        mebooks/apache-php5:latest
$ fc6a9553583f        mysql:5.6

# Access our PHP container
docker exec -it 832af3ff45d8 bash
</code></pre>
<p>Alternatively, we can use <code>eb</code> to find the details, including the human-readable container names:</p>
<pre><code class="language-bash">eb local status

$ Platform: 64bit Amazon Linux 2015.09 v2.0.8 running Multi-container Docker 1.9.1 (Generic)
$ Container name: elasticbeanstalk_mysql_1
$ Container ip: 127.0.0.1
$ Container running: True
$ Exposed host port(s): 3306
$ Full local URL(s): 127.0.0.1:3306

$ Container name: elasticbeanstalk_phpapache_1
$ Container ip: 127.0.0.1
$ Container running: True
$ Exposed host port(s): 80
$ Full local URL(s): 127.0.0.1:80
</code></pre>
<p>Access our PHP container:</p>
<pre><code class="language-bash">docker exec -it elasticbeanstalk_phpapache_1 bash

# check our apache config
apachectl configtest

# view our apache config
cat /etc/apache2/sites-enabled/vhost.conf

# Find the ip of our MySQL container
env | grep MYSQL_1_PORT_3306_TCP_ADDR

$ ELASTICBEANSTALK_MYSQL_1_PORT_3306_TCP_ADDR=172.17.0.2
$ MYSQL_1_PORT_3306_TCP_ADDR=172.17.0.2

# Login to mysql
mysql -u root -h 172.17.0.2  -p
</code></pre>
<p>Access our MySQL container:</p>
<pre><code class="language-bash">docker exec -it elasticbeanstalk_mysql_1 bash

# Display our databases -- we should see my_db
mysql -u root -p -e &quot;show databases;&quot;
</code></pre>
<h2 id="createourdockerrunawsjsonsinglecontainerversion">Create our <code>Dockerrun.aws.json</code> (single container version)</h2>
<p>Although the multi-container version above is useful for testing locally, in production we'll be using RDS to host MySQL, so we'll use a single container <code>Dockerrun.aws.json</code> to deploy our <code>php-app</code> container, and use an Amazon RDS instance for our database.</p>
<pre><code class="language-json">{
    &quot;AWSEBDockerrunVersion&quot;: 1,
    &quot;Image&quot;: {
        &quot;Name&quot;: &quot;mebooks/php-app:2.4.0-8-gb0eef33&quot;,
        &quot;Update&quot;: &quot;true&quot;
    },
    &quot;Authentication&quot;: {
        &quot;Bucket&quot;: &quot;elasticbeanstalk-ap-southeast-2-&lt;aws_account_id&gt;&quot;,
        &quot;Key&quot;: &quot;.dockercfg.json&quot;
    },
    &quot;Ports&quot;: [
        {
            &quot;ContainerPort&quot;: &quot;80&quot;
        }
    ],
    &quot;Logging&quot;: &quot;/var/log/apache2&quot;
}
</code></pre>
<h2 id="initialiseourelasticbeanstalkapp">Initialise our Elastic Beanstalk app</h2>
<p>Now that we're happy with the running the containers up locally, we need to initialise our <code>.elasticbeanstalk/config.yml</code> so we can deploy our containers to AWS:</p>
<pre><code class="language-bash">    eb init
</code></pre>
<p>More info about the <code>eb cli</code> tool is to be found on the <a href="http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3-getting-started.html">Amazon site</a>.</p>
<p>We'll need to customise our configuration, so create a standard config as a starting point:</p>
<pre><code class="language-bash"># This creates `.elasticbeanstalk/local-elasticbeanstalk.env.yml`
eb config
</code></pre>
<p>Ensure we have settings as we want, e.g. the <code>MinSize</code> for autoscaling:</p>
<pre><code class="language-bash">  aws:autoscaling:asg:
    Availability Zones: Any
    Cooldown: '360'
    Custom Availability Zones: 'ap-southeast-2'
    MaxSize: '4'
    MinSize: '2'
</code></pre>
<h2 id="createourelasticbeanstalkapplicationenvironment">Create our Elastic Beanstalk application environment</h2>
<p>Refer <a href="https://github.com/hopsoft/relay/wiki/How-to-Deploy-Docker-apps-to-Elastic-Beanstalk">https://github.com/hopsoft/relay/wiki/How-to-Deploy-Docker-apps-to-Elastic-Beanstalk</a></p>
<pre><code class="language-bash"># Ensure `.env_local` specifies the correct `EB_ENVIRONMENT`:
export EB_ENVIRONMENT=dev-local-elasticbeanstalk

# Create our new environment on Elastic Beanstalk
make environment
</code></pre>
<p><code>make environment</code> effectively runs something like the following, depending on<br>
your environment variable settings in <code>.env_local</code>:</p>
<pre><code class="language-bash">eb create -v \
    --scale 2
    --cname dev-local-elasticbeanstalk \
    dev-local-elasticbeanstalk
</code></pre>
<p>We should then see output at our terminal like the following:</p>
<pre><code class="language-bash">    INFO: Creating new application version using project code
    WARNING: You have uncommitted changes.
    INFO: Getting version label from git with git-describe
    Creating application version archive &quot;app-8215-160402_175219&quot;.
    INFO: creating zip using git archive HEAD
    INFO: git archive output: Dockerrun.aws.json
    php-app/
    php-app/.env
    php-app/composer.json
    php-app/composer.lock
    php-app/public/
    php-app/public/index.php
    php-app/public/mysql.php
    INFO: Uploading archive to s3 location: local-elasticbeanstalk-php-demo/app-8215-160402_175219.zip
    Uploading local-elasticbeanstalk-php-demo/app-8215-160402_175219.zip to S3. This may take a while.
    Upload Complete.
    INFO: Creating AppVersion app-8215-160402_175219
    INFO: Creating new environment
    Environment details for: dev-local-elasticbeanstalk
      Application name: local-elasticbeanstalk-php-demo
      Region: ap-southeast-2
      Deployed Version: app-8215-160402_175219
      Environment ID: e-myq9pzyjxg
      Platform: 64bit Amazon Linux 2015.09 v2.0.8 running Docker 1.9.1
      Tier: WebServer-Standard
      CNAME: dev-local-elasticbeanstalk.ap-southeast-2.elasticbeanstalk.com
      Updated: 2016-04-02 04:52:24.369000+00:00
    Printing Status:
    INFO: createEnvironment is starting.
    INFO: Using elasticbeanstalk-ap-southeast-2-&lt;aws_account_id&gt; as Amazon S3 storage bucket for environment data.
     -- Events -- (safe to Ctrl+C)

# CTRL-C when prompted, and follow progress:
eb status
</code></pre>
<p>If we want to reference an existing config, we can do this during creation:</p>
<pre><code class="language-bash">    eb create -v --cfg local-elasticbeanstalk
</code></pre>
<p>Note that <code>eb create</code> will use the settings from <code>.gitattributes</code> to <code>export-ignore</code> files in the zip file that it creates and uploads to s3.<br>
As such, we have to be careful that the <code>Dockerrun.aws.json</code> file is included in the root level of our zip file.</p>
<pre><code class="language-bash">Creating application version archive &quot;app-4bbe-160402_140739&quot;.
Uploading local-elasticbeanstalk-php-demo/app-4bbe-160402_140739.zip to S3. This may take a while.
</code></pre>
<p>If we wish, we can easily download this zip file and inspect the contents:</p>
<pre><code class="language-bash">s3cmd get s3://elasticbeanstalk-ap-southeast-2-&lt;aws_account_id&gt;/local-elasticbeanstalk-php-demo/app-4bbe-160402_140739.zip
</code></pre>
<p>Once created (and even during creation), we will see our envionment in our AWS dashboard:<br>
<a href="https://ap-southeast-2.console.aws.amazon.com/elasticbeanstalk/home">https://ap-southeast-2.console.aws.amazon.com/elasticbeanstalk/home</a></p>
<h2 id="rds">RDS</h2>
<p>We now need to create our <a href="https://aws.amazon.com/rds/">RDS instance</a>.<br>
For this project we'll use:</p>
<ul>
<li>RDS MySQL</li>
<li>T2 Small</li>
<li>Multi-AZ</li>
<li>Allocated storage: 5GB</li>
<li>DB Instance Identifier: mebooks-mysql-dbinstance</li>
<li>Master Username: root</li>
</ul>
<p>Using <code>Multi-AZ</code> ensures that we mitigate the chance of failure by having our main RDS instance in one availability zone, and a RDS failover in another availability zone.<br>
Ensure that the security group used for the RDS instance allows connections to and from port 3306, and that the EC2 instances will be also using this security group.</p>
<p>Importantly, we should ensure that our RDS instance is private, but set to use the same VPC as our EC2 instances.</p>
<p>Once setup, we should be able to connect from an EC2 instance in the same group as follows (depending on the actual assigned RDS endpoint):</p>
<pre><code class="language-bash">mysql -h mebooks-mysql-dbinstance.criieggarwwz.ap-southeast-2.rds.amazonaws.com -u root -p
</code></pre>
<p>As per <a href="http://serverfault.com/a/540916">this answer</a>, there's currently no easy way to automate the attachment of an RDS database existing outside of an Elastic Beanstalk environment to the environment during creation.<br>
Amazon seem to assume that you'll be wanting to create an RDS instance inside the Elastic Beanstalk environment, which means that your database becomes less independent, and may disappear should anything go wrong with your Elastic Beanstalk environment.</p>
<p>Instead, if you want the RDS instance to exist outside of the environment you can simply provide the connection parameters as environment variables via the EB Console: Configuration -&gt; Web Layer -&gt; Software Configuration:</p>
<pre><code class="language-bash">RDS_HOSTNAME: mebooks-mysql-dbinstance.criieggarwwz.ap-southeast-2.rds.amazonaws.com
RDS_DB_NAME : my_db
RDS_USERNAME: root
RDS_PASSWORD: WHATEVER
</code></pre>
<p>These environment variables can then be <a href="http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_PHP.rds.html#create_deploy_PHP.rds.newDB">accessed from inside your PHP app</a>.</p>
<p>This does mean that our application will not be able to connect to our database on the initial deployment during the <code>eb create</code>, as we won't have yet had the chance to set these RDS<br>
environment variables in the EB dashboard.</p>
<p>Note that with this method, as the <code>RDS_PASSWORD</code> will be in plain sight on the AWS dashboard, it's probably wise to change this regularly. When you make changes to your environment variables on the EB dashboard and click <code>apply</code>, Elastic Beanstalk will then re-provision your existing EC2 instances with the new values.</p>
<h2 id="accessingourapplication">Accessing our application</h2>
<p>To see our application in the browser, we can use:</p>
<pre><code class="language-bash"># opens a browser tab with something like:
# http://dev-local-elasticbeanstalk.ap-southeast-2.elasticbeanstalk.com/
eb open
</code></pre>
<p>We should see our PHP Info page, and be able to see our database connection details at <code>/mysql.php</code>.</p>
<p>Once our Elastic Beanstalk cluster is running , we can ssh into our load balancer, and then telnet to our instances:</p>
<pre><code class="language-bash"># once sshed into the load balancer, you'll be presented with a choice of the
# EC2 instances if there are more than one.
eb ssh
sudo -s
yum install telnet
telnet mebooks-mysql-dbinstance.criieggarwwz.ap-southeast-2.rds.amazonaws.com
</code></pre>
<p>Once we know the public DNS of our ec2 instance, we can also directly ssh in as <code>ec2-user</code>:</p>
<pre><code class="language-bash">ssh -i ~/.ssh/aws-eb  ec2-user@ec2-52-62-4-141.ap-southeast-2.compute.amazonaws.com

# Once logged in, elevate to root to be able to acces docker:
sudo -s
</code></pre>
<h2 id="redeploying">Redeploying</h2>
<p>Presuming that we're only updating our custom <code>php-app</code> code, successive deployments should be a matter of the following:</p>
<pre><code class="language-bash"># create the image and push it to hub.docker
make app
</code></pre>
<p>As <code>make app</code> tagged our <code>Dockerrun.aws.json</code> file with the current version of the repo, we need to commit the changes, otherwise the <code>eb create</code> / <code>eb deploy</code> (which uses <code>git archive</code>) will use the previously-committed version of the <code>Dockerrun.aws.json</code> file.</p>
<pre><code class="language-bash">git add Dockerrun.aws.json
git commit -m &quot;Updated Docker image tag in Dockerrun.aws.json&quot;

eb deploy -v local-elasticbeanstalk
</code></pre>
<h2 id="terminating">Terminating</h2>
<p>Once we've finished with a particular environment, we can terminate it:</p>
<pre><code class="language-bash">eb terminate local-elasticbeanstalk
</code></pre>
<h2 id="cleaningup">Cleaning up</h2>
<p>Once we're finished, we can remove our containers locally, either by id or by name</p>
<pre><code class="language-bash">docker rm 832af3ff45d8 fc6a9553583f
</code></pre>
<p>If we're finished with our image, we can delete it:</p>
<pre><code class="language-bash">docker rmi mebooks/php-app
docker rmi mebooks/apache-php5
</code></pre>
<h2 id="summary">Summary</h2>
<p>Hopefuly the above is well-enough detailed to help you get to grips with Elastic Beanstalk.</p>
<p>It's a great mechanism for easily deploying a scalable Docker-based application, and all the better when we can automate the creation and deployment of our environments and applications.</p>
<p>In case using a <code>Makefile</code> as we did here becomes a bit limiting, you may want to look at using something like Ansible to automate the creation and deployment tasks, and there's<br>
a great example of this at <a href="https://github.com/hsingh/ansible-elastic-beanstalk">https://github.com/hsingh/ansible-elastic-beanstalk</a>.</p>
<p>Elastic Beankstalk is a very handy service, but should you want to create a more enterprise-level cloud application, you might also want to look at something like RedHat's <a href="https://www.openshift.org/">OpenShift Origin</a>.</p>
<p>However, being a developer rather than operations, I found the method outlined above<br>
suited me, as it was more Dev-ops (with a big 'D') rather than dev-Ops (with a big 'O').</p>
<h2 id="furtherreading">Further reading</h2>
<p>In putting together the above, I found these posts of help:</p>
<ul>
<li><a href="http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_docker-eblocal.html">http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_docker-eblocal.html</a></li>
<li><a href="http://victorlin.me/posts/2014/11/26/running-docker-with-aws-elastic-beanstalk">http://victorlin.me/posts/2014/11/26/running-docker-with-aws-elastic-beanstalk</a></li>
<li><a href="https://github.com/hopsoft/relay/wiki/How-to-Deploy-Docker-apps-to-Elastic-Beanstalk">https://github.com/hopsoft/relay/wiki/How-to-Deploy-Docker-apps-to-Elastic-Beanstalk</a></li>
<li><a href="http://www.sitepoint.com/docker-and-dockerfiles-made-easy/">http://www.sitepoint.com/docker-and-dockerfiles-made-easy/</a></li>
<li><a href="http://www.michaelgallego.fr/blog/2015/07/18/using-elastic-beanstalk-multi-container-with-php/">http://www.michaelgallego.fr/blog/2015/07/18/using-elastic-beanstalk-multi-container-with-php/</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Fast HTTPS using Pound, Varnish, LetsEncrypt and Pagespeed]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1458863602/winterbarrel_jjwsbg.jpg) -->
<p><small><em>Image courtesy of <a href="http://jaymantri.com/">Jay Mantri</a>.</em></small></p>
<h2 id="makinghttpsfast">Making https fast</h2>
<p><a href="https://letsencrypt.org/">Let’s Encrypt</a> is a great initiative allowing anyone to easily generate and install SSL certificates so that traffic can be securely served over https.</p>
<p>As well as gaining the benefits of secure traffic, by using https we're also setting ourselves up</p></div>]]></description><link>https://blog.mebooks.co.nz/fast-https-using-pound-varnish-letsencrypt-and-pagespeed/</link><guid isPermaLink="false">59d060bdc7843e0001a0ffdc</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Fri, 25 Mar 2016 03:27:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/winterbarrel_jjwsbg.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1458863602/winterbarrel_jjwsbg.jpg) -->
<img src="https://blog.mebooks.co.nz/content/images/2017/10/winterbarrel_jjwsbg.jpg" alt="Fast HTTPS using Pound, Varnish, LetsEncrypt and Pagespeed"><p><small><em>Image courtesy of <a href="http://jaymantri.com/">Jay Mantri</a>.</em></small></p>
<h2 id="makinghttpsfast">Making https fast</h2>
<p><a href="https://letsencrypt.org/">Let’s Encrypt</a> is a great initiative allowing anyone to easily generate and install SSL certificates so that traffic can be securely served over https.</p>
<p>As well as gaining the benefits of secure traffic, by using https we're also setting ourselves up to benefit from <a href="https://www.smashingmagazine.com/2016/02/getting-ready-for-http2/">HTTP/2</a>, which will allow a number of performance benefits, but is only available over https.</p>
<p>However, generating and installing SSL certificates using <a href="https://letsencrypt.org/">Let’s Encrypt</a> doesn't inherently make our site faster, and actually adds some overhead in terms of extra negotiation between the client and the server, as well as requiring the client to check with the certificate authorities to determine whether a certificate has been revoked (at least until we configure the server to take over this task using <a href="https://en.wikipedia.org/wiki/OCSP_stapling">OCSP stapling</a>.)</p>
<p>Two great measures for improving website performance are <a href="https://www.varnish-cache.org/">Varnish</a> and Google's <a href="https://developers.google.com/speed/pagespeed/module/">Pagespeed Module for Apache and Nginx</a>.</p>
<p>Below, we'll look at how to generate and install SSL certificates with Let’s Encrypt, and then how to configure our site so that we can still make use of Varnish and the Pagespeed Module to optimise our site performance.</p>
<p>We're assuming here that you've already got a site up and running using Varnish, and Apache/Nginx, serving http traffic on port 80.</p>
<h2 id="letsencrypt">Let’s Encrypt</h2>
<p><a href="https://letsencrypt.org/">Let’s Encrypt</a> is now in public beta, meaning that it's available for anyone to use, though you'll need to be aware of the <a href="https://community.letsencrypt.org/t/rate-limits-for-lets-encrypt/6769">rate-limiting</a> to prevent abuse of the service, the most relevant parts of which are:</p>
<ul>
<li>
<p><em>Names/Certificate</em> is the limit on how many domain names you can include in a single certificate. This is currently limited to 100 names, or websites, per certificate issued.</p>
</li>
<li>
<p><em>Certificates/Domain</em> you could run into through repeated re-issuance. This limit measures certificates issued for a given combination of Public Suffix + Domain (a &quot;registered domain&quot;). This is limited to 5 certificates per domain per week.</p>
</li>
</ul>
<p>To install and use Let’s Encrypt, we'll do the following (presuming you're running Ubunutu or Debian):</p>
<pre><code># Create a directory for letsencrypt
mkdir /usr/local/letsencrypt

# Clone letsencrypt from the repo.
# You may want to adjust the release tag
cd /usr/local/letsencrypt
git clone https://github.com/letsencrypt/letsencrypt.git#v0.4.2

# Stop any webservers
service apache2 stop
service nginx stop

# Run letsencrypt to create our certificate.
./letsencrypt-auto certonly --email whoever@gmail.com --agree-tos \
    -d whatever.nz -d www.whatever.nz \
    -d whatever.co.nz -d www.whatever.co.nz
</code></pre>
<p>Note that we stop our webservers before running letsencrypt, as it needs to use ports 80 and 443 for communication with the certificate authorities.</p>
<p>We're also using <code>--agree-tos</code> to automatically agree to the terms of service, and <code>--email</code> to provide our email so that letsencrypt can create our account. We actually use <a href="https://www.ansible.com/">Ansible</a> configuration management scripts for provisioning our webservers and so need these scripts to run automatically, but in the interests of simplicity, we're not showing that here.</p>
<p>Letsencrypt can include a number of domains on a given certificate (currently up to a 100), though it can't produce wildcard certificates, and we use the <code>-d</code> option above to specify each domain.</p>
<p>Note also that we're using the <code>certonly</code> function; letsencrypt can automatically update webserver configuration files to use the newly generated certificate, but in our case we're going to use <a href="http://www.apsis.ch/pound/">Pound</a> to handle https, and our webservers will therefore only be dealing with http traffic.</p>
<p>More information about running letsencrypt can be found on <a href="https://letsencrypt.org/getting-started/">the letsencrypt site</a>.</p>
<p>Letsencypt certificates are currently valid for 90 days, and in the interests of making life easier for ourselves, we'll set up a cron job to automatically attempt to renew the certificate on a weekly basis. Our bash script, <code>/usr/local/letsencrypt/letsencrypt-auto-renew.sh</code>, is as follows:</p>
<pre><code>#!/bin/bash

# letsencrypt needs access to port 80
monit stop pound

# To simulate a dry-run near the end of the certificate term:
# /usr/local/letsencrypt/letsencrypt-auto renew --dry-run --email mebooks.support@gmail.com --agree-tos
if ! /usr/local/letsencrypt/letsencrypt-auto renew --email mebooks.support@gmail.com --agree-tos &gt; /var/log/le-renew.log 2&gt;&amp;1; then
    echo Automated renewal failed:
    cat /var/log/le-renew.log
    exit 1
fi

# Restart pound
monit start pound

# Cat the privkey and fullchain for pound
cat /etc/letsencrypt/live/whatever.co.nz/privkey.pem /etc/letsencrypt/live/whatever.co.nz/fullchain.pem &gt; /etc/letsencrypt/archive/whatever.co.nz/privkey_fullchain.pem

# Recreate the symlink
unlink /etc/letsencrypt/live/whatever.co.nz/privkey_fullchain.pem
ln -s /etc/letsencrypt/archive/whatever.co.nz/privkey_fullchain.pem /etc/letsencrypt/live/whatever.co.nz/privkey_fullchain.pem
</code></pre>
<p>We can ensure that this runs on a weekly frequency by entering a line in our cron file, using <code>crontab -e</code>:</p>
<pre><code># m h  dom mon dow   command
 30 2  *   *   1     /usr/local/letsencrypt/letsencrypt-auto-renew.sh &gt;&gt; /var/log/le-renew.log
</code></pre>
<p>So, we've now got a newly-generated certificate installed, but we actually need to make some use of it.</p>
<h2 id="varnish">Varnish</h2>
<p><a href="https://www.varnish-cache.org/">Varnish</a> provides a great http caching layer for websites, and is probably the single most important mechanism for ensuring that your site can serve traffic quickly to a large audience.</p>
<p>However, Varnish only serves traffic via http, and this comment from the maintainer makes it pretty clear it's <a href="https://www.varnish-cache.org/docs/3.0/phk/ssl.html">unlikely it will ever have support for https</a>:</p>
<blockquote>
<p>Would I be able to write a better stand-alone SSL proxy process than the many which already exists ?</p>
</blockquote>
<blockquote>
<p>Probably not, unless I also write my own SSL implementation library, including support for hardware crypto engines and the works.</p>
</blockquote>
<blockquote>
<p>That is not one of the things I dreamt about doing as a kid and if I dream about it now I call it a nightmare.</p>
</blockquote>
<p>So, if we want to use https, we have to use another service in front of Varnish to proxy https traffic as http to and from Varnish.</p>
<p>If you are using Varnish to directly cache traffic on port 80, your <code>/etc/default/varnish</code> would probably have a line that looks like:</p>
<pre><code>DAEMON_OPTS=&quot;-a :80 \
</code></pre>
<p>However, as we'll now use Pound to receive traffic in front of Varnish, we'll ensure that Varnish is receiving traffic on its default port  of 6081:</p>
<pre><code>DAEMON_OPTS=&quot;-a :6081 \
</code></pre>
<h2 id="enterpound">Enter Pound</h2>
<p><a href="http://www.apsis.ch/pound/">Pound</a> is a great project, and works very nicely in front of Varnish as reverse proxy, accepting http and/or https traffic as configured, and passing it through to Varnish as http.</p>
<p>In order to be able to disable the SSLv3 protocol to cope with the <a href="https://www.digitalocean.com/community/tutorials/how-to-protect-your-server-against-the-poodle-sslv3-vulnerability">POODLE vulnerability</a>, we'll need to use Pound 2.7f, as <a href="http://www.apsis.ch/pound/pound_list/archive/2015/2015-11/1447518215000">outlined here</a>. Currently, the standard Ubuntu repositories only make Pound 2.6 available.</p>
<pre><code># Add the PPA
sudo add-apt-repository 'deb https://mslinn-ppa.s3.amazonaws.com stable main'

# Update our apt-get cache
sudo apt-get update

# Check that we see: Candidate:2.7f-0ubuntu1
sudo apt-cache policy pound

# Install Pound
sudo apt-get install pound
</code></pre>
<p>Once installed, we'll use the following <code>/etc/pound/pound.cfg</code>:</p>
<pre><code>## Minimal sample pound.cfg
##
## see pound(8) for details
######################################################################
## global options:

User            &quot;www-data&quot;
Group           &quot;www-data&quot;
#RootJail       &quot;/chroot/pound&quot;

## Logging: (goes to syslog by default)
##      0       no logging
##      1       normal
##      2       extended
##      3       Apache-style (common log format)
LogLevel        1

## check backend every X secs:
Alive           30

## use hardware-accelleration card supported by openssl(1):
#SSLEngine      &quot;&lt;hw&gt;&quot;

# poundctl control socket
Control &quot;/var/run/pound/poundctl.socket&quot;
######################################################################
## listen, redirect and ... to:

ListenHTTP
    # 0.0.0.0 below should be your public IP address
    Address  0.0.0.0
    Port     80
    # This part makes sure you redirect all HTTP traffic to HTTPS
    Service
        HeadRequire &quot;Host: whatever.co.nz&quot;
        Redirect 301 &quot;https://whatever.co.nz&quot;
    End
End

# As per https://milos.jakovljevic.me/howto-lets-encrypt-ssl-with-varnish-and-pound-on-ubuntu-server/
ListenHTTPS
        HeadRemove &quot;X-Forwarded-Proto&quot;
        AddHeader  &quot;X-Forwarded-Proto: https&quot;
        Address    {{ public_ip }}
        Port       443
        Cert       &quot;/etc/letsencrypt/live/whatever.co.nz/privkey.pem&quot;
        # http://permalink.gmane.org/gmane.comp.web.pound.general/7489
        Disable SSLv2
        Disable SSLv3
        SSLAllowClientRenegotiation 0
        SSLHonorCipherOrder 1
        # We want to get an A on the Qualys SSL Test (https://www.ssllabs.com/ssltest)
        # https://scotthelme.co.uk/a-plus-rating-qualys-ssl-test/
        Ciphers     &quot;ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS&quot;
        # Ensure pound doesn't rewrite location headers, as this can cause a redirect loop
        RewriteLocation 0
        Service
                BackEnd
                        Address 127.0.0.1
                        Port    6081
                End
        End
End

# Get pound to do our hostname redirects (whether they come in as HTTP or HTTPS)
Service
        HeadRequire &quot;^Host: www.whatever.co.nz$&quot;
        Redirect 301 &quot;https://whatever.nz&quot;
End
</code></pre>
<p>Note the following:</p>
<ul>
<li>We're using the <code>ListenHTTP</code> block to listen for http traffic on port 80, and redirect it (permanently) to port 443.</li>
<li>We use the <code>ListenHTTPS</code> block to listen for https traffic on port 443, and direct it to Varnish on port 6081</li>
<li>We disable the SSLv2 and SSLv3 protocols using <code>Disable SSLv2</code> and <code>Disable SSLv3</code></li>
<li>We disallow client renegotiation of the SSL protocol using <code>SSLAllowClientRenegotiation</code>, in order to stop attackers trying to demote a secure connection to a less secure connection</li>
<li>We use <code>SSLHonorCipherOrder</code> to ensure our ciphers are applied in the order that we specify them; this allows us to ensure that the browsers will use the most secure first, if they have support for it</li>
<li>We specify our allowed ciphers using <code>Ciphers</code></li>
<li>We use <code>RewriteLocation</code> to stop Pound rewriting location headers, as this can lead to redirect loops</li>
<li>We use Pound to do our hostname redirects (whether they come in as HTTP or HTTPS), redirecting <a href="http://www.whatever.co.nz">www.whatever.co.nz</a> to <a href="http://whatever.co.nz">http://whatever.co.nz</a>.</li>
</ul>
<p>Note that, in order to avoid the POODLE vulnerability, we're disabling the SSLv3 protocol, and not just the SSLv3 cipher. There appears to be some confusion, with some thinking that disabling the SSLv3 cipher is enough to avoid POODLE, but that's <a href="http://permalink.gmane.org/gmane.comp.web.pound.general/7489">not the case</a>.</p>
<p>Our <code>Ciphers</code> is a list of reasonably modern ciphers, ordered from most-secure to least-secure:</p>
<pre><code>ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
</code></pre>
<p>We don't provide support for ciphers such as RC4, as this is considered too weak. This means people using Internet Explorer 6 won't be able to access our site, but that's not really a problem these days.</p>
<p>We're using Scott Helme's <a href="https://scotthelme.co.uk/a-plus-rating-qualys-ssl-test/">article on Getting an A+ rating on the Qualys SSL Test</a> to ensure that we follow good practice regarding security. The <a href="https://www.ssllabs.com/ssltest">Qualsys SSLTest</a> is a great resource to determine how secure your site is, and using the above ciphers and SSLv2/SSLv3 migitations, we're able to acheive a solid <strong>A</strong> grade.</p>
<p><img src="https://res.cloudinary.com/mebooks/image/upload/v1458869320/qualsys_lz5zzb.jpg" alt="Fast HTTPS using Pound, Varnish, LetsEncrypt and Pagespeed"></p>
<p>As security good practice evolves, you'll want to revisit the <a href="https://www.ssllabs.com/ssltest">Qualsys SSLTest</a> and ensure that you're continuing to provide a secure site.</p>
<p>As we're going to be using Pound to listen for traffic on port 443, we need to ensure that our webserver doesn't bind to this port.</p>
<p>For Apache, our <code>/etc/apache2/ports.conf</code> will look like:</p>
<pre><code># Varnish serves traffic to Apache on port 8080
Listen 8080

&lt;IfModule ssl_module&gt;
    # We'll park Apache's SSL to listen on 8443
    Listen 8443
&lt;/IfModule&gt;

&lt;IfModule mod_gnutls.c&gt;
    # We'll park Apache's SSL to listen on 8443
    Listen 8443
&lt;/IfModule&gt;
</code></pre>
<p>You'll want to ensure that your Apache config changes are valid before restarting Apache:</p>
<pre><code>apache2ctl configtest
</code></pre>
<p>In order to activate Pound, we need to enable it:</p>
<pre><code># Check that our pound.cfg is valid
pound -c

# Remove the pound default startup state in /etc/default/pound
sed -i '/startup=0/startup=1/' /etc/default/pound

# Start pound 
service pound start
</code></pre>
<h2 id="pagespeedmodule">Pagespeed module</h2>
<p>We've now got pound enforcing that all traffic is https, and working nicely with Varnish, which continues to cache our site content.</p>
<p>We've not had to make any changes to our Apache or Nginx site vhost configs, as they're still seeing traffic as http.</p>
<p>Google's <a href="https://developers.google.com/speed/pagespeed/module/">Pagespeed Module for Apache and Nginx</a> is a great drop-in webserver module for speeding up our site by making a number of on-the-fly optimisations. It's a topic in itself, and it's worth reading through the documentation to understand how it works and how it can benefit you.</p>
<pre><code>sudo dpkg -i mod-pagespeed-*.deb
sudo apt-get -f install
</code></pre>
<p>This results in the Pagespeed module being installed, and since we're using Apache, we'll find the Pagespeed configuration at <code>/etc/apache2/mods-enabled/pagespeed.conf</code>.</p>
<p>We'll want to make the following changes in <code>/etc/apache2/mods-enabled/pagespeed.conf</code>:</p>
<ul>
<li>Ensure it's on: <code>ModPagespeed on</code></li>
<li>Respect <strong>X-Forwarded-Proto headers</strong>: <code>ModPagespeedRespectXForwardedProto on</code></li>
</ul>
<p>As Pagespeed is seeing all traffic coming from Varnish as http, it will normally serve any optimisations accordingly as http. However, this will cause <code>Mixed content</code> warnings in our browser, as http assets are being served on an https site.</p>
<p>Using <code>ModPagespeedRespectXForwardedProto on</code> ensures that Pagespeed checks for the <code>X-Forwarded-Proto: https</code> header added by Pound, and serves any associated optimisations as https.</p>
<h2 id="summary">Summary</h2>
<p>We've glossed over a few things above, as it's presumed that you've already installed and are using Varnish and a web-server such as Apache or Nginx to serve traffic on port 80.</p>
<p>However, the above is hopefully easy enough to follow, and will help you to serve your site securely using <a href="https://letsencrypt.org/">Let’s Encrypt</a> and <a href="http://www.apsis.ch/pound/">Pound</a>, and fast, using <a href="https://www.varnish-cache.org/">Varnish</a> and Google's <a href="https://developers.google.com/speed/pagespeed/module/">Pagespeed Module for Apache and Nginx</a>.</p>
<p>More notes about configuring Pound and Varnish can be found at the following:</p>
<ul>
<li><a href="http://www.geoffstratton.com/2013/11/web-server-performance-part-ii-varnish-pound/">Web Server Performance Part II: Varnish and Pound</a></li>
<li><a href="https://secwise.nl/lets-encrypt-certifcates-and-pound-load-balancer/">Let's Encrypt Certificates and Pound Load Balancer</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Setting Up Your Own PAAS Using Dokku]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1456648995/uk-landscape_hmdudv.jpg) -->
<p>These are notes about how we setup Dokku on EC2 with <a href="https://www.vagrantup.com/">Vagrant</a>, <a href="http://docker.com/">Docker</a> and <a href="http://dokku.viewdocs.io/dokku/">Dokku</a>.</p>
<blockquote>
<p>Note: an earlier version of this post mentioned that the maintainers of Dokku worked at Deis, however that's incorrect — previously Deis sponsored the project, but this has now finished, and none of the maintainers work</p></blockquote></div>]]></description><link>https://blog.mebooks.co.nz/setting-up-your-own-paas-using-dokku/</link><guid isPermaLink="false">59d06136c7843e0001a0ffdd</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Thu, 25 Feb 2016 03:29:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/uk-landscape_hmdudv.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1456648995/uk-landscape_hmdudv.jpg) -->
<img src="https://blog.mebooks.co.nz/content/images/2017/10/uk-landscape_hmdudv.jpg" alt="Setting Up Your Own PAAS Using Dokku"><p>These are notes about how we setup Dokku on EC2 with <a href="https://www.vagrantup.com/">Vagrant</a>, <a href="http://docker.com/">Docker</a> and <a href="http://dokku.viewdocs.io/dokku/">Dokku</a>.</p>
<blockquote>
<p>Note: an earlier version of this post mentioned that the maintainers of Dokku worked at Deis, however that's incorrect — previously Deis sponsored the project, but this has now finished, and none of the maintainers work at or have worked at Deis.</p>
</blockquote>
<blockquote>
<p>Also, it appears that a custom Buildpack is no longer needed for releasing a static site (though use of the plugin mentioned below won't break the deploy)</p>
</blockquote>
<h2 id="whatisdokku">What is Dokku?</h2>
<p><a href="http://dokku.viewdocs.io/dokku/">Dokku</a> allows us to spin up our own Platform-as-a-Service (PaaS) and create a <a href="https://heroku.com/">Heroku-style</a> environment that's cheap, and that we can easily deploy to simply by performing a <code>git push</code>.</p>
<p>Under the covers, Dokku uses Nginx to serve sites, creating a reverse proxy Nginx config for each app that we deploy. Dokku also installs a command-line tool that allows us to configure and stop / start our applications.</p>
<p>We can deploy apps written using a range of technologies, including Node.js, PHP, Ruby-on-rails, and even static sites, and Dokku supports a variety of mechanisms to determine how to deploy an app, including:</p>
<ul>
<li>the presence of a <code>package.json</code> file, letting Dokku know that it's dealing with a node.js app</li>
<li>Procfiles</li>
<li>Docker files</li>
<li>Heroku-style Buildpacks</li>
</ul>
<h2 id="dokkuversusdokkualt">Dokku versus Dokku-alt</h2>
<p>Our approach described below is based on <a href="http://blog.clearbit.com/ec2-heroku/">the tutorial by Alex MaCaw at ClearBit</a>, but we'll amend this to use <a href="http://dokku.viewdocs.io/dokku/">Dokku</a> rather than <a href="https://dokku-alt.github.io/">Dokku-alt</a>.</p>
<p>Dokku-alt arose because people wanted Dokku installed complete with a range of verified plugins, and because the Dokku project wasn't being maintained after the founder, Jeff Lindsay, stepped aside.</p>
<p>However, the situation has now reversed, with Dokku receiving active support from it's new maintainers, and Dokku-alt now a number of versions behind Dokku.</p>
<p>Jeff Lindsay described his experience with <a href="http://progrium.com/blog/2015/12/04/leadership-guilt-and-pull-requests/">stepping aside from Dokku</a> and he left the new maintainers a <a href="https://github.com/dokku/dokku/wiki/Refactoring">roadmap for future development</a> which the maintainers have been following.</p>
<p>Recent Dokku features include:</p>
<ul>
<li><a href="http://dokku.viewdocs.io/dokku/checks-examples/#zero-downtime-deploys">zero-downtime deployment</a>, with Dokku waiting a configurable amount of time before routing traffic to from your old container to your newly-deployed container</li>
<li>an increasing <a href="http://dokku.viewdocs.io/dokku/plugins/#official-plugins-beta">range of plugins</a></li>
<li>the ablity to specify <a href="http://dokku.viewdocs.io/dokku/checks-examples/#checks-examples">deploy-time checks</a> (such as text appearing at a given URL) to ensure that<br>
the deploy worked correctly</li>
<li>the ability to rename a deployed app</li>
</ul>
<h2 id="amazonec2instancesetup">Amazon EC2 instance setup</h2>
<p>We presume that you've already setup an AWS account, and have retrieved your <code>AWS_ACCESS_KEY_ID</code> and your <code>AWS_SECRET_ACCESS_KEY</code>.</p>
<p>We need to create our <a href="https://console.aws.amazon.com/ec2/v2/home#KeyPairs:">EC2 keypair</a> as <code>aws</code>, and save the <code>aws.pem</code> to <code>~/.ssh/aws.pem</code>.</p>
<p>Then, we create our <a href="https://console.aws.amazon.com/ec2/v2/home#SecurityGroups:">EC2 Security Group</a> as <code>production</code>, adding the rules to allow incoming traffic from port 22 (SSH) and port 80 (HTTP).</p>
<p>Next, set environment variables for our <code>AWS_ACCESS_KEY_ID</code> and our <code>AWS_SECRET_ACCESS_KEY</code>:</p>
<pre><code>export AWS_ACCESS_KEY_ID=WHATEVER
export AWS_SECRET_ACCESS_KEY=WHICHEVER
</code></pre>
<p>These can be set in a <code>.bash_profile</code> or similar.</p>
<p>Create a new <code>t2.micro</code> instance — in our case we'll use the region <code>ap-southeast-2</code>.<br>
Then, in the AWS dashboard, access the instance, and check the public IP address, which we'll need later to set our DNS:</p>
<pre><code>52.62.179.58
</code></pre>
<p>Note that, depending on the apps you'll be deploying, you may need to upgrade to a larger EC2 instance.</p>
<p>Now, we need to determine the AMI instance to use. Use the <a href="http://cloud-images.ubuntu.com/locator/ec2/">Amazon EC2 AMI Locator</a><br>
to determine the correct ami-id — scroll down to the bottom of the locator and use the dropdowns to filter the available instances.</p>
<p>We'll need one that has:</p>
<ul>
<li>region: ap-southeast-2</li>
<li>version: 14.04 LTS</li>
<li>instance type: hvm:ebs</li>
</ul>
<p>Originally, when I ran the <code>Vagrantfile</code> below, I received the following message:</p>
<pre><code>InvalidParameterValue =&gt; Value () for parameter groupId is invalid
</code></pre>
<p>The problem turned out to be that I was specifying an <code>aws.ami</code> id that was not valid for the specified <code>aws.region</code> — using the <a href="http://cloud-images.ubuntu.com/locator/ec2/">Amazon EC2 AMI Locator</a> allowed me to find a suitable AMI id.</p>
<p>Add an entry accordingly to our <code>/etc/hosts</code> file — here we presume we'll be using the domain <code>dokku.me</code>, as we'll be using our hosts file to specify our DNS, but you can use a real (i.e. registered) domain if you'd like to make your dokku box accessiable via the public web.</p>
<pre><code>52.62.179.58	dokku.me
</code></pre>
<h2 id="vagrantsetup">Vagrant setup</h2>
<p>Install the <a href="https://github.com/mitchellh/vagrant-aws">vagrant plugin for aws</a>:</p>
<pre><code>vagrant plugin install vagrant-aws
</code></pre>
<p>We'll retrieve the following <code>Vagrantfile</code>:</p>
<pre><code>curl https://gist.githubusercontent.com/jcdarwin/e1991320f1c4f3d1db2f/raw/d5e2796a3c632ec63e8a503501ead7af5744d6c2/Vagrantfile &gt; Vagrantfile
</code></pre>
<p>Our <code>Vagrantfile</code> is as follows:</p>
<pre><code>Vagrant::configure('2') do |config|
	config.vm.define :dokku, autostart: false do |box|
		box.vm.box      = 'trusty'
		box.vm.box_url  = 'https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box'
		box.vm.hostname = 'dokku.me'

		box.vm.provider :aws do |aws, override|
			aws.access_key_id     = ENV['AWS_ACCESS_KEY_ID']
			aws.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
			aws.keypair_name      = 'aws'

			# As per the Amazon EC2 AMI Locator
			aws.ami               = 'ami-4f32142c'
			aws.region            = 'ap-southeast-2'
			aws.instance_type     = 't2.micro'

			# Customize this to the id of the security group you're using
			aws.security_groups   = 'production'

			# To mount EBS volumes
			aws.block_device_mapping = [
				{
					:DeviceName =&gt; &quot;/dev/sdb&quot;,
					:VirtualName =&gt; &quot;ephemeral0&quot;
				},
				{
					:DeviceName =&gt; &quot;/dev/sdc&quot;,
					:VirtualName =&gt; &quot;ephemeral1&quot;
				}
			]

			override.ssh.username = 'ubuntu'

			# Customize this to your AWS keypair path
			override.ssh.private_key_path = '~/.ssh/aws.pem'
		end

		# To make sure we use EBS for our tmp files
		box.vm.provision &quot;shell&quot; do |s|
			s.privileged = true
			s.inline = %{
				mkdir -m 1777 /mnt/tmp
				echo 'export TMPDIR=/mnt/tmp' &gt; /etc/profile.d/tmpdir.sh
			}
		end

		# To make sure packages are up to date
		box.vm.provision &quot;shell&quot; do |s|
			s.privileged = true
			s.inline = %{
				export DEBIAN_FRONTEND=noninteractive
				apt-get update
				apt-get --yes --force-yes upgrade
			}
		end

		# Install dokku as per http://dokku.viewdocs.io/dokku/getting-started/install/debian/
		box.vm.provision &quot;shell&quot; do |s|
			s.privileged = true
			s.inline = %{
				export DEBIAN_FRONTEND=noninteractive

				echo &quot;install prerequisites&quot;
				sudo apt-get update -qq &gt; /dev/null
				sudo apt-get install -qq -y apt-transport-https

				echo &quot;setup unattended installation&quot;
				echo &quot;dokku dokku/vhost_enable boolean true&quot; | sudo debconf-set-selections
				echo &quot;dokku dokku/hostname string dokku.me&quot; | sudo debconf-set-selections

				echo &quot;install docker&quot;
				sudo apt-get install lxc wget bsdtar curl
				sudo apt-get install linux-image-extra-$(uname -r)
				sudo modprobe aufs
				sudo usermod -aG docker ubuntu
				wget -nv -O - https://get.docker.com/ | sh

				echo &quot;install dokku&quot;
				wget -nv -O - https://packagecloud.io/gpg.key | apt-key add -
				echo &quot;deb https://packagecloud.io/dokku/dokku/ubuntu/ trusty main&quot; | sudo tee /etc/apt/sources.list.d/dokku.list
				sudo apt-get update -qq -y &gt; /dev/null
				sudo apt-get install -qq -y dokku
				sudo dokku plugin:install-dependencies --core
                sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
			}
		end
	end
end
</code></pre>
<p>Note that our <code>box_url</code> is a blank Vagrant box to coax Vagrant into working with AWS — we can add our box manually:</p>
<pre><code>vagrant box add dummy https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box
</code></pre>
<p>Inspect the metadata:</p>
<pre><code>cat ~/.vagrant.d/boxes/dummy/0/aws/metadata.json
</code></pre>
<p>We should see something like:</p>
<pre><code>{
    &quot;provider&quot;: &quot;aws&quot;
}
</code></pre>
<h2 id="createourec2instanceusingvagrant">Create our EC2 instance using Vagrant</h2>
<p>Start up our VM:</p>
<pre><code>vagrant up dokku --provider=aws
</code></pre>
<p>Our <code>Vagrantfile</code> will create the EC2 instance, and run the provisioning script.</p>
<p>Once Vagrant finishes provisioning the box, you should be able to ssh into it using Vagrant:</p>
<pre><code>vagrant ssh dokku
</code></pre>
<p>Note that if you experience any problems with the provisioning, you may need to check that Dokku and the core plugins installed correctly:</p>
<pre><code>sudo apt-get install dokku
sudo dokku plugin:install-dependencies --core
</code></pre>
<p>While in our box, change <code>/home/dokku/HOSTNAME</code> and <code>/home/dokku/VHOST</code> from the Amazon-generated name (e.g. <code>ip-172-31-3-31.ap-southeast-2.compute.internal</code>) to our domain:</p>
<pre><code>dokku.me
</code></pre>
<p>We can now <code>exit</code> from our ssh session.</p>
<h2 id="completedokkusetup">Complete Dokku setup</h2>
<p>We should now be able to finish the Dokku setup using Dokku's web interface at <a href="http://dokku.me">http://dokku.me</a></p>
<ul>
<li>change our hostname from the IP address to our domain: <code>dokku.me</code></li>
<li>choose <code>Use virtualhost naming for apps</code></li>
</ul>
<h2 id="setupdirectsshaccess">Setup direct ssh access</h2>
<p>We want to be able to ssh into our box without relying on Vagrant.</p>
<p>First, register our normal public key with our box:</p>
<pre><code>cat ~/.ssh/id_rsa.pub | vagrant ssh dokku -- sudo sshcommand acl-add dokku ${USER}
</code></pre>
<p>Check the ssh config for our box:</p>
<pre><code>vagrant ssh-config dokku

Host dokku
  HostName ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com
  User ubuntu
  Port 22
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/jasondarwin/.ssh/aws.pem
  IdentitiesOnly yes
  LogLevel FATAL
</code></pre>
<p>We can use the reported <code>HostName</code> for direct ssh access to our box:</p>
<pre><code># This will print out the Dokku help menu
ssh dokku@ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com

# We can run commands over ssh
ssh dokku@ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com version
</code></pre>
<h2 id="deployourapp">Deploy our app</h2>
<p>We'll use the <a href="https://github.com/heroku/node-js-sample">heroku node-js-sample app</a> to prove that we can deploy an app to our dokku box:</p>
<pre><code># Clone the repo
git clone https://github.com/heroku/node-js-sample
cd node-js-sample

# Add our Dokku box as a remote:
git remote add dokku dokku@ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com:&lt;our app name&gt;

# Push our repo to our box and set off the deploy
git push dokku master
</code></pre>
<p>Add an entry to our hosts file for our app:</p>
<pre><code>52.62.179.58	node-js-sample.dokku.me
</code></pre>
<p>We should now be able to access our app at <a href="http://node-js-sample.dokku.me">http://node-js-sample.dokku.me</a></p>
<pre><code>dokku config:set --no-restart node-js-sample DOKKU_LETSENCRYPT_EMAIL=&lt;email&gt;
dokku letsencrypt node-js-sample
</code></pre>
<p>More notes about application deployments can be found in <a href="http://dokku.viewdocs.io/dokku/application-deployment/">the Dokku documentation</a>.</p>
<h2 id="further">Further</h2>
<ul>
<li>Dokku has support for <a href="https://github.com/progrium/dokku/wiki/Plugins">plugins</a> which allow<br>
functionality such as database setup and access.</li>
<li>Note also that web applications, like the sample nodejs app, are expected to be exposed through port 5000 unless an environment variable is specified.</li>
<li>We can use <a href="https://www.florianheinemann.com/github/dokku/2014/11/17/Hosting-static-pages-on-Dokku.html">Florian Heinemann's custom<br>
buildpack</a> to deploy a static site.</li>
<li>Note that we can run docker directly on our box, e.g.<br>
vagrant ssh dokku<br>
docker run hello-world</li>
<li>Check the <a href="http://dokku.viewdocs.io/dokku/installation/">Dokku docs</a> for further information</li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Automating Linux Package Upgrades Using Cron Apt and Postfix]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1428219695/sunset_cropped_jlnfha.jpg) -->
<p>As the number of servers under our control grows, we want to be able to improve our control over package upgrades.</p>
<p>The problems here are two-fold:</p>
<ul>
<li>we want to know which packages are ready to be installed</li>
<li>we want to have these packages already downloaded so we can install them</li></ul></div>]]></description><link>https://blog.mebooks.co.nz/automating-linux-package-upgrades-using-cron-apt-and-postfix/</link><guid isPermaLink="false">59d061aac7843e0001a0ffde</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Sun, 05 Apr 2015 04:31:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/sunset_cropped_jlnfha.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1428219695/sunset_cropped_jlnfha.jpg) -->
<img src="https://blog.mebooks.co.nz/content/images/2017/10/sunset_cropped_jlnfha.jpg" alt="Automating Linux Package Upgrades Using Cron Apt and Postfix"><p>As the number of servers under our control grows, we want to be able to improve our control over package upgrades.</p>
<p>The problems here are two-fold:</p>
<ul>
<li>we want to know which packages are ready to be installed</li>
<li>we want to have these packages already downloaded so we can install them when we're ready</li>
</ul>
<p>However, we don't want to fully automate the upgrading of these packages, as:</p>
<ul>
<li>there may be packages that want to overwrite existing configuration, so we'll need to figure out what the best couse of action is</li>
<li>there may be packages that we don't want to install</li>
</ul>
<p>As a solution, we'll use <code>cron-apt</code> in conjunction with the <code>postfix</code> mail server to inform us of package updates that are ready to apply.</p>
<h2 id="cronapt">cron-apt</h2>
<p><code>cron-apt</code> allows us to automate <code>apt-get</code> commands. The documentation is a little sparse, though some writeups do exist <a href="http://www.the-art-of-web.com/system/cron-apt-wheezy/">here</a> and <a href="https://code.google.com/p/mycodedump/wiki/CronApt">here</a>.</p>
<p>Installing <code>cron-apt</code>:</p>
<pre><code>apt-get install cron-apt
</code></pre>
<p>In <code>/etc/cron-apt/config</code>:</p>
<pre><code># Configuration for cron-apt. For further information about the possible
# configuration settings see /usr/share/doc/cron-apt/README.gz.
MAILTO=&quot;mebooks.support@gmail.com&quot;
#Send us an email when upgrades are readt to apply
MAILON=&quot;upgrade&quot;
</code></pre>
<p>We'll want to make sure that the cron job is set up correctly in <code>/etc/cron.d/cron-apt</code>:</p>
<pre><code>#
# Regular cron jobs for the cron-apt package
#
# Every night at 4 o'clock.
0 4 * * *   root    test -x /usr/sbin/cron-apt &amp;&amp; /usr/sbin/cron-apt
# Every hour.
# 0 *   * * *   root    test -x /usr/sbin/cron-apt &amp;&amp; /usr/sbin/cron-apt /etc/cron-apt/config2
# Every five minutes.
# */5 * * * *   root    test -x /usr/sbin/cron-apt &amp;&amp; /usr/sbin/cron-apt /etc/cron-apt/config2
</code></pre>
<h2 id="postfix">postfix</h2>
<p>Possibly the most difficult part of setting up <code>cron-apt</code> is configuring the mail server to allow <code>cron-apt</code> to send out its emails about packages ready to be upgraded (presuming you don't already have a mail server set up).</p>
<p>In our case, we'll configure <code>postfix</code> to <a href="http://help.mandrill.com/entries/23060367-Can-I-configure-Postfix-to-send-through-Mandrill-">route outbound emails</a> via <a href="https://mandrill.com/">Mandrill</a>, an SMTP email relaying service.</p>
<p>Our <code>postfix</code> configuration file (as generated by our ansible role) <code>/etc/postfix/main.cf</code> looks like the following:</p>
<pre><code># Ansible managed: /Users/jasondarwin/workspace/ansible-digitalocean/roles/postfix/templates/main-cf.j2 modified on 2015-04-03 15:37:33 by jasondarwin on hare.lan

smtpd_banner = $myhostname ESMTP $mail_name
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no

# Uncomment the next line to generate &quot;delayed mail&quot; warnings
#delay_warning_time = 4h

readme_directory = no

# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

# Enable SASL authentication
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_use_tls = yes

# General
myhostname = host1
myorigin = $mydomain
mydestination =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = loopback-only
relayhost = [smtp.mandrillapp.com]
inet_protocols = ipv4
sender_canonical_maps = hash:/etc/postfix/sender_canonical
</code></pre>
<p>Note the following:</p>
<ol>
<li>
<p>We use SASL authentication to authenticate with Mandrill, so have a <code>/etc/postfix/sasl_passwd</code> which contains something like:</p>
<pre><code> [smtp.mandrillapp.com]    jcdarwin@gmail.com:WHATEVERYOURAPIKEYIS
</code></pre>
</li>
</ol>
<p>This file is compiled as follows:</p>
<pre><code>    /usr/sbin/postmap /etc/postfix/sasl_passwd
</code></pre>
<ol start="2">
<li>
<p>We specify Mandrill as our <code>relayhost</code>:</p>
<pre><code> relayhost = [smtp.mandrillapp.com]
</code></pre>
</li>
<li>
<p>We restrict the protocol used to IPV4, as Mandrill doesn't yet seem to support IPV6:</p>
<pre><code> inet_protocols = ipv4
</code></pre>
</li>
<li>
<p>We use <code>sender_canonical_maps</code> to specify the email sender address.</p>
<pre><code> sender_canonical_maps = hash:/etc/postfix/sender_canonical
</code></pre>
<p>and this file contains something like</p>
<pre><code> root root@mebooks.co.nz
</code></pre>
<p>This file is compiled as follows:</p>
<pre><code> /usr/sbin/postmap /etc/postfix/sender_canonical
</code></pre>
<p>If this file didn't exist, <code>cron-apt</code> will send email as <code>root</code>, meanign our <code>From</code> address would look like <code>root@localdomain</code>, and Mandrill will refuse to relay these messages.</p>
</li>
</ol>
<h3 id="testingpostfix">Testing postfix</h3>
<p>We can use <code>sendmail</code> to test postfix:</p>
<pre><code>sendmail mebooks.support@gmail.com
Here is the message body
CTRL+D
</code></pre>
<p>As no <code>From:</code> is specified, postfix should use the address specified for <code>root</code> in <code>/etc/postfix/sender_canonical</code>, and we should be able to see this in our mail log by running <code>cat /var/log/mail.log</code>:</p>
<pre><code>Apr  5 02:17:32 mebooks1 postfix/qmgr[26754]: 95A76142FF1: removed
Apr  5 02:20:06 mebooks1 postfix/master[26750]: terminating on signal 15
Apr  5 02:20:07 mebooks1 postfix/master[26940]: daemon started -- version 2.11.0, configuration /etc/postfix
Apr  5 02:20:21 mebooks1 postfix/pickup[26943]: 2529F142FF1: uid=0 from=&lt;root&gt;
Apr  5 02:20:21 mebooks1 postfix/cleanup[26951]: 2529F142FF1: message-id=&lt;20150405062021.2529F142FF1@host1&gt;
Apr  5 02:20:21 mebooks1 postfix/qmgr[26944]: 2529F142FF1: from=&lt;root@mebooks.co.nz&gt;, size=250, nrcpt=1 (queue active)
Apr  5 02:20:21 mebooks1 postfix/smtp[26953]: 2529F142FF1: to=&lt;mebooks.support@gmail.com&gt;, relay=smtp.mandrillapp.com[52.74.52.47]:25, delay=6.8, delays=6.8/0.04/0.03/0.01, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 5B9082A00BD)
Apr  5 02:20:21 mebooks1 postfix/qmgr[26944]: 2529F142FF1: removed
</code></pre>
<p>If nothing appears in <code>/var/log/mail.log</code>, have a look in <code>/var/log/mail.err</code> for any messages.</p>
<h2 id="wrappingup">Wrapping up</h2>
<p>Presuming that <code>cron-apt</code> and <code>postfix</code> are working correctly, we should receive an email before too long notifying us that there are packages to upgrade.</p>
<p>Upgrading these is simply a matter of running the following on the server:</p>
<pre><code>apt-get dist-upgrade
</code></pre>
</div>]]></content:encoded></item><item><title><![CDATA[NPM Shrinkwrap: a Great Way to Hack Dependencies]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1428220647/canada_cropped_uylf6l.jpg) -->
<p>One of the greatest things about using <a href="https://nodejs.org/">Node.js</a> is the huge number of modules that you can pull down and use in your project — more than 120,000 <a href="http://www.modulecounts.com/">according to modulecounts.com</a>.</p>
<p>This often makes it trivial to create a Node application — for example, you can create a web</p></div>]]></description><link>https://blog.mebooks.co.nz/npm-shrinkwrap-a-great-way-to-hack-dependencies/</link><guid isPermaLink="false">59d0622bc7843e0001a0ffdf</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Fri, 20 Mar 2015 03:34:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/canada_cropped_uylf6l.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1428220647/canada_cropped_uylf6l.jpg) -->
<img src="https://blog.mebooks.co.nz/content/images/2017/10/canada_cropped_uylf6l.jpg" alt="NPM Shrinkwrap: a Great Way to Hack Dependencies"><p>One of the greatest things about using <a href="https://nodejs.org/">Node.js</a> is the huge number of modules that you can pull down and use in your project — more than 120,000 <a href="http://www.modulecounts.com/">according to modulecounts.com</a>.</p>
<p>This often makes it trivial to create a Node application — for example, you can create a web server to start serving a site in less than a minute.</p>
<h2 id="theproblem">The problem</h2>
<p>However, when you start using other people's Node modules throughout your project, you might find yourself running into the problem of using a module which has a dependency that is out of date, and the module maintainer hasn't gotten around to releasing a new version of their package.</p>
<p>For instance, a project I'm working on uses <a href="https://www.npmjs.com/package/gulp-sass">gulp-sass</a> to compile our SASS into CSS. Gulp-sass is essentially a wrapper around <a href="https://github.com/sass/node-sass">node-sass</a>, which in turn is a wrapper around <a href="https://github.com/sass/libsass">Libsass</a>, a blazingly fast SASS compiler written in C/C++.</p>
<p>LibSass has come on well over the last year or so, and has pretty good coverage of the various SASS features, and is now able to compile most of the SASS directives to CSS.</p>
<p>However, earlier versions of Libsass had less coverage of the SASS directives, meaning that if you were trying something even a little bit esoteric with your SASS directives, you could find yourself gazing at a screen full of errors.</p>
<p>And, it turns out that the most recent version of <a href="https://www.npmjs.com/package/gulp-sass">gulp-sass</a> (1.3.3 as of writing this post) uses a version of <a href="https://github.com/sass/node-sass">node-sass</a> (^2.0.1) which in turn uses an old version of Libsass which unfortunately breaks some of our SASS.</p>
<p>For instance, we make use of <a href="http://viget.com/extend/sass-maps-are-awesome">SASS maps</a>, which appear in SASS 3.3 and allow us to create and use data-structures which in some way resemble JavaScript objects:</p>
<pre><code>$icon-type: (
    email: #00824a,
    facebook: #3b5998,
    twitter: #55acee,
    googleplus: #dd4b39,
    linkedin: #007bb6,
    whatsapp: #64d448,
    reddit: #ff5700,
);

@each $icon-type, $bgcolor in $icon-type {
    a.icon--#{$icon-type},
    a.icon--before--#{$icon-type}:before {
        @include icon-svg('icons/icon-#{$icon-type}.svg');
    }
    background-color: $bgcolor;
    }
}
</code></pre>
<h2 id="aoneoffsolution">A one-off solution</h2>
<p>If we use a more recent version of <code>node-sass</code>, which in turn uses a more recent version of <code>libsass</code>, the above will compile fine.</p>
<p>We could hack the <code>gulp-sass</code> <code>package.json</code>, changing:</p>
<pre><code>&quot;node-sass&quot;: &quot;^0.9&quot;,
</code></pre>
<p>to</p>
<pre><code>&quot;node-sass&quot;: &quot;^3.0&quot;,
</code></pre>
<p>and then, in the gulp-sass directory, re-install the package:</p>
<pre><code>npm install
</code></pre>
<p>This would be fine for our individual use, but the next time that one of our team members checks out the project and runs <code>npm install</code> in the root directory, they'd end up with the old 0.9 version of <code>node-sass</code>.</p>
<h2 id="therealsolution">The real solution</h2>
<p><a href="https://docs.npmjs.com/cli/shrinkwrap">NPM shrinkwrap</a> offers a nice solution to this problem.<br>
It allows us to override that version of a particular dependency of a particular sub-module.</p>
<p>Essentially, when you run <code>npm install</code>, npm will first look in your root directory to see whether a <code>npm-shrinkwrap.json</code> file exists. If it does, it will use this first to determine package dependencies, and then falling back to the normal process of working through the <code>package.json</code> files.</p>
<p>To create an <code>npm-shrinkwrap.json</code>, all you need to do is</p>
<pre><code> npm shrinkwrap --dev
</code></pre>
<p>The <code>--dev</code> option is needed if the dependencies you're trying to override are in the <code>devDependencies</code> section of the <code>package.json</code>.</p>
<p>So, in our case, where we want to force <code>gulp-sass</code> to use a more-recent version of node-sass, we'd do the following:</p>
<ol>
<li>
<p>Hack <code>gulp-sass/package.json</code> to change <code>&quot;node-sass&quot;: &quot;^0.9&quot;</code> to <code>&quot;node-sass&quot;: &quot;^3.0&quot;</code></p>
</li>
<li>
<p>Run <code>npm shrinkwrap --dev</code> in our root directory to generate the <code>npm-shrinkwrap.json</code></p>
</li>
<li>
<p>Since the only package dependency that we're worried about is <code>node-sass</code> for the <code>gulp-sass</code> package, we can remove everything from the main <code>&quot;dependencies&quot;</code> block except for the <code>&quot;gulp-sass&quot;</code> block</p>
</li>
<li>
<p>Commit our changes back to our repo, so that the next team member can checkout the repo and simply run <code>npm install</code></p>
</li>
</ol>
<p>There's much more detail to be found about <code>shrinkwrap</code>, both on <a href="https://docs.npmjs.com/cli/shrinkwrap">npm</a> and on the <a href="http://blog.nodejs.org/2012/02/27/managing-node-js-dependencies-with-shrinkwrap/">Node.js blog</a></p>
</div>]]></content:encoded></item><item><title><![CDATA[ES6 Modules with System JS]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1426191769/stars_bhysw4.jpg) -->
<h1 id="usinges6modulestoday">Using ES6 modules today</h1>
<p>With the arrival of the latest version of JavaScript, <a href="http://en.wikipedia.org/wiki/ECMAScript#ECMAScript_Harmony_.286th_Edition.29">ECMAScript version 6</a>, we have access to some very useful features, such as the ability to properly modularise our JavaScript.</p>
<p>Being able to use JavaScript modules is important, as it means that we can build applications (both</p></div>]]></description><link>https://blog.mebooks.co.nz/es6-modules-with-system-js/</link><guid isPermaLink="false">59d0628fc7843e0001a0ffe0</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Thu, 12 Mar 2015 03:35:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/stars_bhysw4.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1426191769/stars_bhysw4.jpg) -->
<h1 id="usinges6modulestoday">Using ES6 modules today</h1>
<img src="https://blog.mebooks.co.nz/content/images/2017/10/stars_bhysw4.jpg" alt="ES6 Modules with System JS"><p>With the arrival of the latest version of JavaScript, <a href="http://en.wikipedia.org/wiki/ECMAScript#ECMAScript_Harmony_.286th_Edition.29">ECMAScript version 6</a>, we have access to some very useful features, such as the ability to properly modularise our JavaScript.</p>
<p>Being able to use JavaScript modules is important, as it means that we can build applications (both for the browser and that run in the backend, e.g. using Node) that have a number of benefits, including:</p>
<ul>
<li>being more self-contained, and less likely to cause side-effects in other code</li>
<li>less reliance on exposing and consuming global variables</li>
<li>being easier to test, as we can use dependency injection to mock our dependencies</li>
<li>better architecture of our applications</li>
<li>better access to code created by other parties</li>
</ul>
<h2 id="existingsolutions">Existing solutions</h2>
<p>Because JavaScript has not had native support for modules, a number of solutions have sprung up over the years to provide this, including:</p>
<ul>
<li>the use of patterns such as the <a href="http://toddmotto.com/mastering-the-module-pattern/">self-revealing module pattern</a>, known in the wild as an <a href="http://benalman.com/news/2010/11/immediately-invoked-function-expression/">IFFE</a></li>
<li><a href="http://en.wikipedia.org/wiki/Asynchronous_module_definition">AMD modules</a>, as exemplified by <a href="http://requirejs.org/docs/whyamd.html">Require.js</a></li>
<li><a href="http://spinejs.com/docs/commonjs">CommonJS modules</a>, as exemplified by <a href="https://nodejs.org/docs/latest/api/modules.html">Node</a></li>
</ul>
<p>All of these solutions are in wide use, and there's plenty of code out there that has been written to use one or more of the above solutions; particularly, there's a wealth of CommonJS modules available on <a href="https://www.npmjs.com/">NPM</a> (<a href="http://www.modulecounts.com/">more than 120,000</a> at the time this post was written).</p>
<h2 id="theproblem">The Problem</h2>
<p>Ideally, we'd start using and creating ES6 Modules today; however it's going to be some time before all browsers implement the specification, and for those of us who have to support older browser versions such as IE8, we'd really like a solution that we can use now.</p>
<p>Also, although we may create new code using the ES6 modules format, we may also want to take advantage of the other modules formats, particularly CommonJS, so we can build off the efforts of others.</p>
<p>So, in short we want a solution that will let us use all of the major existing module solutions, along with ES6 modules, in the browsers used by our audience today.</p>
<h2 id="thesolution">The Solution</h2>
<p><a href="https://github.com/systemjs/systemjs">SystemJS</a> is a project that meets our needs, in that it acts as a polyfill for the ES6 module specification, but also lets us make use of AMD and CommonJS modules as well as our IIFEs.</p>
<p>SystemJS bills itself as a universal dynamic module loader, and is designed as a wrapper around the <a href="https://github.com/ModuleLoader/es6-module-loader">es6-module-loader</a>, a project which is also maintained by the SystemJS creator, <a href="https://github.com/guybedford">Guy Bedford</a>.</p>
<p>The good news is that we can make use of SystemJS and the es6-module-loader to provide wide support for all major JavaScript module formats, so that we can start developing applications now that allow us to work flexibly with existing code and third party modules.</p>
<h2 id="usage">Usage</h2>
<p>The following is one example of how you'd make use of SystemJS, but there are plenty of others ways that it can be used; in our case we only want ES6 modules, but if you want to make use of other ES6 language features now, you'll want to use <a href="https://github.com/google/traceur-compiler">Traceur</a> or <a href="https://babeljs.io/">Babel</a> (both of which SystemJS integrates with nicely). More possibilities are outlined in <a href="http://guybedford.com/practical-workflows-for-es6-modules">an article written by Guy Bedford, the SystemJS creator</a>, as well as <a href="https://github.com/systemjs/systemjs/wiki/Production-Workflows">on the SystemJS site itself</a>.</p>
<h3 id="buildingourbundle">Building our Bundle</h3>
<p>For our example app, we want to create a bundle of modules — that is, a single file that can be downloaded by a browser, but which contains all of our custom modules that we expect to use in our application.</p>
<p>To achieve this, we make use of the <a href="https://github.com/systemjs/builder">SystemJS builder</a> as follows (note that the following presumes that you're familiar with using build tools such as <a href="http://gruntjs.com/">Grunt</a> or <a href="http://gulpjs.com/">Gulp</a>):</p>
<pre><code class="language-javascript">Builder = require('systemjs-builder');

gulp.task('modules', function(cb) {

    doModules('manifest', './dist/bundle.js')
    .then(function(){
        console.log('Build of modules complete');
        cb();
    })
    .catch(function(err){
        console.error(err);
    });

});

function doModules(manifest, target) {
    return new Builder({
        baseURL: 'assets/js/'
    })
    .build(manifest, target);
}
</code></pre>
<p>The above shows a gulp task that uses a manifest specifying the modules to include in the bundle, like follows:</p>
<pre><code class="language-javascript">require('module1');
require('module2');
</code></pre>
<p>Note that each of the files in the manifest is expected to be found on disk with the <code>.js</code> extension.</p>
<h3 id="scriptloading">Script Loading</h3>
<p>In our example app, we want to be able to use <a href="http://demosthenes.info/blog/702/matchMedia-Media-Queries-For-JavaScript">window.matchMedia</a> so we can do media queries in JavaScript, however in <a href="http://caniuse.com/#feat=matchmedia">IE8 and IE9</a> this means making use of the <a href="https://github.com/weblinc/media-match">media.match polyfill</a>.</p>
<p>Once <code>matchMedia</code> support is available, we'll load the <a href="http://wicky.nillia.ms/enquire.js/">enquire.js</a> wrapper for <code>matchMedia</code>.</p>
<p>And, once enquire.js is available, we can then load our app.</p>
<p>To do all of the above, we'll use SystemJS as a script loader, pulling in the required modules in the correct order. For example, our <code>app.js</code> might look like the following:</p>
<pre><code class="language-javascript">;
'use strict';

(function () {

    System.config({
        baseURL: 'assets/js/dist/'
    });

    // We need to let System know it can find module1
    // in bundle.js file.
    System.bundles['bundle'] = ['module1'];

    // Test whether we need the media.match polyfill
    // for older browsers before loading enquire.
    if (!window.matchMedia) {
        // Set up our chained dependencies: 
        // module1 &lt;= enquire.min &lt;= media.match.min
        System['import']('media.match.min')
        .then(function(){
            return System['import']('enquire.min');
        }).then(function(m){
            // We need to force enquire 
            // onto the window object for IE8
            window.enquire = m;

            return System['import']('module1');
        });
    } else {
        // Set up our chained dependencies: 
        // module1 &lt;= enquire.min
        System['import']('enquire.min')
        .then(function(){
            return System['import']('module1');
        });
    }

})();
</code></pre>
<p>Some important points to note about the above:</p>
<ul>
<li>Given we're supporting IE8 which uses ES3, we have to use <code>System['import']</code> rather than <code>System.import</code>, as <code>import</code> is a reserved word in ES3</li>
<li>SystemJS uses promises, so the resolved promise will return our loaded module, and in the <code>.then</code> we can load the next module in the chain</li>
<li>Both the media.match polyfill and enquire.js are globals (aka <a href="http://benalman.com/news/2010/11/immediately-invoked-function-expression/">IFFEs</a>), which SystemJS can load</li>
<li>As the media.match polyfill and enquire.js are not specified as belonging to a bundle, SystemJS expects to find them in the location specified by <code>baseURL</code></li>
<li>For IE8 we need to explicitly force the global onto the window object (hence <code>window.enquire = m;</code>), however this is not needed for other browsers</li>
</ul>
<h3 id="importingmodules">Importing Modules</h3>
<p>Now that our app has been loaded, we can include modules as necessary. For instance, in <code>module1</code>:</p>
<pre><code class="language-javascript">;
'use strict';

import module2 from 'module2';

(function () {
    module2.init();
});
</code></pre>
<p>In <code>module2.js</code>:</p>
<pre><code class="language-javascript">;
var msgText = 'module2 loaded';

var module2  = {

    init: function() {
        function msg() {
            return msgText;
        }

        msg();
    }
};

export default module2;
</code></pre>
<p>The ES6 modules specification provides <a href="http://www.sitepoint.com/understanding-es6-modules/">plenty of ways to expose functionality from a given module</a>, though in the above case we take the approach of simply exposing the main object, meaning that our consuming module has access to all methods and variables defined on the object, though in the above example it won't have access to <code>msgText</code>.</p>
<p>Note that the only code we need code in our HTML is the <code>app.js</code>; all other modules are loaded via <code>app.js</code>.</p>
<h2 id="goingfurther">Going Further</h2>
<p>If you want to explore an app that demonstrates the above, check out this <a href="http://codepen.io/jcdarwin/pen/jEpWwg">codepen</a>.</p>
<h2 id="notes">Notes</h2>
<p>As some of this technology is still relatively new, there are probably going to be a few wrinkles. In our experience, once we worked things out SystemJS worked well, but we did run across a strange bug in IE8, where at least some of the modules wouldn't load, but then did as soon as the developer tools were opened (and kept loading when the developer tools were closed again).</p>
<p>Also, we got tripped up for a little while with IE8 by <a href="https://github.com/ModuleLoader/es6-module-loader/issues/321">a bug that's now been fixed</a> (but not released as of writing this post).</p>
</div>]]></content:encoded></item><item><title><![CDATA[Testing on Devices Using a Local Webserver]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/c_scale,e_brightness:-50,w_1250/v1411984909/cave-mountain-nature-1717_low_fmmowc.jpg) -->
<p>Although it's improved in recent times, one of the painful aspects of web development can be testing that your site works well on mobile devices.</p>
<p>This particularly applies when you're developing on your local laptop with  wireless network connection, and want to view the site on a mobile device, without</p></div>]]></description><link>https://blog.mebooks.co.nz/testing-on-devices-using-a-local-webserver/</link><guid isPermaLink="false">59d06310c7843e0001a0ffe1</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Mon, 29 Sep 2014 03:37:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1491173385378-ab59d3805adf?ixlib=rb-0.3.5&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;s=10829ed1a092c26afaf6fc7dbea2b937" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/c_scale,e_brightness:-50,w_1250/v1411984909/cave-mountain-nature-1717_low_fmmowc.jpg) -->
<img src="https://images.unsplash.com/photo-1491173385378-ab59d3805adf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&s=10829ed1a092c26afaf6fc7dbea2b937" alt="Testing on Devices Using a Local Webserver"><p>Although it's improved in recent times, one of the painful aspects of web development can be testing that your site works well on mobile devices.</p>
<p>This particularly applies when you're developing on your local laptop with  wireless network connection, and want to view the site on a mobile device, without first having to deploy to a staging server.</p>
<h2 id="ngrok">Ngrok</h2>
<p>One tool that takes much of this pain away is <a href="https://ngrok.com/">ngrok</a>, which is described on the site thusly:</p>
<blockquote>
<p>“I want to securely expose a local web server to the internet and capture all traffic for detailed inspection and replay.”</p>
</blockquote>
<blockquote>
<p>ngrok creates a tunnel from the public internet (<a href="http://subdomain.ngrok.com">http://subdomain.ngrok.com</a>) to a port on your local machine. You can give this URL to anyone to allow them to try out a web site you're developing without doing any deployment.</p>
</blockquote>
<p>What this means is that, when we're webserving a site such as <code>http://mebooks.localhost</code> on our laptop, within a couple of minutes we can be visiting a URL such as <a href="http://mebooks.ngrok.com">http://mebooks.ngrok.com</a> in a mobile browser and seeing the same site; as long as the mobile browser has a connection to the internet, it should be able to load our page.</p>
<h3 id="installation">Installation</h3>
<pre><code>wget https://api.equinox.io/1/Applications/ap_pJSFC5wQYkAyI0FIVwKYs9h1hW/Updates/Asset/ngrok.zip?os=darwin&amp;arch=amd64&amp;channel=stable

cd ~/workspace &amp;&amp; mkdir ngrok &amp;&amp; mv ngrok.zip ngrok &amp;&amp; cd ngrok
</code></pre>
<p>Once installed, usage is as simple as:</p>
<pre><code># Serve all port 80 traffic via ngrok
./ngrok 80
</code></pre>
<p>Once connected, we'll see something like the following:</p>
<pre><code>ngrok                        (Ctrl+C to quit)

Tunnel Status                 online
Version                       1.7/1.6
Forwarding                    http://2ebd2fe0.ngrok.com -&gt; 127.0.0.1:80
Forwarding                    https://2ebd2fe0.ngrok.com -&gt; 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        41
Avg Conn Time                 260.80ms


HTTP Requests
-------------

GET /assets/images/141207361_ 200 OK
GET /assets/images/144566138_ 200 OK
GET /assets/images/142808332_ 200 OK
GET /assets/images/83691787_t 200 OK
</code></pre>
<p>Ngrok has opened up a tunnel from our local machine to the ngrok site, and we can then visit <a href="http://2ebd2fe0.ngrok.com">http://2ebd2fe0.ngrok.com</a> in our mobile device browser to see our locally-served site.</p>
<p>You'll probably need to add the following directive to your vhost conf (presuming you're using apache):</p>
<pre><code>    # This goes, say, below 'ServerName mebooks.localhost'
    ServerAlias 2ebd2fe0.ngrok.com
</code></pre>
<h3 id="configuration">Configuration</h3>
<p>The subdomain assigned by <a href="http://ngrok.com">ngrok</a> is not guaranteed to last from one session to the next, so ideally we want to use a custom subdomain such as <code>mebooks.ngrok.com</code>.</p>
<p>We can easily specify a custom subdomain when starting ngrok:</p>
<pre><code>    ./ngrok -subdomain mebooks 80
</code></pre>
<p>However, while we don't have to pay to be able to use a custom domain, unless we've paid a donation to ngrok these subdomains are on a first-come basis; if someone else has paid to reserve our subdomain or is currently using ngrok with the specified subdomain, then we won't be able to use it.</p>
<p>Once logged in, we'll find a auth token on our <a href="https://ngrok.com/dashboard">dashboard</a> that allows us to identify ourselves. This auth token only needs to be specified once, as it then gets saved in <code>~/.ngrok</code>.</p>
<p>Typically, on our local webserver we'll be serving our site using a name-based virtual host, expecting to see our site at <a href="http://mebooks.localhost">http://mebooks.localhost</a>, and having the following in our <code>/etc/private/hosts</code> file:</p>
<pre><code>127.0.0.1    mebooks.localhost
</code></pre>
<p>To ensure that our webserver can correctly capture requests from <a href="http://mebooks.ngrok.com">http://mebooks.ngrok.com</a>, we need to ensure that our vhost has the <code>ServerAlias mebooks.ngrok.com</code> set, and our webserver has been restarted.</p>
<p>Note that we can also see a nice page showing our traffic served at <a href="http://localhost:4040/http/in">http://localhost:4040/http/in</a></p>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1411987386/ngrok_e8sr9q.jpg" alt="Testing on Devices Using a Local Webserver"></p>
<p>More details about ngrok can be found on the <a href="https://ngrok.com/">ngrok site</a>, and in <a href="http://www.sitepoint.com/accessing-localhost-from-anywhere/">this sitepoint article</a> which covers ngrok and similar services.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Using Local Storage to Improve Page Load]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1411465171/abstract-apple-blur-831_emsxkl.jpg) -->
<p>Many devices these days support local storage, meaning that we can store assets in local storage on the first visit by the user to a given URL, and then on subsequent visits choose to retrieve assets from local storage rather than downloading them again.</p>
<p>Steve Souders has done a <a href="http://www.stevesouders.com/blog/2011/09/26/app-cache-localstorage-survey/">quick</a></p></div>]]></description><link>https://blog.mebooks.co.nz/using-local-storage-to-improve-page-load/</link><guid isPermaLink="false">59d06381c7843e0001a0ffe2</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Tue, 23 Sep 2014 04:39:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/abstract-apple-blur-831_emsxkl.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1411465171/abstract-apple-blur-831_emsxkl.jpg) -->
<img src="https://blog.mebooks.co.nz/content/images/2017/10/abstract-apple-blur-831_emsxkl.jpg" alt="Using Local Storage to Improve Page Load"><p>Many devices these days support local storage, meaning that we can store assets in local storage on the first visit by the user to a given URL, and then on subsequent visits choose to retrieve assets from local storage rather than downloading them again.</p>
<p>Steve Souders has done a <a href="http://www.stevesouders.com/blog/2011/09/26/app-cache-localstorage-survey/">quick survey</a> on the use of local storage and, combined with a cookie, this technique of retrieving assets from local storage can be useful in reducing the size and number of assets that are retrieved remotely. Currently, it's being <a href="http://www.stevesouders.com/blog/2011/03/28/storager-case-study-bing-google/">used by Google and Bing</a> amongst others.</p>
<p>An interesting statement in Steve's analysis is the following:</p>
<blockquote>
<p>Another surprise from last week’s survey was that the mobile version of Google Search had 68 images in the results HTML document as data: URIs, compared to only 10 for desktop and iPad. Mobile browsers open fewer TCP connections and these connections are typically slower compared to desktop, so reducing the number of HTTP requests is important.</p>
</blockquote>
<p>Another way of putting this is that, when it comes to serving pages for mobile devices, Google are prepared to take a hit in terms of the total amount of data served (as a data URI for a given resource is around 30% larger than the original resource) in return for a lower total number of TCP connections. Therefore, it seems we can take this as evidence that reducing the number of TCP connections more than makes up for increasing our overall data payload by 30%.</p>
<p>As an aside, when considering TCP connections, you should think about initially serving a large resource for a given domain followed by smaller resources, rather than the opposite.  This reason for this was outlined in Hossein Lotfi's <a href="https://www.youtube.com/watch?v=9PdA6soLX-Y&amp;list=PL055Epbe6d5YDU6sikjqcd_YM9XT4OehD&amp;index=2">Velocity 2014 presentation</a>, where he pointed out that serving a large resource initially will expand the slow start TCP window quicker than will serving a smaller resource.</p>
<p>As for the reasons for why local storage is of more interest on mobile devices than on desktop, Steve Souders provides the following reasons in <a href="https://blog.mebooks.co.nz/using-local-storage-to-improve-page-load/">another post</a>:</p>
<blockquote>
<ul>
<li><em>Mobile latencies are higher and connection speeds are lower, so clientside caching is more important on mobile.</em></li>
</ul>
</blockquote>
<ul>
<li><em>Mobile disk cache sizes are smaller than desktop sizes, so a better alternative is needed for mobile.</em></li>
<li><em>There are still desktop browsers with significant market share that are missing many HTML5 capabilities, whereas mobile browsers have more support for HTML5.</em></li>
</ul>
<h2 id="whatsizeisthebrowsercachecomparedtolocalstorage">What size is the browser cache compared to local storage?</h2>
<p>Browser cache can be relatively small on mobile devices, and local storage can help ensure that certain resources are available longer on the mobile device than.</p>
<p>Browser cache sizes are really <a href="http://www.webperformancetoday.com/2012/07/12/early-findings-mobile-browser-cache-persistence-and-behaviour/">all over the map</a>. On devices that run Android 2.2, we only have browser cache sizes of around 5MB, while on recent iOS versions, we have browser cache sizes of more than 50MB.</p>
<p>This means that, on many of the lower-end devices (and on higher-end devices where the user is actively visiting many sites), content may be quickly ejected from the browser cache.</p>
<p>Contrast this with local storage, where we typically have 5MB available <em>per domain</em> fully under our control, and you'll see that we've got a good chance of ensuring that site resources persist longer in local storage than they do in browser cache.</p>
<p>However, given the chance that a resource may still be in browser cache, it only makes sense to prefer local storage over browser cache if it is at least as fast.</p>
<h2 id="whichisfasterbrowsercacheorlocalstorage">Which is faster, browser cache or local storage?</h2>
<p><a href="http://www.mobify.com/blog/smartphone-localstorage-outperforms-browser-cache/">Peter McLachlan at Mobify</a> did some tests comparing local storage and browser cache across a range of mobile operating systems, and found that local storage is typically faster, sometimes more than twice as much.</p>
<p>It does appear  that there are differences between mobile and desktop in terms of the relative speeds of local storage and browser cache and, apparently, on desktop browser cache can be faster than local storage.</p>
<p>Mat Scales did <a href="http://jsperf.com/localstorage-versus-browser-cache">some useful tests</a> to prove this is the case (and you can <a href="http://jsperf.com/localstorage-versus-browser-cache/7">repeat these on your own device</a>), showing that local storage is often typically slower in loading than the browser cache on desktop (though Safari is the notable exception, being <a href="http://www.webdirections.org/blog/localstorage-perhaps-not-so-harmful/">blazingly fast and reading and writing from local storage</a>).</p>
<h2 id="thelesson">The lesson</h2>
<p>So, the lesson here seems to be that using local storage for caching is useful on mobile, but not necessarily so on desktop.</p>
<p>It also appears that local storage retrieval is markedly slower on multi-process browsers, as the OS is retrieving from disk rather than memory, and multi-process browsers currently do this <a href="http://www.stevesouders.com/blog/2011/03/28/storager-case-study-bing-google/#comment-3158">via a blocking call to the main process</a>).</p>
<p>In this case it appears wise to ensure that each retrieval returns as much information as possible; ideally, I'd guess we'd store all of the assets for a given domain in one local storage chunk. Do note that you're typically limited to 5MB of local storage per domain, and that you'll have to handle any problems that may arise if you go over this limit.</p>
<h2 id="otherfactors">Other factors</h2>
<p>From the above discussion, the use of local storage for caching appears attractive; however, note that a problem arises if the cookie exists, but local storage has been cleared or compromised. In this case we need to detect the problem, and this is <a href="http://www.stevesouders.com/blog/2011/03/28/storager-case-study-bing-google/#comment-3476">the approach that Google use</a>, clearing the cookie and reloading the entire page in the process, and repopulating local storage.</p>
<p>Note also that the browser cache has other advantages over local storage than simply speed. When we serve a page, we can specify the <code>Last-Modified</code> and <code>Expires</code> cache headers, thus allowing the browser to know when to invalidate the resources in the browser cache.</p>
<p>If we are instead using local storage, we have to either build this functionality into our scripts that control the loading of resources from the local storage, or alternatively restrict ourselves to only storing long-lived resources in local storage.</p>
<p><a href="http://www.mobify.com/blog/smartphone-localstorage-outperforms-browser-cache/">Peter McLachlan's post</a> also mentioned a few other considerations:</p>
<blockquote>
<ul>
<li><em>LocalStorage space is relatively small, 5MB on most browsers, and it can only store string data. This is effectively halved because localStorage stores strings as double-byte characters (UTF-16).</em></li>
</ul>
</blockquote>
<ul>
<li><em>LocalStorage reads and writes do block the rendering thread. Where possible reads/writes should be done after initial page rendering is complete. You may want to avoid operations that are likely to cause performance problems such as a very large numbers of operations (thousands or tens of thousands of reads or writes), as well as read/write operations larger than 1MB.</em></li>
<li><em>Browsers should do a better job of exposing storage space used by websites taking advantage of the web storage APIs to smartphone users. Be a good neighbour and don't use localStorage recklessly, use it for key resources that are in your critical path.</em></li>
</ul>
<h2 id="basketjs">Basket.js</h2>
<p>Addy Osmani (with contributions from Sindre Sorhus, Andrée Hansson and Mat Scales) has created <a href="http://addyosmani.github.io/basket.js/">basket.js</a>, which he is calling &quot;a simple (proof-of-concept) script loader that caches scripts with localStorage&quot;.</p>
<p>The great thing about this project is that it looks like it gives us all the abilities that we need to manage the loading, caching, retrieval and cache invalidation of scripts into local storage.</p>
<p>Note that if you want to use <a href="http://addyosmani.github.io/basket.js/">basket.js</a> to store non-script resources in local storage (such as CSS), you'll need to wrap them in JavaScript, which is not too difficult, though do be aware of the problems of <code>@import</code> statements in your CSS.</p>
<p>As <a href="https://github.com/andrewwakeling/basket-css-example#import-not-handled">mentioned by Andrew Wakeling</a>:</p>
<blockquote>
<p>CSS has the ability to load other resources stylesheets via @import and these calls aren't currently handled by basket.js. (A solution would probably require parsing of the CSS.)</p>
</blockquote>
<blockquote>
<p>Since the CSS is directly inserted into the DOM as a style element, relative paths are no longer relative to their source path. This is demonstrated by the difference seen in basket.html vs normal.html.</p>
</blockquote>
<blockquote>
<p>This problem is not exclusive to @import but can occur with any other resource which includes a path (e.g. url). A workaround to this problem is to always use absolute paths.</p>
</blockquote>
<p>Persuing <a href="https://github.com/addyosmani/basket.js/issues">the issues for the basket.js project</a>, it looks like they've given thought to making basket.js more fully support CSS other resources, as well as supporting other storage solutions besides local storage, though it appears progress is slow.</p>
<h2 id="goingfurther">Going further</h2>
<p>We can incorporate the idea of using local storage for caching resources on mobile into a wider strategy to make our pages load faster on mobile devices.</p>
<p>For instance, the Filament group are <a href="http://filamentgroup.com/lab/performance-rwd.html">using an approach</a> whereby they are shortening the critical path for loading pages.</p>
<p>This involves inlining the CSS needed for critical path rendering and asynchronously loading other CSS and scripts where possible to prevent blocking.</p>
<p>Normally, inlining CSS means that the benefits of caching are lost, as the browser doesn't know to cache the inlined CSS. The Filament group's approach here is to use a cookie:</p>
<blockquote>
<p>On the first time a browser visits this site we set a cookie after asynchronously requesting certain files (such as our site’s full CSS file) to specify that now that the files have been requested, and are likely to be cached by the browser. Then, on subsequent visits to our site, our server-side code checks if that cookie is present and if so, it avoids including any inline CSS and instead just references the full CSS externally with an ordinary link element. This seems to make the page load a little more cleanly on return visits. We do the same for our fonts and icons CSS files as well.</p>
</blockquote>
<p>They've taken this concept further, creating a library called <a href="https://github.com/filamentgroup/enhance">enhance.js</a> which can be used to apply these enhancements progressively, depdending on whether the <a href="http://responsivenews.co.uk/post/18948466399/cutting-the-mustard">browser cuts the mustard</a>.</p>
<p>We can adapt the Filament Group's approach such that, instead of just retrieving resources asynchronously from remote URLs, we instead add a local storage caching layer using something like <a href="http://addyosmani.github.io/basket.js/">basket.js</a>.</p>
<p>With the combination of the two approaches, we end up with a resource loading strategy whereby:</p>
<ul>
<li>CSS resources on the critical path are inlined, therefore not incurring an extra HTTP request</li>
<li>scripts that we need before render start (such as <code>HTML5shiv.js</code>) are loaded synchronously</li>
<li>we asynchronously load those scripts and CSS resources not on the critical path, storing them in local storage. Note that this includes the full CSS, i.e. including the CSS that was inlined in the page</li>
<li>we set a cookie indicating that these resources are now cached in local storage</li>
<li>on the first request these asynchronous resources are loaded from remote URLs</li>
<li>on subsequent requests these asynchronous resources are loaded from local storage</li>
<li>ideally, we'd avoid serving the inlined CSS more than once, by using server side includes (<a href="https://github.com/filamentgroup/enhance#a-fully-configured-head-setup-for-enhancejs">as the filament group do, with their enhance.js project</a>)</li>
</ul>
<p>Using the above technique (sans the local storage caching), the Filament Group managed to acheive start render times of around 300ms, as opposed to around 900ms without deferring resources to be loaded asynchronously.</p>
<p>Putting together inlined critical path CSS and asynchronous / lazy loading of non critical assets with the use of local storage for caching seems like a good potential solution for fast rendering on mobile devices on the initial page load, and constantly fast subsequent page reloads.</p>
<p>I'd be interested in hearing from anyone who's given this a go in practice.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Supporting Internet Explorer 8 in Foundation 5]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1411039927/iemoon_small_jiwbyd.jpg) -->
<p><a href="http://foundation.zurb.com/">Foundation 5</a> is a great CSS framework, providing lots of useful functionality that makes building modern websites easier.</p>
<p>However, <a href="http://foundation.zurb.com/docs/compatibility.html">Foundation 5 doesn't provide support for IE8</a>, although we can make up for this with a few polyfills and some well-placed conditional comments.</p>
<h2 id="internetexplorer8liveson">Internet Explorer 8 lives on</h2>
<p>Although the days</p></div>]]></description><link>https://blog.mebooks.co.nz/supporting-internet-explorer-8-in-foundation-5/</link><guid isPermaLink="false">59d063d5c7843e0001a0ffe3</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Thu, 18 Sep 2014 04:41:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/iemoon_small_jiwbyd.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/v1411039927/iemoon_small_jiwbyd.jpg) -->
<img src="https://blog.mebooks.co.nz/content/images/2017/10/iemoon_small_jiwbyd.jpg" alt="Supporting Internet Explorer 8 in Foundation 5"><p><a href="http://foundation.zurb.com/">Foundation 5</a> is a great CSS framework, providing lots of useful functionality that makes building modern websites easier.</p>
<p>However, <a href="http://foundation.zurb.com/docs/compatibility.html">Foundation 5 doesn't provide support for IE8</a>, although we can make up for this with a few polyfills and some well-placed conditional comments.</p>
<h2 id="internetexplorer8liveson">Internet Explorer 8 lives on</h2>
<p>Although the days of IE6 are pretty much past, and IE7 can be similarly disregarded as the audience share for a given site is often below 1%, IE8 is different.</p>
<p>IE10 can be regarded as Microsoft's first <a href="http://eisenbergeffect.bluespire.com/evergreen-browsers/">evergreen browser</a>. Technically, their <a href="http://blogs.windows.com/ie/2011/12/15/ie-to-start-automatic-upgrades-across-windows-xp-windows-vista-and-windows-7/">adoption of a self-updating browser</a> means that users on Windows 7 using IE9 got upgraded to IE10, however <a href="http://stackoverflow.com/a/19060334">users on Vista using IE9 and users on XP using IE8 didn't</a>.</p>
<p>Thus, users on Windows XP who are determined to use Internet Explorer are left with IE8 as the only option until they either adopt a non-Microsoft browser, install a new operating system, or — more likely — move to a new computer.</p>
<p>Even if Windows XP is now unsupported, its share as of writing this remains at <a href="http://thenextweb.com/microsoft/2014/08/01/windows-xp-falls-25-market-share-windows-8-1-loses-share-first-time/">around 25% of the browser market</a>, meaning that there will be a significant number of users visiting your site who will be using IE8 for sometime to come.</p>
<p>Therefore, we need to make sure that any site we make will be usable by these XP users running IE8, and this means that we need to do a little bit of work to support:</p>
<ul>
<li>Media queries</li>
<li>HTML5 elements</li>
<li>CSS3 decorations, such as rounded corners (<code>border-radius</code>)</li>
</ul>
<p>Media queries aren't actually that important, as IE8 is only available on desktop systems, and most users will use their browser viewport at its maximum size. However, we do want to try and allow a consistent experience where possible across browsers and operating systems.</p>
<p>The solution we use is detailed below.</p>
<h2 id="polyfills">Polyfills</h2>
<p>Thankfully, through the hard work of others, we can fulfil our requirements of Foundation 5 support for IE8 through the use of a number of JavaScript polyfills.</p>
<ul>
<li>the use of <a href="http://javascript.nwbox.com/NWMatcher/">NWMatcher</a> to provide CSS3 selectors (for IE6 and IE7)</li>
<li>the use of <a href="http://selectivizr.com/">selectivizr</a> to provide CSS3 pseudo-classes and attribute selectors (for IE6 and IE7)</li>
<li>the use of <a href="https://github.com/aFarkas/html5shiv">html5shiv</a> to provide HTML5 DOM elements (for IE6–IE8)</li>
<li>the use of <a href="https://github.com/scottjehl/Respond">respond.js</a> to provide min/max-width CSS3 Media Queries (for IE6–IE8)</li>
<li>The use of <a href="http://css3pie.com/">CSS3 PIE</a> to provide support for CSS3 decoarations (for IE6–IE9)</li>
</ul>
<p>We also need to ensure that anything less than IE9 uses the legacy jQuery 1.x.x, while anything from IE9 onwards can use jQuery 2.</p>
<p>If we wish to support REMs, then we can use the <a href="https://github.com/chuckcarpenter/REM-unit-polyfill">REM-unit-polyfill</a>, though be aware of the following <a href="http://foundation.zurb.com/forum/posts/241-foundation-5-and-ie8">note</a>:</p>
<blockquote>
<p>There is currently a bug in the REM polyfill that seems to stop it working on an element if a property the element includes the !important rule, in the following screenshots I've just removed <code>!important</code> from <code>.button</code> in foundation.css, until the bug can be fixed.</p>
</blockquote>
<h2 id="conditionalcomments">Conditional comments</h2>
<p>We wrap the polyfills in IE conditional comments, such that the specified version/s of IE will detect and load them, but other IE versions and other browsers will simply ignore them, e.g.</p>
<pre><code>&lt;!--[if IE 8]&gt;
    &lt;html class=&quot;lt-ie9&quot; xml:lang=&quot;en&quot;&gt;
&lt;![endif]--&gt;
</code></pre>
<p>In the case where we want to target a version of IE but also ensure that normal browsers pick up the polyfill, we use a little trick to reveal the contents of the conditional comments to these browsers:</p>
<pre><code>&lt;!--[if gt IE 8]&gt;
    &lt;!--&gt;
    &lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;
    &lt;!--
&lt;![endif]--&gt;
</code></pre>
<p>In the above code, the <code>&lt;!--&gt;</code> acts to close the HTML comment started on the previous line for normal browsers, while the <code>&lt;!--</code> reopens the comment. In this setting, these two lines effectively mean nothing to IE, but serve to close off the beginning and ending IE condtional comments to other browsers.</p>
<p>The full boilerplate for our solution is as follows:</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;!--[if IE 8]&gt;
    &lt;html class=&quot;lt-ie9&quot; xml:lang=&quot;en&quot;&gt;
&lt;![endif]--&gt;
&lt;!--[if gt IE 8]&gt;
    &lt;!--&gt;
    &lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;
    &lt;!--
&lt;![endif]--&gt;

    &lt;head&gt;
        &lt;meta content=&quot;IE=Edge&quot; http-equiv=&quot;x-ua-compatible&quot; /&gt;
        &lt;meta charset=&quot;utf-8&quot; /&gt;
        &lt;meta content=&quot;width=device-width, initial-scale=1.0&quot; name=&quot;viewport&quot; /&gt;
        &lt;meta content=&quot;en&quot; http-equiv=&quot;content-language&quot; /&gt;

        &lt;title&gt;Our web site&lt;/title&gt;

        &lt;!--[if lt IE 8]&gt;
            &lt;script src=&quot;//s3.amazonaws.com/nwapi/nwmatcher/nwmatcher-1.2.5-min.js&quot;&gt;&lt;/script&gt;
            &lt;script src=&quot;//html5base.googlecode.com/svn-history/r38/trunk/js/selectivizr-1.0.3b.js&quot;&gt;&lt;/script&gt;
        &lt;![endif]--&gt;

        &lt;!-- REM-unit-polyfill is not available on a CDN, so serve locally --&gt;
        &lt;!--[if lt IE 9]&gt;
            &lt;link href=&quot;css/ie.css&quot; rel=&quot;stylesheet&quot; /&gt;
            &lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.6.2/html5shiv.js&quot;&gt;&lt;/script&gt;
            &lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/respond.js/1.1.0/respond.min.js&quot;&gt;&lt;/script&gt;
            &lt;script src=&quot;js/rem.js&quot;&gt;&lt;/script&gt;
        &lt;![endif]--&gt;
    &lt;/head&gt;

    &lt;body&gt;
        &lt;!--[if lt IE 9]&gt;
            &lt;script src=&quot;//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js&quot;&gt;&lt;/script&gt;
            &lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/css3pie/2.0beta1/PIE_IE678.js&quot;&gt;&lt;/script&gt;
        &lt;![endif]--&gt;
        &lt;!--[if gte IE 9]&gt;
            &lt;!--&gt;
            &lt;script src=&quot;//code.jquery.com/jquery-latest.min.js&quot;&gt;&lt;/script&gt;
            &lt;script src=&quot;javascript/foundation.min.js&quot;&gt;&lt;/script&gt;
            &lt;script src=&quot;javascript/foundation.topbar.js&quot;&gt;&lt;/script&gt;
            &lt;script src=&quot;javascript/foundation.clearing.js&quot;&gt;&lt;/script&gt;
            &lt;!--
        &lt;![endif]--&gt;

        &lt;!--[if eq IE 9]&gt;
            &lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/css3pie/2.0beta1/PIE_IE9.js&quot;&gt;&lt;/script&gt;
        &lt;![endif]--&gt;

        &lt;script src=&quot;javascript/app.js&quot;&gt;&lt;/script&gt;
    &lt;/body&gt;

&lt;/html&gt;
</code></pre>
<h2 id="beforeandafter">Before and after</h2>
<p>Before these improvements are made, viewing Foundation 5 in IE8 looks like this:</p>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1411039124/FoundationIE8Before_pbp2wh.png" alt="Supporting Internet Explorer 8 in Foundation 5"></p>
<p>Once we've made the above improvements, we get a much more pleasing experience:</p>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1411039128/FoundationIE8After_m0bpwp.png" alt="Supporting Internet Explorer 8 in Foundation 5"></p>
<p>The inspiration for the above was provided by <a href="http://foundation.zurb.com/forum/posts/241-foundation-5-and-ie8">Jame's Cocker's post</a> on the Foundation Forum.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Capturing Errors with Sentry]]></title><description><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/e_brightness:-50/v1409383942/map_qq5ocr.jpg) -->
<p>When deploying applications into production, the challenge then becomes keeping on top of any errors that may be thrown during day-to-day operation.</p>
<p>In the bad old days, you might put in some error trapping to log to a disk file (and then remember to check this file regularly) or send</p></div>]]></description><link>https://blog.mebooks.co.nz/capturing-errors-with-sentry/</link><guid isPermaLink="false">59d0642fc7843e0001a0ffe4</guid><dc:creator><![CDATA[Jason Darwin]]></dc:creator><pubDate>Sat, 30 Aug 2014 04:42:00 GMT</pubDate><media:content url="https://blog.mebooks.co.nz/content/images/2017/10/map_qq5ocr.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><!-- ![thumb image-post](https://res.cloudinary.com/mebooks/image/upload/e_brightness:-50/v1409383942/map_qq5ocr.jpg) -->
<img src="https://blog.mebooks.co.nz/content/images/2017/10/map_qq5ocr.jpg" alt="Capturing Errors with Sentry"><p>When deploying applications into production, the challenge then becomes keeping on top of any errors that may be thrown during day-to-day operation.</p>
<p>In the bad old days, you might put in some error trapping to log to a disk file (and then remember to check this file regularly) or send yourself an email.</p>
<p>Thankfully, we've largely outgrown this approach, and these days there's a number of services which provide online error tracking, such as <a href="https://raygun.io/">Raygun</a>; Paul Irish has a <a href="https://plus.google.com/+PaulIrish/posts/12BVL5exFJn">list of similar services</a>.</p>
<p>If you want to install an error tracking service on your own hardware, then <a href="http://getsentry.com/">Sentry</a> is a pretty good choice, being a Python application that is available in both SAAS and <a href="https://github.com/getsentry/sentry">OpenSource</a> versions.</p>
<p>Sentry is the product of David Cramer (an engineer at Dropbox) and Chris Jennings (a designer at Github) and was started while they were at Disqus. In <a href="http://blog.leanstack.io/founder-stories-how-sentry-built-their-open-source-service/">an interview on LeanStack</a> they mention that they consider themselves fortunate enough to have good day jobs such that Sentry can thrive as a OpenSource project:</p>
<blockquote>
<p>Yeah, so we open-sourced it because we wanted people to use it. Not because we wanted to create this thing and make money off of it. And it's still the same thing today. A lot of people don't pay for Sentry. I would say 90% of people just host it themselves...But those 90% of people who aren't paying us are directly making Sentry better for the people who are paying us so it works.</p>
</blockquote>
<p><img src="http://res.cloudinary.com/mebooks/image/upload/v1409385036/sentry-dashboard_r0y7ku.jpg" alt="Capturing Errors with Sentry"><br>
<em>The Sentry dashboard, showing a number of different errors</em></p>
<p>Sentry provides an HTTP API to allow your application to capture and send it information about errors, typically achieved using the officially sanctioned <a href="http://raven.readthedocs.org/en/latest/">Raven libraries</a>, and Raven provides error collectors for languages such as <a href="https://github.com/getsentry/raven-php">PHP</a>, <a href="https://github.com/getsentry/raven-python">Python</a>, <a href="https://github.com/getsentry/raven-ruby">Ruby</a>, <a href="https://github.com/getsentry/raven-node">Node</a>, and <a href="https://github.com/getsentry/raven-js">client-side JavaScript</a> (a full list can be found in the <a href="http://sentry.readthedocs.org/en/latest/client/">Sentry docs</a>).</p>
<h2 id="installation">Installation</h2>
<p>Although I initially tried to install Sentry using <code>pip</code> and <code>easy_install</code>, I ran into some problems with reported errors not making it onto the dashboard, so ended up installing from source.</p>
<p>The following notes were developed through deploying Sentry on an Ubuntu VPS, but should be broadly similar for any Linux-like environment.</p>
<p>As I'm a fan of <a href="https://github.com/ansible/ansible">Ansible</a>, I include some of the steps below as Ansible scripts; if you're not conversant with Ansible, don't worry as you should be able to figure out the equivalent shell commands from the scripts without too much problem.</p>
<p>Our main control file looks like:</p>
<pre><code>---
# roles/sentry/main.yml
# This Playbook deploys sentry!

# http://cavaencoreparlerdebits.fr/blog/2014/04/install-sentry-to-catch-and-manage-your-errors-softwares
# http://blog.jesse-obrien.ca/post/add-beautiful-and-effective-exception-handling-to-laravel-with-sentry

- hosts: droplets
  user: root
  vars_files:
    - vars/main.yml
  handlers:
   - include: handlers/main.yml

  tasks:
  - include: tasks/sentry_packages.yml
  - include: tasks/sentry_user.yml
  - include: tasks/sentry_env.yml
  - include: tasks/sentry_mysql.yml
  - include: tasks/sentry_install.yml
  - include: tasks/sentry_nginx.yml
</code></pre>
<p>We also have some variables that are referenced by the scripts:</p>
<pre><code># roles/sentry/vars/main.yml
sentry:
  user: sentry
  password: whatever
  hostname: whatever
  listen_addr: 127.0.0.1
  listen_port: 9000
  db: sentry
  db_username: sentry
  db_password: whatever
  url: http://whatever
  env: sentry-env
  email_host: whatever
  email_host_user: whoever@wherever
  email_host_password: whatever
  email_port: whichever
  email_use_tls: True
</code></pre>
<h3 id="installtherequisitepackages">Install the requisite packages</h3>
<pre><code># roles/sentry/tasks/sentry_packages.yml
# ===============================================================
# packages: install the necessary packages
# ===============================================================

# Add the officially-sanctioned nodejs ppa
- name: add-apt-repository ppa:chris-lea/node.js
  action: template 
    src=files/chris-lea-node_js-wheezy.list
    dest=/etc/apt/sources.list.d/chris-lea-node_js-wheezy.list
    owner=root group=root mode=0644
  tags: packages

- name: update apt cache
  apt: update_cache=yes cache_valid_time=3600
  sudo: yes
  tags: packages

- name: install packages
  apt: pkg={{ item }} state=latest
  sudo: yes
  with_items:
    - nginx
    - python-software-properties
    - python-pip
    - python-dev
    - build-essential
  tags: packages

- name: force install packages (because we're using a ppa)
  apt: pkg={{ item }} state=latest force=yes
  sudo: yes
  with_items:
    - nodejs
  tags: packages

- name: install python packages using pip
  pip: name={{ item }}
  with_items:
    - virtualenv
    - virtualenvwrapper
    - python-memcached
  tags: packages
</code></pre>
<p>The above Ansible script can be run using the <code>--tags</code> option:</p>
<pre><code># Install the packages
ansible-playbook -l droplets roles/sentry/main.yml -i hosts --tags=packages
</code></pre>
<p>Here we install a number of python-based packages, as well as Node from the officially-sanctioned PPA. Note that we had to change <code>wheezy</code> to <code>lucid</code> in the PPA definition as it appears the PPA is not yet supporting <code>wheezy</code>:</p>
<pre><code># roles/sentry/files/chris-lea-node_js-wheezy.list
deb http://ppa.launchpad.net/chris-lea/node.js/ubuntu lucid main
deb-src http://ppa.launchpad.net/chris-lea/node.js/ubuntu lucid main
</code></pre>
<h3 id="createouruser">Create our user</h3>
<p>We need to create a user to run Sentry under:</p>
<pre><code># roles/sentry/tasks/sentry_user.yml
# ===============================================================
# user: create the sentry user
# ===============================================================

- name: Create sentry user
  user: name={{ sentry.user }}
        password={{ sentry.password }}
        createhome=yes
        shell=/bin/bash
        state=present
  tags: user

- name: Create the SSH directory
  file: state=directory path=/home/{{ sentry.user }}/.ssh

- name: Add authorized key for the user
  authorized_key: user={{ sentry.user }} key='{{ item }}'
  with_file:
    - ~/.ssh/id_rsa.pub

- name: Backup sudoers file
  command: cp -f /etc/sudoers /etc/sudoers.bak
  tags: user

- name: Add sentry user to sudoers
  action: lineinfile
        dest=/etc/sudoers
        regexp='{{ sentry.user }} ALL'
        line='{{ sentry.user }} ALL=(ALL) ALL'
        state=present
  tags: user

- name: Check sudoers file syntax
  shell: visudo -q -c -f /etc/sudoers
  register: result
  ignore_errors: True
  tags: user

- name: Rolling back - restoring backed-up sudoers file
  action: cp -f /etc/sudoers.bak /etc/sudoers
  when: result|failed
  tags: user
</code></pre>
<p>The above Ansible script can be run using the <code>--tags</code> option:</p>
<pre><code># Create the sentry user and home directory
ansible-playbook -l droplets roles/sentry/main.yml -i hosts --tags=user
</code></pre>
<h3 id="createourvirtualenvironment">Create our virtual environment</h3>
<p>We want to use a python virtual environment to isolate our Sentry python dependencies, and ensure that we can run different versions of python for other applications.</p>
<p>We also install our Sentry conf — normally this is installed using <code>sentry init</code> once Sentry has been installed, however we want to template our Sentry conf for use with Ansible and customise a few of the directives, such as using MySQL rather than the default SQLite. More details about the Sentry configuration can be found on the <a href="http://sentry.readthedocs.org/en/latest/quickstart/index.html">Sentry site</a>.</p>
<p>The important part of the <code>sentry.conf</code> is the <code>SENTRY_URL_PREFIX</code>, which should be set to your domain:</p>
<pre><code>SENTRY_URL_PREFIX = 'example.com'  # No trailing slash!
</code></pre>
<hr>
<pre><code># ===============================================================
# env: initialise the sentry environment
# ===============================================================

- name: include virtualenvwrapper in our shell
  lineinfile: &gt;
    destfile=/home/{{ sentry.user }}/.bashrc
    regexp=&quot;^source /usr/local/bin/virtualenvwrapper\.sh&quot;
    line=&quot;source $HOME/.virtualenvs/{{ sentry.env}}/bin/activate&quot;
    state=present
  remote_user: &quot;{{ sentry.user }}&quot;
  tags: env

- name: create our virtual environment
  shell: &gt;
    executable=/bin/bash
    source `which virtualenvwrapper.sh` &amp;&amp; mkvirtualenv {{ sentry.env }}
  register: create_virtualenv
  remote_user: &quot;{{ sentry.user }}&quot;
  environment:
    HOME: /home/{{ sentry.user }}
  tags: env

- debug: var=create_virtualenv.stdout_lines
  tags: env

- name: install the sentry conf
  template: &gt;
    src=templates/sentry.conf.py.j2
    dest=~/sentry.conf.py
    owner=&quot;{{ sentry.user }}&quot; group=&quot;{{ sentry.user }}&quot; mode=0644
  remote_user: &quot;{{ sentry.user }}&quot;
  environment:
    HOME: /home/{{ sentry.user }}
  tags: env

- name: install python packages inside the virtual env using pip
  pip: name={{ item }} virtualenv={{ sentry.env }}
  with_items:
    - MySQL-python
  tags: env
</code></pre>
<p>The above Ansible script can be run using the <code>--tags</code> option:</p>
<pre><code># Create our virtual environment
ansible-playbook -l droplets roles/sentry/main.yml -i hosts --tags=env
</code></pre>
<p>Importantly, the above script will create our virtual environment (in <code>/home/sentry/.virtualenvs/sentry-env</code>) and will add the following line to our <code>.bashrc</code>, which ensures that the virtual environment is loaded when we run Sentry under our sentry user:</p>
<pre><code>source $HOME/.virtualenvs/{{ sentry.env}}/bin/activate
</code></pre>
<h3 id="installingsentry">Installing Sentry</h3>
<p>Now that we've done the hard work of setting up the environment, the actual install of Sentry is quite easy:</p>
<pre><code># Clone the sentry project
cd /home/sentry
git clone git://github.com/getsentry/sentry.git sentry

# Make sentry.
# This will be created in /home/sentry/.virtualenvs/sentry-env
cd /home/sentry/sentry/
make

# Create our db and user
ansible-playbook -l droplets roles/sentry/main.yml -i hosts --tags=mysql

# Run the database migrations
sentry --config=/home/sentry/sentry.conf.py upgrade
</code></pre>
<p>Here we're using the following Ansible script to setup our MySQL environment as we'd like it, prior to running Sentry's database migrations:</p>
<pre><code># roles/sentry/tasks/sentry_mysql.yml
#     ===============================================================
# mysql: install sentry database
# ===============================================================

- name: install mysql dependencies
  action: apt pkg={{item}} state=installed
  with_items:
    - mysql-server 
    - mysql-client 
    - libmysqlclient-dev
    - python-mysqldb
  tags: mysql


# 'localhost' needs to be the last item for idempotency, see
# http://ansible.cc/docs/modules.html#mysql-user
- name: determine whether .my.cnf exists
  shell: ls -la ~/.my.cnf
  ignore_errors: True
  register: ls_my_cnf
  tags: mysql

- name: copy .my.cnf file with empty root password credentials
  template: src=.my.cnf.orig.j2 dest=/root/.my.cnf owner=root mode=0600
  when: ls_my_cnf.stdout.find(&quot;cannot access&quot;) == 1
  tags: mysql

- name: update mysql root password for all root accounts
  mysql_user: name={{ mysql.user }} host={{ item }} password={{ mysql.password }} priv=*.*:ALL,GRANT
  with_items:
    - 127.0.0.1
    - localhost
  tags: mysql

- name: update mysql sentry password for all sentry accounts
  mysql_user: name={{ sentry.db_username }} host={{ item }} password={{ sentry.db_password }} priv={{ sentry.db }}.*:ALL,GRANT
  with_items:
    - localhost
  tags: mysql

- name: copy .my.cnf file with root password credentials
  template: src=.my.cnf.j2 dest=/root/.my.cnf owner=root mode=0600
  tags: mysql

- name: ensure database sentry is present
  mysql_db: name={{ sentry.db }}
        collation=utf8_unicode_ci
        encoding=utf8
        state=present
  tags: mysql

- name: ensure database user for sentry is present and has necessary privileges
  mysql_user: name={{ sentry.db_username }}
        host={{ item }}
        password={{ sentry.db_password }}
        priv={{ sentry.db }}.*:ALL,GRANT
        state=present
  with_items:
    - localhost
  tags: mysql
</code></pre>
<p>We then need to create the administrator account:</p>
<pre><code>sentry --config=/home/sentry/sentry.conf.py createsuperuser
</code></pre>
<p>And following this, run the repair</p>
<pre><code>sentry --config=/homee/sentry/sentry.conf.py repair --owner=&lt;superusername&gt;
</code></pre>
<h3 id="nginx">Nginx</h3>
<p>We want to reverse-proxy Sentry to the outside world using Nginx:</p>
<pre><code>server {
    listen 80;
   server_name {{ sentry.hostname }};

    access_log /var/log/nginx/{{ sentry.hostname }}.access.log;
    error_log /var/log/nginx/{{ sentry.hostname }}.error.log;

    # keepalive + raven.js is a disaster
    keepalive_timeout 0;

    location / {
        # proxy_pass http://127.0.0.1:9000;
        proxy_pass http://{{ sentry.listen_addr }}:{{ sentry.listen_port }};
        proxy_redirect off;

        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;

        error_page 500 502 503 504 /500.html;
    }
}
</code></pre>
<p>The above Ansible script can be run using the <code>--tags</code> option:</p>
<pre><code># Create our virtual environment
ansible-playbook -l droplets roles/sentry/main.yml -i hosts --tags=env

nginx reload
</code></pre>
<h3 id="supervisor">Supervisor</h3>
<p>We'll use Supervisor to look after our Sentry process, and this means creating the following as <code>/etc/supervisor/conf.d/sentry.conf</code>:</p>
<pre><code>[program:sentry-web]
directory=/home/{{ sentry.user }}/.virtualenvs/{{ sentry.env }}/
command=/home/{{ sentry.user }}/.virtualenvs/{{ sentry.env }}/bin/sentry --config=/home/{{ sentry.user }}/sentry.conf.py start
autostart=true
autorestart=true
redirect_stderr=true

[program:sentry-worker]
directory=/home/{{ sentry.user }}/.virtualenvs/{{ sentry.env }}/
command=/home/{{ sentry.user }}/.virtualenvs/{{ sentry.env }}/bin/sentry celery worker -B
autostart=true
autorestart=true
redirect_stderr=true
</code></pre>
<p>We need to tell Supervisor about the new configuration file:</p>
<pre><code>sudo supervisorctl restart sentry-web
</code></pre>
<p>Now that Sentry's installed, you should be able to visit the Dashboard and login using the superuser credentials you set earlier.</p>
<p>You'll want to obtain the DSN, which can be found at: <code>http://&lt;your domain&gt;/sentry-internal/sentry/keys/</code></p>
<h3 id="capturingerrorstosentry">Capturing errors to Sentry</h3>
<p>Having Sentry installed is nice, but it's a little bit redundant until we start sending it errors.</p>
<p>The <a href="https://github.com/getsentry/raven-php">Raven PHP client</a> has some good notes about creating a simple application to post to Sentry.</p>
<p>We'll create a simple PHP script to generate a few errors, and capture them to our Sentry installation using Raven.</p>
<pre><code>&lt;?php 
// sentry-test.php

// Use Composer's autoloader
require('vendor/autoload.php');
Raven_Autoloader::register();
 
$client = new Raven_Client(
    // Importantly, we define below the DSN.
    // This is made up of a &lt;public key&gt; and a &lt;secret&gt;, which can be found at
    // http://&lt;your domain&gt;/sentry-internal/sentry/keys/
    // while &lt;your domain&gt; will be the value defined for SENTRY_URL_PREFIX in sentry.conf.
    'http://&lt;public key&gt;:&lt;secret&gt;@&lt;your domain&gt;/1'
    , array(
        'tags' =&gt; array(
            'php_version' =&gt; phpversion()
    ),
));
 
echo &quot;Capture a debug message\n&quot;;
$client-&gt;captureMessage('Test debug message %s', array('foo'), array(
    'level' =&gt; Raven_Client::DEBUG,
    'extra' =&gt; array('foo' =&gt; 'bar')
));

echo &quot;Capture an info message\n&quot;;
$client-&gt;captureMessage('Test info message %s', array('foo'), array(
    'level' =&gt; Raven_Client::INFO,
    'extra' =&gt; array('foo' =&gt; 'bar')
));

echo &quot;Capture a warning message and obtain the Sentry reference\n&quot;;
$event_id = $client-&gt;getIdent($client-&gt;captureMessage('Test warning message %s', array('foo'), array(
    'level' =&gt; Raven_Client::WARNING,
    'extra' =&gt; array('foo' =&gt; 'bar')
)));
echo &quot;Your reference ID is &quot; . $event_id . &quot;\n&quot;;
 
try {
    echo &quot;Capture an exception and obtain the Sentry reference\n&quot;;
    throw new Exception('Uh oh!');
}
catch (Exception $e) {
    $event_id = $client-&gt;getIdent($client-&gt;captureException($e));
    echo &quot;Your reference ID is &quot; . $event_id . &quot;\n&quot;;
}
 
// optionally install a default error handler to catch all exceptions
$error_handler = new Raven_ErrorHandler($client);
 
// Register error handler callbacks
set_error_handler(array($error_handler, 'handleError'));
set_exception_handler(array($error_handler, 'handleException'));
</code></pre>
<p>We'll also need to install the Raven PHP library, and as we also want to make use of Composer's autoloader, we'll use Composer:</p>
<pre><code>{
    &quot;name&quot;: &quot;you/sentry-test&quot;,
    &quot;description&quot;: &quot;Test an newly-installed sentry installation&quot;,
    &quot;require&quot;: {
        &quot;raven/raven&quot;: &quot;dev-master&quot;
    },
    &quot;license&quot;: &quot;whatever&quot;,
    &quot;authors&quot;: [
        {
            &quot;name&quot;: &quot;your name&quot;,
            &quot;email&quot;: &quot;your email&quot;
        }
    ],
    &quot;minimum-stability&quot;: &quot;dev&quot;
}
</code></pre>
<p>To install the composer dependencies:</p>
<pre><code># Install composer if necessary
curl -sS https://getcomposer.org/installer | php

# Install the dependencies for this project
composer install
</code></pre>
<p>Running the following should generate some messages and errors, which should appear in quick fashion on your Sentry dashboard:</p>
<pre><code>php sentry-test.php</code></pre>
</div>]]></content:encoded></item></channel></rss>