• Deployment Script Spring Cleaning

    defunkt 4 Aug 2009

    Better late than never, right? As we get ready to upgrade our servers I thought it’d be a good time to upgrade our deployment process. Currently pushing out a new version of GitHub takes upwards of 15 minutes. Ouch. My goal: one minute deploys (excluding server restart time).

    We currently use Capistrano with a 400 line deploy.rb file. Engine Yard provides a handful of useful Cap tasks (in gem form) that we use along with many of the built-in features. We also use the fast_remote_cache deployment strategy and have written a handful (400 lines or so) of our own tasks to manage things like our service hooks or SVN importer.

    As you may know, Capistrano keeps a releases directory where it creates timestamped versions of your app. All your daemons and processes then assume your app lives under a directory called current which is actually a symlink to the latest timestamped version of your app in releases. When you deploy a new version of your app, it’s put into a new timestamped directory under releases. After all the heavy lifting is done the current symlink is switched to it.

    Which was really great. Before Git. So I went digging.

    First I investigated Vlad the Deployer, the Capistrano alternative in Ruby. I like that it’s built on Rake but it seems to make the same assumptions as Capistrano. Basically both of these tools are modular and built in such a way that they work the same whether you’re using Subversion, Perforce, or Git. Which is great if you’re using SVN but unfortunate if you’re using Git.

    For example, this is from Vlad’s included Git deployment strategy:

    When you deploy a new copy of your app, Vlad removes the existing copy and does a full clone to get a new version. Capistrano does something similar by default but has a bundled “remote_cache” strategy that is a bit smarter: it caches the Git repo and does a fetch then a reset. It still has to then copy the updated version of your app into a timestamped directory and switch the symlink, but it’s able to cut down on time spent pulling redundant objects. It even knows about the depth option.

    The next thing I looked at was Heroku’s rush. It lets you drive servers (even clusters of them) using Ruby over SSH, which looked very promising. Maybe I’d write a little git-deploy script based on it.

    Unfortunately for me Rush needs to be installed on every server you’re managing. It also needs a running instance of rushd. Which makes sense – it’s a super powerful library – but that wouldn’t work for deploying GitHub.

    Fabric is a library I first heard about back in February. It’s like Capistrano or Vlad but with more emphasis on being a framework/tool for remote management of servers. Easy deployment scripts are just a side effect of that mentality.

    It’s very powerful and after playing with it for a while I was extremely pleased. I’ll definitely be using it in all my Python projects. However, I wasn’t looking forward to porting all our custom Capistrano tasks to Python. Also, though I love Python, we’re mostly a Ruby shop and everyone needs to be able to add, debug, and modify our deploy scripts with ease.

    Playing with Fabric did inspire me, though. Capistrano is basically a tool for remote server management, too, if you think about it. We may have outgrown its ideas about deployment but I can always write my own deployment code using Capistrano’s ssh and clustering capabilities. So I did.

    It turned out to be pretty easy. First I created a config/deploy directory and started splitting up the deploy.rb into smaller chunks:

    $ ls -1 config/deploy
    gem_eval.rb
    import.rb
    notify.rb
    queue.rb
    services.rb
    settings.rb
    sudo_everywhere.rb
    symlinks.rb
    

    Then I pulled them in. Careful here: Capistrano override both load and require so it’s probably best to just use load.

    This separation kept the deploy.rb and each specific file small and focused.

    Next I thought about how I’d do Git-based deployment. Not too different from Capistrano’s remote_cache, really. Just get rid of all the timestamp directories and have the current directory contain our clone of the Git repo. Do a fetch then reset to deploy. Rollback? No problem.

    The best part is that because Engine Yard’s gemified tasks and our own code both call standard Capistrano tasks like deploy and deploy:update, we can just replace them and not change the dependent code.

    Here’s what our new deploy.rb looks like. Well, the meat of it at least:

    Great. I like this – very Gitty and simple. But copying and removing directories wasn’t the only slow part of our deploy process.

    Every Capistrano task you run adds a bit of overhead. I don’t know exactly why, but I imagine each task opens a fresh SSH connection to the necessary servers. Maybe. Either way, the less tasks you run the better.

    We were running about eight symlink related tasks during each deploy. Config files and cache directories that only live on the server need to be symlinked into the app’s directory structure after the reset. Cutting these actions down to a single task made everything much, much faster.

    Here’s our symlinks.rb:

    Finally, bundling CSS and JavaScript. I’d like to move us to Sprockets but we’re not using it yet and this adventure is all about speeding up our existing setup.

    Since the early days we’ve been using Uladzislau Latynski’s jsmin.rb to minimize our JavaScript. Our Cap task looked something like this:

    Spot the problem? We’re minimizing the JS locally, on every deploy, then uploading it to each server individually. We also do this same process for Gist’s JavaScript and the CSS (using YUI’s CSS compressor). So with N servers, this is basically happening 3N times on each deploy. Yowza.

    Solution? Do the minimizing and bundling on the servers. The beefy, beefy servers:

    As long as the bundle Rake tasks don’t need to load the Rails environment (which ours don’t), this is much faster.

    Conclusion

    We moved to a more Git-like deployment setup, cut down the number of tasks we run, and moved bundling and minimizing JS and CSS from our localhost to the server. Did it help?

    As I said before, a GitHub deploy can take 15 minutes (not counting server restarts). My goal was to drop it down to 1 minute. How’d we do?

    $ time cap production deploy
      * executing `production'
      * executing `deploy'
        triggering before callbacks for `deploy:update'
      * executing `notify:campfire'
      * executing `deploy:update'
      * executing `deploy:update_code'
        triggering after callbacks for `deploy:update_code'
      * executing `symlinks:make'
      * executing `deploy:bundle'
      * executing `deploy:restart'
      * executing `mongrel:restart'
      * executing `deploy:cleanup'
    
    real	0m14.361s
    user	0m2.049s
    sys	0m0.560s
    

    15 minutes down to 14 seconds. Not bad.

  • Comments

    isaachall Tue Aug 04 23:27:11 -0700 2009

    Pretty sweet. Thanks for sharing.

    ichverstehe Tue Aug 04 23:42:20 -0700 2009

    Using a Git based deploy is definitely the right thing to do.

    Opening a new SSH connection each time is going to slow down stuff, but you can have SSH use an existing connecting for subsequent connects:

    ControlMaster auto
    ControlPath ~/.ssh/master-%r@%h:%p
    
    daviscabral Wed Aug 05 00:05:20 -0700 2009

    Nice post.

    Thanks for sharing +1

    jasonwatkinspdx Wed Aug 05 01:18:04 -0700 2009

    Yeah, moving the release versions inside git is definitely the way to go. One variation I've seen still keeps the capistrano timestamp approach as local branch names. This keeps everything in sync with any deploy log you maintain as well as making it easier to revert if necessary after any number of successful deploys.

    raggi Wed Aug 05 01:26:55 -0700 2009

    Been doing similar stuff with vlad for a long time. Rejected capistrano early on because I don't want to learn a bunch of cruft so that I can write my own scripts that just execute a bunch of commands over ssh.

    leehambley Wed Aug 05 01:39:51 -0700 2009

    Thanks for the post, this is a great message for people who were worried about scaling with Capistrano, I can't think of using anything else, and the idea of Vlad turns my stomach, great to note you took a couple wildcard options into consideration!

    chrismear Wed Aug 05 03:32:23 -0700 2009

    I may be misunderstanding something, but in the rollback task it checks out HEAD^, which is just the parent commit. Would it be better to use HEAD@{1}, or something similar, to get the last checked-out commit, in case you have multiple commits between deploys?

    imbriaco Wed Aug 05 06:08:31 -0700 2009

    Nice post, Chris. You might want to take a look at fast_remote_cache as well, it's what we use to solve the same problem: http://github.com/37signals/fast_remote_cache/tree

    jamis Wed Aug 05 06:14:00 -0700 2009

    You hit the nail on the head, Chris, with rolling your own. Too many people see the default deployment tasks as the entirety of cap, and despair when the default tasks fail them. It really is not hard to write your own deployment tasks. Thank-you for writing this up!

    tapajos Wed Aug 05 06:16:43 -0700 2009

    Chris,

    I've had worked in the same problem in our system and reduced the time a lot too. Basicaly we don't delete anything during the deploy(just move), only in a cron job every night. Our deploy is very fast too.

    I will implement some of your ideas too.

    natacado Wed Aug 05 09:39:15 -0700 2009

    My understanding is that Capistrano uses the releases/YYYMMDDHHMM directory structure and current symlink as a way to ensure an atomic switchover during deployment. Does simply running git reset --hard atomically switch the release, or are you mixing in an load-balancer recipe that ensures the host doesn't receive any traffic during the deployment?

    defunkt Wed Aug 05 10:06:45 -0700 2009

    @natacado @git reset --hard@ is very, very fast.

    danielvlopes Wed Aug 05 13:50:44 -0700 2009

    Thanks for the post.

    rcrowley Wed Aug 05 17:48:16 -0700 2009

    It must not matter for a Rails app but I've found the -T option to ln very useful when writing deploy systems.

    jrmehle Thu Aug 06 08:13:20 -0700 2009

    @natacado: You can always display a maintenance page if you are concerned about limiting traffic to your site while a deployment is going on. It's not the most ideal as the people who visit your site are never happy to see a maintenance page. However if deploys on a larger site like Github only take 14 seconds, then your deploys should be shorter.

    edavis10 Thu Aug 06 08:39:55 -0700 2009

    Thanks for writing this up. My deployments have been taking longer and longer due to git submodules (N clones each deploy). Your recipes along with a basic git submodule init && git submodule update should speed it up. Thanks.

    mkrisher Fri Aug 07 12:05:34 -0700 2009

    anyone else running into an issue with overriding the rollback task?

    "/Library/Ruby/Gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/namespaces.rb:97:in task': defining a task namedrollback' would shadow an existing namespace with that name (ArgumentError)"

    brycethornton Sun Aug 09 16:57:53 -0700 2009

    Yeah, I was just going to post about the issue I was having with the rollback task. I get the exact same error.

    tpitale Tue Aug 11 19:09:34 -0700 2009

    mkrisher/brycethornton, same here. Any solution?

    catgofire Wed Aug 12 20:53:17 -0700 2009

    Wow ... 5600% improvement. That's quite impressive.

    coderifous Mon Aug 17 08:31:21 -0700 2009

    Hey Chris - check out the dancing_with_sprockets rails plugin when you are ready to move to sprockets. I like Sam's sprockets, but the sprockets-rails plugin caused me pain.

    http://github.com/coderifous/dancing_with_sprockets

    kuahyeow Mon Aug 17 22:02:06 -0700 2009

    Hmm, I'm in the same boat with @chrismear. HEAD^ will just rollback to the previous commit. HEAD@{1} seems better. Unless I'm missing something....

    kommen Wed Aug 19 05:41:09 -0700 2009

    Regarding the problem with the "defining a task named 'rollback' would shadow an existing namespace with that name (ArgumentError)" error. This is caused because "rollback" is actually a namespace with a default task. So to prevent the error you should use something like this:

    namespace :rollback do

    desc "Rollback a single commit."
    task :code, :except => { :no_release => true } do
      set :branch, "HEAD^"
      default
    end
    
    task :default do
      rollback
    end
    

    end

    kommen Wed Aug 19 05:49:33 -0700 2009

    And here the code that should actually work...

    namespace :rollback do
      desc "Rollback a single commit."
      task :code, :except => { :no_release => true } do
        set :branch, "HEAD^"
        deploy.default
      end
    
      task :default do
        rollback.code
      end
    end
    

    (the code belongs within the :deploy namespace) Next time I test the code before posting ;)

    pietern Sun Aug 23 03:12:27 -0700 2009

    @mkrisher / @kommen : As you define all the tasks yourself and only use Capistrano as a wrapper for executing commands remotely, you can remove:

    load 'deploy' if respond_to?(:namespace)
    

    from your Capfile.

    brianjlandau Fri Aug 28 07:08:37 -0700 2009

    I forked @defunkt's gist and applied some of the changes suggested above (@kommen & @chrismear) as well as a few of my own improvements. It tries to make things run as smoothly as possible with other cap recipes|plugins that you might be using or might want to use.

    shayarnett Thu Sep 10 20:26:36 -0700 2009

    I recently wrote about this on my blog. Including a gist of my full deploy.rb that can be dropped into most normal deployments.

    bronson Tue Nov 24 14:55:55 -0800 2009

    There is also Inploy: http://github.com/dcrec1/inploy

    If you're using Git and deploying to Passenger, apparently it rocks. I plan on trying it for my next project.

    Also, how about removing the spam above this comment?

    smtlaissezfaire Sun Nov 29 00:31:47 -0800 2009

    Here's a cap plugin wrapping up the idea:

    http://github.com/smtlaissezfaire/fast_git_deploy

    Please log in to comment.