Using Amazon’s CloudFront with Rails & Paperclip

Posted by – September 8, 2009

It took me a bit of experimentation, and I never found an example in a single place that showed how to set it up exactly how I wanted, so here is my code in my model for storing images used by the ArtCat calendar on Amazon S3. I am using Paperclip version 2.3.1.

First you will need to set up the distribution in Amazon for your given bucket, so that you have a URL to use for the :s3_host_alist value. I also set up a CNAME so that I can use a nice url like calcdn.artcat.com.

Note that I don't want to store any images other than my resized ones, so my :default_style is set to :original. Some of these values are actually constants in my config files, but I've replaced those here to make it more clear.

    has_attached_file :image,
      :storage => 's3',
      :s3_credentials => "#{RAILS_ROOT}/config/s3_credentials.yml",
      :bucket => 'artcal-production',
      :s3_host_alias => 'calcdn.artcat.com',
      :url => ':s3_alias_url',
      :path => "images/:class/:id_:timestamp.:style.:extension",
      :styles => { :thumb  => '60x60#', :medium => '270x200#', :original  => '600x600>' },
      :default_style => :original,
      :default_url => 'http://cdn1.artcat.com/pixel.gif',
      :s3_headers => { 'Expires' => 1.year.from_now.httpdate },
      :convert_options => { :all => '-strip -trim' }

Note that you do NOT have to set the ActionController::Base.asset_host to your CNAME for images. Paperclip just handles it as expected for these images.

You'll notice an interpolation in the :path that is not standard. Thanks to this Intridea Company Blog post I learned that I will need to change my image names when they are updated. CloudFront will not update my image due to that Expires header I set above for a whole year, which is not what we want to happen. I solved this by including the timestamp based on the updated_at value for the image. Based on that Intridea post, this is the code I added to config/initalizers/paperclib.rb.

Paperclip.interpolates(:timestamp) do |attachment, style|
  attachment.instance_read(:updated_at).to_i  
end

At first I was storing the images on the file system and serving them via Apache. Moving them to CloudFront improved my page load times by at least 50%, and means that I don't have to run as powerful as server to handle a lot of traffic on this image-heavy site as I might otherwise need.

14 Comments on Using Amazon’s CloudFront with Rails & Paperclip

Respond

  1. Thanks for this post, it was very helpful in setting up our app with Cloudfront! Exactly what I was looking for.

  2. Barry says:

    I'm glad to hear it helped!

  3. Marco B says:

    Thanks for the post.

    I know CloudFront has a 24hours delay to refresh its content if you update a file of yours into S3, so the "change name" does the trick (renaming a file is also the only way to be sure that browser don't use a locally cached version).

    Nice way to implement this into paperclip, thanks again :)

  4. Be careful with the updated_at interpolator if you're going to have any other attributes on the model. For instance. If you have :image and :name in the database. If you do Model#update_attribute(:name, "New name") it will update your Interpolation for the attachment. It will not however update the file name on the server so you're path and the actual file will be out of sync.

  5. Barry says:

    Hello, Cameron. It doesn't work that way. The interpolation is using attachment.instance_read(:updated_at).to_i so it only changes if the file changes, not if anything else on the model changes. The problem you describe only happens if you are relying on Model.updated_at for the interpolation.

  6. Mike Larkin says:

    Excellent write up! One thing I'd suggest would be to use multiple CNAMEs to serve assets -- most browsers limit simultaneous connections to a subdomain to a max of 2, so if you use something like cnd0, cdn1, cdn2, cdn3, you can speed things up a bit:

    :s3_host_alias => "cdn#{rand(4)}.pixellent.com"

  7. Barry says:

    Mike, I find just using the CDN is fast enough, and I want people's browsers to cache all of my assets. Your approach means they might get a different URL for an asset every time they visit one of my pages, so that I end up paying more for outgoing bandwidth.

  8. Barry,

    Thanks for this post. Exactly what I was looking for!

  9. Dave Rapin says:

    Great writeup, thanks a lot. Especially found the timestamp based on *file* modified date useful for cloudfront.

    I'm using this for the next version (still in development) of my open source photographer portfolio (http://grokphoto.org)

  10. Colin Adams says:

    Thanks for this. I was looking for S3 stuff, but the ":default_style => :original" came in handy!

  11. Michael Hellein says:

    I haven't tried using Paperclip with Cloudfront yet, but I was also considering something like Mike Larkin is suggesting to get around the requests-per-domain bottleneck. However, I don't think his approach will work, as the string interpolation should only happen once when the model is loaded. (Or am I mistaken?)

    Fortunately, :s3_host_alias also takes a Proc, so you could do:

    :s3_host_alias => Proc.new { "cdn#{rand(4)}.pixellent.com" }

    I also don't know if I would use rand, because you'd get a more even distribution by repeatedly iterating from 0-n (where n is the number of domains). On some percentage of requests, rand will give us all the same domain, and then we'd have no performance gain!

  12. Michael Hellein says:

    Also, if you use rand in the Proc above, you'll be telling browsers to load the same file from different servers on each request, which is obviously not helpful for caching!

  13. Pedro says:

    It seems that paperclip is already attaching the timestamp as a query parameter. If you specify "forward query parameters" when creating your distribution it'll work automatically.

    http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/QueryStringParameters.html

  14. Paulo says:

    It has been a long time since September 8, 2009, but I would like to point out that the option ":s3_headers", the way it was meant in this example, should also be a Proc. Otherwise, it would be stuck at boot time. So, use ":s3_headers => Proc.new { 'Expires' => 1.year.from_now.httpdate }"

Respond

Comments

Comments