I spend a lot of time lately working on server orchestration type of activities. With the amount of Magento instances I manage and the code bases that make them what they are it’s very beneficial to me to have a streamlined deployment process. If you have experience running multi-node Magento stores you will agree with me when I say Dev-Ops tools like Ansible and Capistrano are must haves in your tool set. In this post I will describe how to get started using Capistrano for Magento deployments from a private Github repository. This post assumes you are on Red Hat/Fedora/CentOS based linux distro.

Prerequisites:
Note: I’m using an older version of Capistrano since I haven’t modified my recipe to work with the latest version and I really have no reason to use the latest version.


# if you dont have git installed
sudo yum install git
# if you dont have ruby installed
sudo yum install ruby ruby-devel rubygems
# install capistrano gem
gem install capistrano -v 2.15.5

Setup your deployment key
In order for Capistrano to be able to connect to your private Github repo you will need to create deployment key. I usually store mine in /usr/share/githubkeys.


mkdir /usr/share/githubkeys/
cd /usr/share/githubkeys/

sudo ssh-keygen -t rsa -C

When it asks you to save the key put:
/usr/share/githubkeys/myRepoName.id_rsa
Enter passphrase (empty for no passphrase): [press enter leave empty]


# chmod 400 your keys (very important)
sudo chmod /usr/share/githubkeys/myRepoName.id_rsa
sudo chmod /usr/share/githubkeys/myRepoName.id_rsa.pub

Add the host mapping for Github:
This trick allows you to have multiple hosts configured that use different private keys. It is especially important if you plan on managing large scale deployments across multiple repositories.


sudo vi ~/.ssh/config
# add this to your ~/.ssh/config

Host deploy-myRepoName.github
        Hostname github.com
        Port 22
        User git
        IdentityFile /usr/share/githubkeys/myRepoName.id_rsa

Add deploy key to Github repo:
Visit your Github repo at github.com and click “Settings” -> “Deploy keys”


cat /usr/share/githubkeys/myRepoName.id_rsa.pub

Paste in the contents on myRepoName.id_rsa.pub (no line breaks!!)

Test the Github connectivity:
Run the command below, if it succeeds you will see Github complain about how it doesn’t provide shell access, but the goal is to at least see if you ssh config host file and key work.


ssh -vT deploy-myRepoName.github

Setup Public Key based SSH logins between your App Nodes
While Capistrano can be setup to use SSH usernames and passwords I tend to set it up to use public key authentication. When Capistrano deploys code to each of your app nodes it does it by initiating a SSH connection as the user that ran “cap”. So this means in order for public key authentication to work you must have your users public key on each app node.

For the purpose of this tutorial I’m assuming you will be using the App Node #1 server as the deployment server. Some people setup a dedicated deployment server strictly for deployment.

First check to see if you have a public key on App Node #1:


cat ~/.ssh/id_rsa.pub

If you dont see anything outputted from the above command you haven’t setup a public key for your user. If you do see something you can skip the next step.

Creating the public key on App Node #1:


ssh-keygen -q -t rsa -f ~/.ssh/id_rsa -N ""
cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Creating the public key on App Node #2:


