Managing software deployments of your PHP applications II

If you enjoyed this article, please leave a comment, rss subscribe to my RSS feed and/or follow me on Twitter. Thank you very much!

This is not (really a part) two of my series, but an Intermezzo (1) between Part I and Part III — because I have no time to finish Part III.

In Part I, I talked about my approach to deploying a website and I offered pear and subversion as solutions to the problem. To briefly elaborate on my subversion part, I want to share the following Capistrano recipe with you.

Capistrano, isn't that Ruby?

Yes it is! Capistrano is a nifty piece of software. Not because it's so super-duper complex, but because it's pretty robust and works really well.

I am sure everyone heard of the "the right tool for the job" theory and this is along those lines. Please don't be afraid of a little Ruby to ease the deployment process. (They don't deserve your fear anyway. ;-))

Installation

On FreeBSD:

cd /usr/ports/devel/rubygem-capistrano && make install clean

On Debilian/Ubuntu:

apt-get install rubygems
gem install capistrano

Go on and create a capfile in $HOME, and this is where we'll keep our tasks.

A short introduction

A capfile (in a nutshell) is a description of tasks to do. Generally, you define a couple roles role :www and entities (webserver01...). set :foo, "bar" are declarations of variables (in PHP: $foo = "bar";) used inside the tasks. And generally things you may have to adjust when you copy and paste my recipe. The #{foo} are the same variables used inside a tasks.

The Tasks

Checkout a release

At first I need a task to check out a new release on all servers. If you remember Part 1 of this series, you remember my subversion layout:

/repo/tags/1.0/
/repo/tags/1.1/
/repo/trunk/
...

And this is the task to do the checkout:

desc "Checkout release on the servers"
task :checkout_release, :roles => :www do

    set(:version, Capistrano::CLI.ui.ask("Version: "))

    run "svn co #{svn_repo}/#{version} #{www_dir}/#{version}"

    run "sudo chown -R #{www_user} #{www_user}/#{version}/var/tmp"
    run "sudo chown -R #{www_user} #{www_user}/#{version}/var/upload"
end

The set(:version, Capistrano::CLI.ui.ask("Version: ")) is particularly interesting because it allows you to specify a version when you execute the tasks. If you don't want the prompt, just remove it and replace it with set :version, "1.0" at the beginning. I enjoy the extra bit of flexibility though.

Deploy it

desc "Put a release live, or rollback to a previous release"
task :deploy, :roles => :www do
    set(:version, Capistrano::CLI.ui.ask("Version: "))

    run "sudo sudo ln -sf #{www_dir}/#{version} #{www_dir}/nginx-root"
    run "sudo #{phpfcgid} restart"
    run "sudo #{nginx} restart"
end

The above task does the following:

  • update the symlink
  • restart my php-cgi processes (start script is here and here)
  • restart nginx

Put it all together!

role :www, "webserver1.example.org", "webserver2.example.org", "webserver3.example.org"

set :svn_repo, "http://example.org/svn/repo/tags"
set :www_dir, "/var/www/example.org"
set :www_user, "www-data"

set :nginx, "/etc/init.d/nginx"
set :phpfcgid, "/etc/init.d/phpfcgid"

desc "Checkout release on the servers"
task :checkout_release, :roles => :www do

    set(:version, Capistrano::CLI.ui.ask("Version: "))

    run "svn co #{svn_repo}/#{version} #{www_dir}/#{version}"

    run "sudo chown -R #{www_user} #{www_user}/#{version}/var/tmp"
    run "sudo chown -R #{www_user} #{www_user}/#{version}/var/upload"
end

desc "Put a release live, rollback"
task :deploy, :roles => :www do
    set(:version, Capistrano::CLI.ui.ask("Version: "))

    run "sudo sudo ln -sf #{www_dir}/#{version} #{www_dir}/nginx-root"
    run "sudo #{phpfcgid} restart"
    run "sudo #{nginx} restart"
end

desc "Deploy a hotfix to specific tag/version"
task :hotfix, :roles => :www do
    set(:version, Capistrano::CLI.ui.ask("Version: "))

    run "cd #{www_dir}/#{version} && sudo svn update"

    run "sudo #{phpfcgid} restart"
end

Note:, there's a hotfix task included, but it should be obvious what it does. ;-)

The next step is to put this into your capfile and then run cap -T in the same directory to display all available tasks. It should look similar to this:

admin01% cap -T
cap checkout_release # Checkout release on the servers
cap deploy           # Put a release live, rollback
cap hotfix           # Deploy a hotfix to specific tag/version
cap invoke           # Invoke a single command on the remote servers.
cap shell            # Begin an interactive Capistrano session.

There are our tasks, checkout_release, deploy and hotfix. invoke and shell are included in capistrano, a simple example invoke would look like the following:

admin01% cap invoke COMMAND="df -h" ROLES=www
...

... the above would show us df -h from all servers defined in www.

Execution

Executing tasks is rather simple — I also gave it away with the above:

admin01% cap checkout_release

Refactoring

So, after a while, your capfile will probably grow beyond two or three tasks and each task will even include duplicate code, so here some Ruby so you can avoid that:

def hello (txt='world')
    p txt
    puts "Hello #{txt}"
    run "/some/command --switch #{txt}"
end

def and end make a function in Ruby, simple as that. The rest of it should be obvious to any beginner PHP coder. The paramter is txt, it is optional and therefor has a default value. p in Ruby is similar to PHP's var_dump(). The rest is self explanatory!

Chaining

If you're a little familiar with jQuery, you probably love it's chaining capabilities. An example would be:

 $('#foo').load('/some/file').show('slow');

In a nutshell, this loads /some/file into the container (<div id="foo" />) and displays the container when it's done — I'm assuming here, it was hidden.

Chaining in Capistrano is a little different, but not less clever.

Example

The naming is somewhat fail (maybe, epic), but anyway: after to the rescue!

set :foo, "/tmp/foo"
desc "Create a file!"
task :create_foo, :roles => :server do
    run "touch #{foo}"
end

desc "Make foo world writable!"
task :make_foo_writable, :roles => :server do
    run "chmod 777 #{foo}"
end

after "create_foo" do
    make_foo_writable
end

after makes sure that after the create_foo task ran, the make_foo_writable task is executed! So simple too, but it took me a while and a bit of Google to extract a meaningful example from a mailing list.

Conclusion?

I'm loving it! I'm currently in the process of turning our deployment into true auto-deployment with as little zero intervention in order to make use of RightScale's nifty autoscaling setup. More on this in another post.

For further reading I'd suggest Capistrano's Getting Started, The Absolute Moron's Guide to Capistrano and this Ruby Tutorial with Code examples. Also, special thanks to my co-worker Lukas Rieder for some initial Ruby foo and general Capistrano pointers! :-)

| More