Blog

Using Amazon Cloudfront with Drupal

Written by Metal Toad Staff | Sep 12, 2012 12:00:00 AM

We like to use our own site to experiment with different technologies. CDN's are nothing new, and Metal Toad has projects running on competing systems including Akamai and Level 3. Still, I think Amazon Cloudfront is an interesting offering and I wanted to give it a spin. Here's my review of the service after setting it up with Drupal:

Pros:

  • Easy setup, low-cost, only pay for what you use
  • Support for CNAMES and domain sharding (e.g. static[1234].metaltoad.com)
  • Supports Accept:Byte-Range headers (important for media files)
  • Supports gzip compression (assuming your origin server does the compression)
  • Supports HTTPS
  • Honors cache-control headers
  • Custom HTTP headers are passed through to edge requests (including CORS)

Cons:

  • No global or directory "purge" command - each file must be invalidated individually
  • No custom SSL Certificates, so you can't use CNAMES with HTTPS
  • Fussy about SSL ciphers on the origin server (Cloudfront wasn't compatible with the ciphers configured on our load balancer, so I had to use "http-only" instead of "match-viewer")
  • Only minimal logging, no reports or graphs

Domain sharding

Multiple CNAMES add a little overhead with extra DNS lookups, but increase the number of parallel downloads (browsers impose a per-hostname limit). To minimize upstream bandwidth needed, these domains should be cookie-free. Since I chose subdomains of the site itself, I needed to adjust some cookie settings in Drupal:

  • In settings.php, set $cookie_domain = 'www.metaltoad.com';
  • In the Google Analytics module, set the tracking to "One domain with multiple subdomains"

Thanks to some new alter hooks in Drupal 7, all you need to implement a static file CDN is hook_file_url_alter(). There's an actively maintained CDN module, but in the spirit of inquiry I decided to implement the hook directly.

Module file

/**
 * Implements hook_file_url_alter().
 */
function mymodule_file_url_alter(&$uri) {
  // Route static files to Amazon CloudFront.
  if ($_SERVER['HTTP_HOST'] == 'www.metaltoad.com') {
    if ($GLOBALS['is_https']) {
      // Cloudfront doesn't support custom SSL certs, so we need to use Amazon's.
      $cdn = 'https://abcdef12345.cloudfront.net';
    }
    else {
      // 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/$path";
    }
    else if (!$scheme && strpos($uri, '//') !== 0) {
      $uri = "$cdn/$uri";
    }
  }
}
 
/**
 * Implements hook_boot().
 */
function mymodule_boot() {
  // Make sure Amazon CloudFront doesn't serve dynamic content.
  if (!empty($_SERVER['HTTP_X_AMZ_CF_ID']) && !strstr($_GET['q'], 'files/styles')) {
    header("HTTP/1.0 404 Not Found");
    print '404 Not Found';
    exit();
  }
}
 
/**
 * Implements hook_css_alter().
 */
function mymodule_css_alter(&$css) {
  // Mangle the paths slightly so that drupal_build_css_cache() will generate
  // different keys on HTTPS.  Necessary because CDN URL varies by protocol.
  if ($GLOBALS['is_https']) {
    foreach ($css as $key => $style) {
      if ($style['preprocess'] && $style['type'] == 'file') {
        $css[$key]['data'] = './' . $style['data'];
      }
    }
  }
}

.htaccess

# Set CORS header on static assets for CDN.
<FilesMatch "\.(ttf|otf|eot|woff|css|css\.gz|js|js\.gz)$">
  <IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin "*"
  </IfModule>
</FilesMatch>

The HTTPS and sharding support adds a little complexity, but overall the integration is straightforward. I'd recommend Cloudfront to anyone who wants an easy and cost-effective scalability win.