
Amazon CloudFront with Drupal 8
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:
- 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; } }
Comments
header() and exit() should be avoided..instead use Symfony Response objects
$response = new Response('404 Not Found', Response::HTTP_NOT_FOUND); $event->setResponse($response);
Tue, 04/19/2016 - 18:37
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?
Fri, 01/27/2017 - 22:41
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).
Wed, 02/15/2017 - 18:15
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.
Thu, 02/16/2017 - 09:11
Hi,
Do you have Drupal7 with AWS CloudFront tutorial?
Mon, 05/18/2015 - 16:07