fog

CDN

version
Gem Version
install
gem install fog
source
fog/fog

Faster websites are better. Better experience, better sales, you name it. Unfortunately, making a website faster can be tough. Thankfully a content distribution network, or CDN, can give you great performance bang for your buck. A CDN helps speed things up by putting copies of your files closer to your users. It’s like the difference between pizza delivery from across the street and pizza delivery from the next town over.

The ease and deliciousness are the good news, but until recently CDN’s were only available in the big leagues via ‘my business guys will talk to your business guys’ deals. Fortunately for us, Amazon recently updated CloudFront, their CDN service, to allow us to get these benefits with just a credit card and an API call. So now we’ll see how you can spend a few minutes to save your users countless hours of load time.

Preliminaries

First, make sure you have fog installed:

gem install fog

Now you’ll need to sign up for Cloudfront. Gather up the credentials your new credentials to initialize a connection to the service:

require 'fog'

# create a connection to the service
cdn = Fog::CDN.new({
  :provider               => 'AWS',
  :aws_access_key_id      => YOUR_AWS_ACCESS_KEY_ID,
  :aws_secret_access_key  => YOUR_AWS_SECRET_ACCESS_KEY
})

Setting Up Your CDN

Now you’ll need to create a ‘distribution’ which represents a mapping from the CDN to your domain. For the examples we’ll pretend we are working on ‘http://www.example.com’, but you can just switch it to your actual domain. Some other options are available, but the only other one we need to fill in is OriginProtocolPolicy. This sets what to do about http vs https. We will use ‘match-viewer’ which returns the same protocol as the request, but you can also choose ‘http-only’ which always returns http responses.

data = cdn.post_distribution({
  'CustomOrigin' => {
    'DNSName'               => 'www.example.com',
    'OriginProtocolPolicy'  => 'match-viewer'
  }
})

# parse the response for stuff you'll need later
distribution_id   = data.body['Id']
caller_reference  = data.body['DistributionConfig']['CallerReference']
etag              = data.headers['ETag']
cdn_domain_name   = data.body['DomainName']

# wait for the updates to propogate
Fog.wait_for {
  cdn.get_distribution(distribution_id).body['Status'] == 'Deployed'
}

Fog also supports models for the AWS CDN. The above code can also be written like this:

distribution = cdn.distributions.create( :custom_origin => {
      'DNSName'               => 'www.example.com',
      'OriginProtocolPolicy'  => 'match-viewer'
    }, :enabled => true
)

distribution.wait_for { ready? }

Like other collections supported by Fog, it is also possible to browse the list of distributions:

cdn.distributions.all

Or get access to a distinct distribution by its identity:

cdn.distributions.get(distribution_id)

Getting Served

With the domain name from the distribution in hand you should now be ready to serve content from the edge. All you need to do is start replacing urls like http://www.example.com/stylesheets/foo.css with #{cdn_domain_name}/stylesheets/foo.css. Just because you can do something doesn’t always mean you should though. Dynamic pages are not really well suited to CDN storage, since CDN content will be the same for every user. Fortunately some of your most used content is a great fit. By just switching over your images, javascripts and stylesheets you can have an impact for each and every one of your users.

Congrats, your site is faster! By default the urls aren’t very pretty, something like http://d1xdx2sah5udd0.cloudfront.net/stylesheets/foo.css. Thankfully you can use CNAME config options to utilize something like http://assets.example.com/stylesheets/foo.css, if you are interested in learning more about this let me know in the comments.

Invalidating the CDN caches

Sometimes, some part of the CDN cache needs to be invalidated because the origin changed and we need a faster propagation than waiting for the objects to expire by themselves. To do this, CloudFront supports creating distributions invalidation.

An invalidation can be created with the following code:

# let's invalidate /test.html and /path/to/file.html
data = cdn.post_invalidation(distribution_id, [ "/test.html", "/path/to/file.html" ])
invalidation_id = data.body['Id']

Fog.wait_for { cdn.get_invalidation(distribution_id, invalidation_id).body['Status'] == 'Completed' }

It is also possible to list past and current invalidation for a given distribution:

cdn.get_invalidation_list(distribution_id)

The same can be written with Fog CDN model abstraction:

distribution = cdn.distributions.get(distribution_id)

invalidation = distribution.invalidations.create(:paths => [ "/test.html", "/path/to/file.html" ])
invalidation.wait_for { ready? }

Listing invalidations is as simple as:

distribution.invalidations.all

# this returns only summarized invalidation
# to get access to the invalidations path:
distribution.invalidations.get(invalidation_id)

Cleaning Up

But, just in case you need to update things I’ll run through how you can make changes. In my case I just want to clean up after myself, so I’ll use the distribution_id and ETag from before to disable the distribution. We need to use the ETag as well because it provides a way to refer to different versions of the same distribution and ensures we are updating the version that we think we are.

data = cdn.put_distribution_config(
  distribution_id,
  etag,
  {
    'CustomOrigin'    => {
      'DNSName'               => 'www.example.com',
      'OriginProtocolPolicy'  => 'match-viewer'
    },
    'CallerReference' => caller_reference,
    'Enabled'         => 'false'
  }
)

# parse the updated etag
etag = data.headers['ETag']

Now you just need to wait for the update to happen like before and once its disabled we can delete it:

Fog.wait_for {
  cdn.get_distribution(distribution_id).body['Status'] == 'Deployed'
}
cdn.delete_distribution(distribution_id, etag)

This can also be written with CDN models as:

distribution = cdn.distributions.get(distribution_id)

# make sure the distribution is deployed otherwise it can't be disabled
distribution.wait_for { ready? }

distribution.disable

# Disabling a distribution is a lengthy operation
distribution.wait_for { ready? }

# and finally let's get rid of it
distribution.destroy

Thats it, now go forth and speed up some load times!

About