Blog

Amazon CloudFront with Drupal 8

Written by Metal Toad Staff | May 14, 2015 12:00:00 AM

This article is one of Metal Toad's Top 20 Drupal Tips. Enjoy!

Since I wrote my first review of CloudFront in 2012, AWS 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;
  }
 
}