AWS

Amazon CloudFront with Drupal 8

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


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

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.