ssh-keygen -q -t rsa -f ~/.ssh/id_rsa -N ""
cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Once you have created the public key. Copy the contents of: (App Node #1)


cat ~/.ssh/id_rsa.pub

Paste the contents into the ~/.ssh/authorized_keys file on App Node 2.

You can test the connectivity between App Node #1 and App Node #2 by trying to SSH from App Node #1 to App Node #2 without issuing a password:


ssh appnode2

Setup the deployment folder structure:
In order for Capistrano to work it needs a place to live. I usually create a deploy folder for it. Note this location is not where your code lives, just the Capistrano recipe for deploying code.


# run these commands as root
sudo -s
mkdir -p /var/www/deploy/myRepoName
cd /var/www/deploy/myRepoName
capify .

Setup the deployment recipe
When “capify .” above it created a config folder with a deploy.rb file in it. This deploy.rb file is the Capistrano recipe for deploying code. It ships with a default recipe. We are going to modify it to work with our github repo.

Edit the /var/www/deploy/myRepoName/config/deploy.rb to look like this:


default_run_options[:pty] = true

set :application, 'MyStore'
set :deploy_via, :checkout
set :use_sudo, false
set :checkout, "export"
set :scm, "git"

# make sure to set this to the host your added in your ~/.ssh/config
# format is host you added to ~/.ssh/config
# host:[github.com repo name]/repoName.git
set :repository, "deploy-myRepoName.github:myRepoName/myRepoName.git"

# symlinks you need in your deployment
# you obviously don't want to keep your app/etc/local.xml in your repo
# nor do you want to keep your media or var directories
set :app_symlinks, ["/media", "/var"]
set :app_shared_dirs, ["/app/etc", "/media", "/var"]
set :app_shared_files, ["/app/etc/local.xml"]

# define the hostnames to your app nodes
role :web, "appnode1", "appnode2"

# set variables when deploying your dev site
task :dev do
    set :htaccess_file, "_dev.htaccess"
    set :branch, "develop"
   	set :deploy_to,	"/var/www/sites/mystore-dev"
end

# set variables when deploying your prod site
task :prod do

	puts "\n\e[0;31m   ######################################################################" 
	puts "   #\n   #       Are you REALLY sure you want to deploy to prod?"
	puts "   #\n   #               Enter y/N + enter to continue\n   #"
	puts "   ######################################################################\e[0m\n" 
	proceed = STDIN.gets[0..0] rescue nil 
	exit unless proceed == 'y' || proceed == 'Y' 

	set :htaccess_file, "_prod.htaccess"
            set :branch, "master"
            set :deploy_to,	"/var/www/sites/mystore-prod"
end


# deployment procedures
namespace :deploy do

    task :update do
        transaction do
            update_code
            compass_compile
            symlink
        end
    end

    task :finalize_update do
        transaction do
            run "chmod -R g+w #{releases_path}/#{release_name}"
            run "chmod +x #{releases_path}/#{release_name}/cron.sh"
            run "chown -R :apache #{latest_release}"
        end
    end

    task :symlink do
        transaction do

            if app_symlinks
                # remove the contents of the shared directories
                app_symlinks.each { |link| run "#{try_sudo} rm -rf #{latest_release}#{link}" }
                # add symlinks the directoris in the shared location
                app_symlinks.each { |link| run "ln -nfs #{shared_path}#{link} #{latest_release}#{link}" }
            end

            if app_shared_files
                # remove the contents of the shared directories
                app_shared_files.each { |link| run "#{try_sudo} rm -rf #{latest_release}#{link}" }
                # add symlinks the directoris in the shared location
                app_shared_files.each { |link| run "ln -nfs #{shared_path}#{link} #{latest_release}#{link}" }
            end

            # put your .htaccess file in place
            run "cd #{latest_release} &&  mv #{htaccess_file} .htaccess"

            # update the 'current' link
            run "ln -nfs #{current_release} #{deploy_to}/#{current_dir}"

        end
    end

    task :compass_compile do
        transaction do
            # compile css for your theme
            run "cd #{latest_release} && compass compile --force skin/frontend/rwd/mytheme/"
        end
    end

end

# run this tasks after you issue deploy:setup
after "deploy:setup", :magento_shared_dirs

# this sets up all the symlinks for your store
task :magento_shared_dirs, :roles => :web do

    if app_shared_dirs
        app_shared_dirs.each { |link| run "#{try_sudo} mkdir -p #{shared_path}#{link} && chmod 777 #{shared_path}#{link}" }
    end

    if app_shared_files
        app_shared_files.each { |link| run "#{try_sudo} touch #{shared_path}#{link} && chmod 777 #{shared_path}#{link}" }
    end

    # clean up some files
    run "cd #{shared_path} && rm -rf pids && rm -rf system && rm -rf log"

end

# enable maintenance mode across your app nodes
task :disable, :roles => :web do
    run "cd #{current_path} && touch maintenance.flag"
end

# disable maintenance mode across your app nodes
task :enable, :roles => :web do
    run "cd #{current_path} && rm -f maintenance.flag"
end

Notes on the deploy.rb
My deploy.rb file contains a task for compiling the SCSS files my theme uses. Magento 1.9/EE 1.14 uses compass to compile them. If you are not using compass or SCSS files in your theme you can remove the task.

Finalize folder structure
Capistrano will setup all the folders and symlinks automatically for you :)


# make sure to run this as root
sudo -s
cd /var/www/deploy/myRepoName
cap dev deploy:setup

Setup Folder permissions
If you are like me you probably have a specific user you want to own your web files. Since you ran the “cap deploy:setup” as root you will need to change the ownership of the folders Capistrano created. You don’t want them being owned by root.


sudo chown www:apache /var/www/sites/mystore-dev -R

Deploy first code using Capistrano
Up until this point we haven’t actually used Capistrano to deploy code. All we have done is get it setup and the folder structure created. The following commands illustrate how you can deploy code to your app nodes.


cd /var/www/deploy/myRepoName

# for the first deploy issue:
cap dev deploy

# for deploying production code issue:
cap prod deploy

# for setting up maintenance page issue:
cap disable
# and to re-enable
cap enable

Advance Techniques
Sometimes it is handy to test code up to a specific commit_id SHA. Here is an example of deploying the develop branch to a specific commit id.


cap -s revision=dca090fa4a24c219cae38ac7cb3c49b7c40823f6 dev deploy

You can also use variables in your config.rb recipe and pass them via the command line. For example if you want to add a way to enable and disable the compiling of compass you could add a variable to the top of your config.rb like this:


set :compass, "on"

Then later in your config.rb you could use a switch case like this:


case compass
    when 'on'
    run "cd #{latest_release}/#{modman_folder}skin/frontend/rwd/mytheme && compass compile --force"
end

Then you could pass the parameter to your deploy like this:


cap dev deploy -s compass=off

case branch
when 'master'
  run "cd #{latest_release}/#{modman_folder}skin/frontend/rwd/mytheme && mv config.rb.prod config.rb"
when 'develop'
  run "cd #{latest_release}/#{modman_folder}skin/frontend/rwd/mytheme && mv config.rb.dev config.rb"
end

This is just a brief explanation of how I use Capistrano for deployments. Another neat thing you can do is keep a main repo with your Magento core code base and add in additional code using a task for modman. Its pretty handy little trick for decoupling your core code base for maintainability. Imagine a “cap dev modman update” command that ran on your app nodes.

Some more resources and information on git and Capistrano can be found in the Ruby Docs here.

  • http://sergeif.me/ Sergei Filippov

    Great write up, very handy! Ansible+Capistrano make a potent mix. Will be using all of this and adding a task for modman and n98-magerun