Amazon CloudFront with Drupal 8

Since I wrote my first review of CloudFront in 2012, Amazon has added support for three essential features:

What this means is that CloudFront is no longer just for static content; it's fully capable of delivering content from a dynamic CMS like Drupal. Here are the configs, step-by-step:

Configure your distribution and origin

This is fairly straightforward. I reccomend using a CNAME for your origin (which could be a single instance, or an elastic load balancer). Ideally, your origin URL should not be accessible from the open internet for serveral reasons:

  • Prevent the origin URL from getting crawled by search engines
  • Pevent DDoS attacks from being able to bypass the CDN
  • Prevent spoofing of the X-Forwarded-For header

Configure a default behavior

Noteworthy settings are:

  • "use origin cache headers" - This means CloudFront will honor the page lifetime set on /admin/config/development/performance within Drupal.
  • Whitelist "Host" and "CloudFront-Forwarded-Proto". This allows virtual hosts, and any SSL redirect logic on the origin to function correctly.
  • Whitelist your site's session cookie.

Drupal 8 workarounds

One of the remaining Drupal 8 critical issues interferes with CloudFront:
[meta] External caches mix up response formats on URLs where content negotiation is in use
As a result, some additional behaviors are needed to work around this. These settings instruct CloudFront to forward all client headers for specific paths:

Domain-sharding

If you plan to use a single domain for your entire site, you're done! On this site, we decided to keep the domain-sharding approach described in my previous post, so we need a little D8 code.

mt_custom.info.yml

name: Metal Toad Custom
description: Stuff that doesn't fit anywhere else.
package: Custom
type: module
core: 8.x
dependencies:

mt_custom.services.yml

services:
  mt_custom_event_subscriber:
    class: Drupal\mt_custom\EventSubscriber\MTCustomSubscriber
    arguments: ['@current_user']
    tags:
      - {name: event_subscriber}

mt_custom.module

use Drupal\Component\Utility\UrlHelper;
 
/**
 * Implements hook_file_url_alter().
 */
function mt_custom_file_url_alter(&$uri) {
 
  // Route static files to Amazon CloudFront, for anonymous users only.
  if (\Drupal::request()->server->get('HTTP_HOST') == 'www.metaltoad.com' &&
    \Drupal::currentUser()->isAnonymous() &&
    !\Drupal::request()->isSecure()) {
 
    // Multiple hostnames to parallelize downloads.
    $shard = crc32($uri) % 4 + 1;
    $cdn = "http://static$shard.metaltoad.com";
 
    $scheme = file_uri_scheme($uri);
    if ($scheme == 'public') {
      $wrapper = file_stream_wrapper_get_instance_by_scheme('public');
      $path = $wrapper->getDirectoryPath() . '/' . file_uri_target($uri);
      $uri = "$cdn/" . UrlHelper::encodePath($path);
    }
    else if (!$scheme && strpos($uri, '//') !== 0) {
      $uri = "$cdn/" . UrlHelper::encodePath($uri);
    }
  }
}
 
/**
 * Implements hook_css_alter().
 */
function mt_custom_css_alter(&$css) {
  // Mangle the paths slightly so that Drupal\Core\Asset\AssetDumper will generate
  // different keys on HTTPS.  Necessary because CDN URL varies by protocol.
  if (\Drupal::request()->isSecure()) {
    foreach ($css as $key => $file) {
      if ($file['type'] === 'file') {
        $css[$key]['data'] = './' . $css[$key]['data'];
      }
    }
  }
}

src/EventSubscriber/MTCustomSubscriber.php

namespace Drupal\mt_custom\EventSubscriber;
 
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Session\AccountInterface;
 
class MTCustomSubscriber implements EventSubscriberInterface {
 
  protected $account;
 
  public function checkForCloudFront(GetResponseEvent $event) {
    $req = $event->getRequest();
 
    /*
     * Make sure Amazon CloudFront doesn't serve dynamic content
     * from static*.metaltoad.com
     */
    if (strstr($req->server->get('HTTP_HOST'), 'static')) {
      if (!strstr($req->getPathInfo(), 'files/styles')) {
        header("HTTP/1.0 404 Not Found");
        print '404 Not Found';
        exit();
      }
    }
  }
 
  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = array('checkForCloudFront');
    return $events;
  }
 
  public function __construct(AccountInterface $account) {
    $this->account = $account;
  }
 
}

header() and exit() should be avoided..instead use Symfony Response objects

$response = new Response('404 Not Found', Response::HTTP_NOT_FOUND);
$event->setResponse($response);

Hey,

Really nice tutorial.

Sorry to be a numpty, but please correct me if I'm wrong...

So, as I understand it, this allows Amazon's CloudFront to cache some of Drupal's HTML pages?

Yes, HTML is cached for anonymous users (as long as there is no session cookie). The max-age header will be respected, so the TTL will be whatever Drupal's "Page cache maximum age" is set to. (We typically set this quite low, in the 1 to 5 minute range). This allows for quick propagation of content changes, yet will still give a good hit rate under a heavy workload (e.g. if we someday make the front page of Reddit).

Hi,
Do you have a tutorial for Drupal7 and AWS Cloudfront? I have a HTTPS enabled e-commerce site hosted in Ireland and I am looking for using AWS CDN features.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <cpp>, <java>, <php>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and email addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.

About the Author

Dylan Tack, Director of Engineering

Dylan is a software engineer with more than a decade of experience working with a wide variety of clients including the Linux Foundation, PBS, Habitat for Humanity, TV.com and the Emmys. His background includes training as an electrical engineer, but he became passionate about open source through his work with a university genetics lab.

Dylan is a proud member of the Drupal community, a member of the Drupal security team, and has extensive experience with Perl and Java. His other interests include computer security, embedded design, climbing, and brewing.