chr4

Devops Diary

Chef deploy_revision and Capistrano git_style

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.

1
2
3
4
5
6
7
8
9
10
11
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

1
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.

1
2
3
4
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
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 certian commit (ambigious statement)

I was addressing those issues in the following way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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.