One thing that was annoying me for a long time, was that, using Capistrano deployment, you cannot spawn a new vanilla virtual machine, and bring it to a fully up-and-running state with just one Chef command.

make deploy_revision compatible with Capistrano, so deployments can happen with Capistrano, until we’ve decided to fully migrate to Chef, or to stick with the push deployment

Attributes

Let’s manage some things with attributes, so we can adjust them centrally later, in case needed.

default['rails']['app-root'] = '/var/www/example.com'
default['rails']['owner'] = 'deploy'
default['rails']['group'] = 'deploy'

# The shared directories that we're going to need later
default['rails']['shared_directories'] = %w{
  shared
  shared/config
  shared/pids
  shared/sockets
}

Workaround to maintain the git repository with a unprivileged user

Due to a bug in git, we cannot use the “user” and “group” attributes in the deploy resource. This would result in the following error

STDERR: fatal: unable to access '/root/.config/git/config': Permission denied

Therefore, we’re chowning the app directory manually after the deploy. See CHEF-3940.

execute 'chown app root' do
  command "chown -R #{node['rails']['owner']}:#{node['rails']['group']} #{node['rails']['app-root']}"
  action :nothing
end

bundle install

We cannot start up the application without an installed bundle. The following example mimicks the Capistrano way of “bundle install”, using chruby

execute 'bundle install' do
  user node['rails']['owner']
  cwd "#{node['rails']['app-root']}/current"

  # bundler needs LC_ALL set, to prevent "invalid byte sequence in US-ASCII" error
  # HOME also needs to be set to allow a user install
  environment 'LC_ALL' => 'en_US.UTF-8',
              'HOME'   => ::File.dirname(node['rails']['app-root'])

  command [ "/usr/local/bin/chruby-exec #{node['rails']['ruby-string']} --",
            "bundle install --gemfile #{node['rails']['app-root']}/current/Gemfile",
                           "--path #{node['rails']['app-root']}/shared/bundle",
                           "--deployment --quiet --without development test" ].join(' ')
  action :nothing
end

The deploy resource

Now to the deploy resource. This was a little tricky, as we’re using capistrano_fanfare’s git_style deployment strategy in our deploy.rb

set :deploy_via, :git_style

This has several advantages to timestamped deploys, and actually Chef’s deploy_revision is using a similar approach. Unfortunately, there are some differences:

  1. The directory where the repository is checked out. Capistrano (with :git_style) uses current, whereas Chef uses shared/cached-copy.

  2. Release management. Capistrano creates empty folders in releases using timestamp-SHA directory names, Chef creates SHA directories, with the complete repository content (but without .git)

  3. Chef symlinks currentto releases/current-SHA, and actually works with the releases/current-SHA directory during deployment, so we need to setup a temporary link to current

  4. Chef places an empty file with SHA of the current commit as the filename in the repository_cache directory. This gives Capistrano hickups when checkout out a certain commit (ambigious statement)

I was addressing those issues in the following way:

deploy_revision node['rails']['app-root'] do
  repository 'git@github.com:chr4/rails-app.git'

  # checkout the repository to current, as capistrano :git_style would do
  repository_cache "../current"

  before_symlink do
    # create shared directories
    node['rails']['shared_directories'].each do |dir|
      directory "#{node['rails']['app-root']}/#{dir}" do
        owner node['rails']['owner']
        group node['rails']['group']
        mode  00755
      end
    end

    # remove the release directory (not a real git repo)
    directory release_path do
      recursive true
      action :delete
    end

    # create a workaround symlink
    link release_path do
      to "#{File.dirname(release_path)}/../current"
    end
  end

  # create symlinks to shared directory
  purge_before_symlink %w{log tmp/sockets tmp/pids}
  create_dirs_before_symlink %w{tmp}

  symlinks( { 'sockets' => 'tmp/sockets',
              'pids'    => 'tmp/pids',
              'log'     => 'log'} )


  # remove the workaround symlink after restarting services
  after_restart do
    link release_path do
      action :delete
    end

    # remove strange SHA file, which is hindering capistrano
    link "#{File.dirname(release_path)}/../current/#{File.basename(release_path)}" do
      action :delete
    end
  end

  # owner workaround (see comment above, CHEF-3940)
  notifies :run,     'execute[chown app root]', :immediately

  # install dependencies and start up application
  notifies :run,     'execute[bundle install]', :immediately
  notifies :restart, 'service[unicorn]'

  # only run deployment once
  # succeeding deploys will be done using capistrano (for now)
  not_if "test -e #{node['rails']['app-root']}/current"
end

The not_if statement allows us to continue using Capistrano to deploy like before, until we might decide to fully migrate to a continous deployment using Chef.