Amazon CloudFront with Drupal 8
Amazon CloudFront with Drupal 8
May 14th, 2015
Since I wrote my first review of CloudFront in 2012, Amazon has added support for three essential features:
- Dynamic content with POST, PUT and other HTTP methods
- Custom SSL certifications with Server Name Indication (SNI) This is important because with SNI, there's no need for a dedicated IPs (and the associated $600 per month fee)
- Wildcard cookies
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;
}
}