Combining Capistrano and Drush for deploying Drupal powered websites
This final instalment builds on what we've run through previously to demonstrate how you can tailor a Capistrano deploy script to fit your application. We've chosen a Drupal website as an example because:
- Drupal is very popular.
- There doesn't seem to be much by way of comprehensive 'how-tos' for combining Capistrano and Drupal.
- We like using Drupal and so we already have a custom deploy script up our sleeves to write about.
For the purpose of this tutorial we're assuming you're already familiar with how to use Drush. For those of you who aren't, Drush is short for 'Drupal Shell', basically it's a tool for running functionality you'd normally run through the administration front end of a Drupal site on the Unix command line. If you're responsible for running a Drupal site and aren't using Drush yet I strongly recommend you look into adopting it.
As a starting point let's look at the Capistrano deploy script we finished with at the end of Part 3:
set :application, "example" set :stages, %w(production staging) set :default_stage, "staging" require 'capistrano/ext/multistage' set :scm, :subversion set :repository_root, "svn+ssh://svn.zodiacmedia.co.uk/example" set :deploy_via, :export #server "staging.zodiacmedia.co.uk", :app, :web, :db #set :deploy_to, "/var/www/staging.example.com" set :user, "deploy" default_run_options[:pty] = true ssh_options[:keys] = [File.join(ENV["HOME"], ".ssh", "id_rsa")] ssh_options[:port] = 22 set :use_sudo, false set :keep_releases, 5 after "deploy:update", "deploy:cleanup" set(:tag) { Capistrano::CLI.ui.ask("Enter SVN tag to deploy (or type 'trunk' to deploy from trunk): ") } set(:repository) { (tag == "trunk") ? "#{repository_root}/trunk" : "#{repository_root}/tags/#{tag}" }
Writing a tailored Cap deploy script is all about using Capistrano hooks. The following line is an example of a Capistrano hook in action:
after "deploy:update", "deploy:cleanup"
What we're telling Capistrano to do here is run the task deploy:cleanup
after the task deploy:update
has run. In this syntax deploy
is a Ruby namespace and cleanup
and update
are tasks within this namespace. You can see all of the tasks in the deploy
namespace within this file from Capistrano's source code.
Now let's cut to the chase, if 'Example.com' were a Drupal powered site we'd use the following deploy script:
set :application, "example" set :stages, %w(production staging) set :default_stage, "staging" require 'capistrano/ext/multistage' set :scm, :subversion set :repository_root, "svn+ssh://svn.zodiacmedia.co.uk/example" set :deploy_via, :export set :user, "deploy" default_run_options[:pty] = true ssh_options[:keys] = [File.join(ENV["HOME"], ".ssh", "id_rsa")] ssh_options[:port] = 22 set :use_sudo, false set :keep_releases, 5 set :drupal_file_public_path, "sites/default/files" set :drupal_file_private_path, "sites/default/files/private" set :drupal_file_temporary_path, "../../shared/tmp" after "deploy:setup", "drupal:prepare_shared_paths", "drupal:prepare_database_backup_path" before "deploy:update_code", "deploy:repository_location" "drupal:cache_clear" before "deploy:finalize_update", "drupal:rsync_shared_files" after "deploy:finalize_update", "drupal:set_permissions_for_runtime", "drupal:create_site_settings_file", "drupal:backupdb" after "deploy:update", "drupal:updatedb", "deploy:cleanup" namespace :deploy do desc "Prompt user for repository location to deploy from" task :repository_location, :except => { :no_release => true } do set :tag, Capistrano::CLI.ui.ask("Enter SVN tag to deploy (or type 'trunk' to deploy from trunk): ") set :tag, "trunk" if eval("#{:tag.to_s}.empty?") set :repository, (tag == "trunk") ? "#{repository_root}/trunk" : "#{repository_root}/tags/#{tag}" end desc "Overwrite equivalent native namespace from Capistrano" namespace :web do task :disable, :except => { :no_release => true } do drupal.site_offline end task :enable, :except => { :no_release => true } do drupal.site_online end end end namespace :drupal do desc "Include creation of additional folder for backups" task :prepare_database_backup_path, :except => { :no_release => true } do run "mkdir -p #{deploy_to}/database_backups" run "chmod 750 #{deploy_to}/database_backups" end desc "Include creation of additional Drupal specific shared folders" task :prepare_shared_paths, :except => { :no_release => true } do run "mkdir -p #{shared_path}/sites/default/files" run "mkdir -p #{shared_path}/tmp" end desc "Synchronise shared files" task :rsync_shared_files, :except => { :no_release => true } do on_rollback do run -EOC ln -s #{shared_path}/sites/default/files #{current_release}/sites/default/files EOC end run "rsync -av #{latest_release}/sites/default/files/ #{shared_path}/sites/default/files/" run "rm -rf #{latest_release}/sites/default/files" run "ln -s #{shared_path}/sites/default/files #{latest_release}/sites/default/files" end desc "Update file permissions to follow best security practice: https://drupal.org/node/244924" task :set_permissions_for_runtime, :except => { :no_release => true } do run "find #{latest_release} -type f -exec chmod 640 {} ';'" run "find #{latest_release} -type d -exec chmod 2750 {} ';'" run "find #{shared_path}/sites/default/files -type f -exec chmod 660 {} ';'" run "find #{shared_path}/sites/default/files -type d -exec chmod 2770 {} ';'" run "find #{shared_path}/tmp -type d -exec chmod 2770 {} ';'" end desc "Create site settings.php" task :create_site_settings_file, :except => { :no_release => true } do run "ln -s #{release_path}/sites/default/#{drupal_settings_file} #{release_path}/sites/default/settings.php" end desc "Backup the database" task :backupdb, :except => { :no_release => true } do release_name = Time.now.utc.strftime("%Y%m%d.%H%M%S-") + tag + ".r" + real_revision.to_s run "drush -r #{deploy_to}/current sql-dump --gzip --result-file=#{deploy_to}/database_backups/#{release_name}.sql.gz" end desc "Run Drupal database migrations if required. This applies database updates for modules installed on filesystem but DB updates haven't been run yet." task :updatedb, :except => { :no_release => true } do run "drush -r #{deploy_to}/current updatedb -y" end desc "Clear the drupal cache" task :cache_clear, :except => { :no_release => true } do run "drush -r #{deploy_to}/current cc all" end desc "Set the site offline" task :site_offline, :except => { :no_release => true } do on_rollback do run "drush -r #{deploy_to}/current vdel -y --exact maintenance_mode" end run "drush -r #{deploy_to}/current vset --exact maintenance_mode 1" end desc "Set the site online" task :site_online, :except => { :no_release => true } do run "drush -r #{deploy_to}/current vdel -y --exact maintenance_mode" end desc "Set file system variables" task :set_file_system_variables, :except => { :no_release => true } do run "drush -r #{deploy_to}/current vset --exact file_public_path #{drupal_file_public_path}" run "drush -r #{deploy_to}/current vset --exact file_private_path #{drupal_file_private_path}" run "drush -r #{deploy_to}/current vset --exact file_temporary_path #{drupal_file_temporary_path}" end end
And the corresponding 'live' and 'staging' multistage deploy scripts are respectively as follows:
server "www.example.com", :app, :web, :db, :primary => true set :deploy_to, "/var/www/www.example.com" set :drupal_settings_file, "settings.php.live"
server "staging.zodiacmedia.co.uk", :app, :web, :db, :primary => true set :deploy_to, "/var/www/staging.example.com" set :drupal_settings_file, "settings.php.staging"
A lot to digest here! I believe the task names and inline task descriptions mean that the script is accessible to competent developers so I won't go through it line by line because that will be too painful. Salient points include:
- We define our Drupal website's 'shared paths', (the files folders we want to maintain across releases) at the start of the file. We then incorporate their creation into the
deploy:setup
Capistrano task and also synchronise files exported from SVN into these locations upon deploy. - We're using a symbolic link to create the
settings.php
config file for the Drupal site so that staging and live have their own unique version of this file stored in SVN. - We define what to do
on_rollback
for some tasks. These lines of code are executed if there is an error in the release process and the task they are defined in has already run.
Happy deploying!