Using a CDN for all Static Assets
This is the second part of a two part series on how we set up Codeship with Heroku. Read the first part that deals with Heroku and how to use it more efficiently with Unicorn.
In the first part we introduced our Unicorn setup that helps you manage several concurrent requests on one Heroku Dyno. The best optimization though is to remove all unnecessary requests completely. As the Heroku Cedar stack has no Proxy in front of your Rails application any more static assets have to be delivered through your application. This is very inefficient and keeps your dynos or unicorn workers busy with serving your assets.
This blogpost shows how we use the Asset Pipeline, Compass and Amazon Cloudfront to serve all of our assets fast without sending any requests to our application directly. We use all of it on codeship.com to make sure we make the best of all the resources the cloud provides. Although this guide is written with Rails in mind the concepts like Sprites or a CDN can be used with any framework and for any website.
At first add the necessary Gems to your Gemfile. It is assumed that you use the latest versions available of all gems.
https://gist.github.com/2694525.js?file=Gemfile Then you need to configure the asset pipeline in application.rb, development.rb and production.rb
https://gist.github.com/2694525.js?file=application.rb We set staticcachecontrol to "public, max-age=31536000" so the browser caches all static assets for up to a year. In production every asset has a hash added to its name, so whenever the file changes the browser requests the latest version as the hash and therefore the whole filename changes. Through this mechanism you do not have to invalidate any CDN cache at all, but you can by simply setting config.assets.version this will change the Hash as well and make the browser request every asset again.
https://gist.github.com/2694525.js?file=development.rb https://gist.github.com/2694525.js?file=production.rb All of your assets will be precompiled during deployment to Heroku.
To minimize the requests needed for loading our site we create a single application.css through the asset pipeline. The inherent problem with this setup is that css definitions for different subpages can potentially clash. To make sure this doesn't happen we give the body tag a special id composed of the controller and action name of the current site.
https://gist.github.com/2694525.js?file=application.html.erb Then we start every css file that only contains scss for a specific page with the id of that page and name the file controllername_actionname.scss:
https://gist.github.com/2694525.js?file=home_index.scss It's definitely not an ideal solution, but it works fine and we haven't figured out any better way to do this.
We use Spriting for all of the images in our application. Spriting is the process of combining several images into one bigger image to lower the number of requests to your website. We use the spriting feature of compass-rails heavily in our application. You should have already added compass-rails to your Gemfile before. Configure compass by adding a compass.rb file in config/compass.rb
https://gist.github.com/2694525.js?file=compass.rb Now put your images into app/assets/images or one of its subfolders. It is now very easy to create sprites for your website. Create an SCSS File like the following, which expects your buttons to be in app/assetsimages/buttons
https://gist.github.com/2694525.js?file=buttons.scss This will create a css file for you which references the specific buttons. For example consider you have btn_signup.png and btn_signup_hover.png in your buttons folder. You will then get a buttons-btn_signup css class that you can put onto any element and which sets the background accordingly. The following example creates a link that has btn-signup as its background and gets a hover state. The second css class button is there to set the height and width of the button.
https://gist.github.com/2694525.js?file=button_example.html.haml You have to make sure that you set the height and width of every button correctly, so the background fills the element exactly. Setting height and width is best practice in general as the Browser doesn't have to redraw the page when new images are loaded. You can read more about compass in their Reference Documentation.
To make sure all of your assets and pages are compressed before they are sent to your users add Rack:Deflater to your config.ru file.
https://gist.github.com/2694525.js?file=config.ru This will make sure that all your pages are compressed and all the assets that are stored in Cloudfront later on are stored there compressed as well.
Now that all assets are set up correctly we can go on to configure Amazon Cloudfront as our CDN. With Cloudfront you can set a Custom Origin pointing to your application. Thus upon the first time you hit a specific Cloudfront url it sends a request to your application and from then on caches the result. To get started go to the cloudfront tab in your aws console and click Create Distribution.
[caption id="attachment_49" align="alignnone" width="560"]
In the first step choose Download as this distribution is used for caching static assets. [caption id="attachment_50" align="alignnone" width="560"]
AWS Distribution Wizard[/caption]
In the next step enter your Domain name (or Heroku App Name) as the Origin Domain Name. Every requests that will go to your Cloudfront distribution will be made to this URL and the result will be cached. If you use https set theOrigin Protocol Policy to Match Viewer, so we can later set it to https and all transfers between your application and Cloudfront are encrypted.
[caption id="attachment_52" align="alignnone" width="562"]
AWS Distribution Origin[/caption]
The default values set for the caching behaviour are sufficient. Cloudfront will use the Cache headers we set earlier and store all assets for up to a year.
[caption id="attachment_51" align="alignnone" width="560"]
AWS Distribution Caching[/caption]
In the next window make sure the Distribution state is set to enabled.
[caption id="attachment_53" align="alignnone" width="560"]
AWS create distribution[/caption]
You are presented with a last overview of your new distribution and will be able to create it then. Now you can set the asset host for your application. In your production.rb set config.actioncontroller.assethost. We prefer to set it to ENV['ASSET_HOST'] as we can easily switch to another distribution then, which is very handy when using a staging server. Simply create another distribution for your staging environment and point there in your staging config.
In either case you have to point the asset_host to the distributions Domain Name which should in the end look something likehttps://d2d3cu3tt4cei5.cloudfront.net, though in the AWS console https is not shown.
https://gist.github.com/2694525.js?file=production.rb Before deploying into production test this on your staging app. Make sure that the assets are loaded from cache after your first request. In Chrome open the developer tools and go to the network tab. If you reload the page with F5 chrome will not use your cache, so make sure you either click somewhere in the page or simply open the page again through the NavBar. You should see lots of (from cache) for your requests. Click through your application and make sure that all of your assets are loaded from cache the second time you request them.
[caption id="attachment_54" align="alignnone" width="549"]
Check your Heroku logs as well to be sure only the bare minimum of reqeusts are sent to your application. If all works fine Congratulations you have made your application much more responsive. Now go and build something awesome (and tell me about it).
Combining Unicorn on Heroku with the Asset Pipeline and Amazon Cloudfront gives you an incredible platform to scale from. Only the bare minimum of requests are sent to your application and caches are used all along the way to make your application fast, responsive and cheap to run. If you have any questions regarding the setup or anything else you can send an email to email@example.com or a Tweet to @Codeship.
Thanks to Arvid Andersson and Tom Coleman for their blogposts.
Stay up to date
We'll never share your email address and you can opt out at any time, we promise.