Wednesday, December 12, 2012

Rails 3.1 Asset Syncing to S3 on Heroku

Rails 3.1's Asset Pipeline is a wonder in management of static assets in modern web development. It streamlines the process of gzipping, minifying, and fingerprinting asset files as well as setting HTTP cache headers and integrating into a reverse-proxy cache server such as Rack-Cache (comes with Rails) or Varnish.

On Heroku's new Cedar stack however, one cannot use a high-performance static asset server like Nginx or even a high-performance reverse-proxy server like Varnish. So, the next high-performance option, which is arguably higher-performance is remote asset hosting on Amazon S3 or a CDN (Content Distribution Network) like Amazon Cloudfront.

Fortunately, one gem - asset_sync - comes to the rescue. It automates syncing of static assets from your app to S3 or a CDN upon asset compilation.

I used it to serve static content from S3 for a website I am architecting, so I would like to share these tips with anyone who has attempted to follow Heroku's asset syncing guide:
https://devcenter.heroku.com/articles/cdn-asset-host-rails31

If you try to have it sync automatically on deploy to Heroku, it fails miserably due to Heroku not loading its ENV variables in production mode. We missed that when we deployed to staging because asset syncing was working there during the Heroku asset compilation step.

To avoid falling for that trap, you need to compile assets locally, commit, and push. Unfortunately, this can get painful with setting all the ENV vars for asset_sync or passing it options that are supposed to be secure passwords you do not want to put in the git repo.

I wrote a rake task (inspired by a colleague's rake file) to automate the process of grabbing the ENV vars from Heroku instead, compiling, committing, pushing, and then deploying to heroku all in one command.

Code:
https://gist.github.com/4273735

module RailsApplication
module Deploy
class << self
def run(*cmd)
system(*cmd)
raise "Command #{cmd.inspect} failed!" unless $?.success?
end
def deploy(app, branch = "master")
branch ||= "master" #in case it was passed as nil
puts "-----> Compiling Assets..."
heroku_env_vars = heroku_app_env_vars(app)
heroku_env_vars.each {|k, v| ENV[k] = v unless ENV[k].present?}
run "git checkout #{branch}"
Rake::Task["assets:precompile"].execute
run "git add public/assets"
run "git commit -m'deploy compiled assets'"
run "git push origin #{branch}"
puts "-----> Pushing..."
#run "git push git@github.com:RailsApplication/#{app}.git HEAD:master -f"
run "git push --force git@heroku.com:#{app}.git #{branch}:master"
puts "-----> Migrating..."
run "heroku run rake db:migrate --app #{app}"
puts "-----> Seeding..."
run "heroku run rake db:seed --app #{app}"
puts "-----> Restarting..."
run "heroku restart --app #{app}"
end
def heroku_app_env_vars(app)
heroku_env_vars_output = `heroku config -a #{app}`
heroku_env_vars_output_split = heroku_env_vars_output.split("\n")
heroku_env_var_lines = heroku_env_vars_output_split[1, heroku_env_vars_output_split.size]
heroku_env_vars = heroku_env_var_lines.inject({}) do |output, line|
match_data = line.match('([^:]+):(.*)')
output.merge match_data[1].strip => match_data[2].strip
end
end
end
end
end
desc "Deploy heroku app from branch (or master by default)"
task :deploy, [:app, :branch] => [:environment] do |t, args|
RailsApplication::Deploy.deploy(args[:app], args[:branch])
end
view raw deploy.rake hosted with ❤ by GitHub
Usage:
rake deploy app=heroku_app_name branch=branch_to_deploy

Alternate syntax:
rake deploy[heroku_app_name,branch_to_deploy]

Default branch as master:
rake deploy[heroku_app_name]

2 comments:

Jesse Curry said...

There is a Heroku labs feature that will include the environment variables for you, that allows you to simply deploy as usual and have the asset pipeline work its magic.

Jesse Curry said...

And the link: https://devcenter.heroku.com/articles/labs-user-env-compile