Merge pull request #250 from wvengen/feature-deploy

deployment with Capistrano 3 (in progress)
This commit is contained in:
wvengen 2014-01-27 03:08:24 -08:00
commit 381d5de0a3
8 changed files with 267 additions and 58 deletions

View file

@ -65,9 +65,11 @@ group :development do
gem 'quiet_assets'
# Deploy with Capistrano
gem 'capistrano', '2.13.5', require: false
gem 'capistrano-ext', require: false
#gem 'common_deploy', require: false, path: '../../common_deploy' # pending foodcoops/foodsoft#34, git: 'git://github.com/fsmanuel/common_deploy.git'
gem 'capistrano', '~> 3.0', require: false
# https://github.com/capistrano/rails/issues/48#issuecomment-31443739
gem 'capistrano-rvm', github: 'capistrano/rvm', require: false
gem 'capistrano-bundler', '>= 1.1.0', require: false
gem 'capistrano-rails', require: false
# Avoid having content-length warnings
gem 'thin'
end

View file

@ -4,6 +4,14 @@ GIT
specs:
localize_input (0.1.0)
GIT
remote: git://github.com/capistrano/rvm.git
revision: 6aa7cb9d75361c802f466b54d0e345b7237ea3bb
specs:
capistrano-rvm (0.1.1)
capistrano (~> 3.0)
sshkit (~> 1.2)
GIT
remote: git://github.com/fnando/i18n-js.git
revision: eab4137f83777963f0ebe6960704a7f64fd8911d
@ -74,14 +82,16 @@ GEM
bullet (4.7.1)
activesupport
uniform_notifier (>= 1.4.0)
capistrano (2.13.5)
highline
net-scp (>= 1.0.0)
net-sftp (>= 2.0.0)
net-ssh (>= 2.0.14)
net-ssh-gateway (>= 1.1.0)
capistrano-ext (1.2.1)
capistrano (>= 1.0.0)
capistrano (3.1.0)
i18n
rake (>= 10.0.0)
sshkit (~> 1.3)
capistrano-bundler (1.1.1)
capistrano (~> 3.0)
sshkit (>= 1.2.0)
capistrano-rails (1.1.0)
capistrano (>= 3.0.0)
capistrano-bundler (>= 1.0.0)
capybara (2.2.1)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
@ -143,7 +153,6 @@ GEM
actionpack (>= 3.2, < 5)
activesupport (>= 3.2, < 5)
hashery (2.1.1)
highline (1.6.20)
hike (1.2.3)
i18n (0.6.9)
i18n-spec (0.4.0)
@ -191,11 +200,7 @@ GEM
mysql2 (0.3.14)
net-scp (1.1.2)
net-ssh (>= 2.6.5)
net-sftp (2.1.2)
net-ssh (>= 2.6.5)
net-ssh (2.7.0)
net-ssh-gateway (1.2.0)
net-ssh (>= 2.6.5)
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
pdf-reader (1.3.3)
@ -321,6 +326,10 @@ GEM
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.8)
sshkit (1.3.0)
net-scp (>= 1.1.2)
net-ssh
term-ansicolor
term-ansicolor (1.2.2)
tins (~> 0.8)
therubyracer (0.12.0)
@ -370,8 +379,10 @@ DEPENDENCIES
binding_of_caller
bootstrap-datepicker-rails
bullet
capistrano (= 2.13.5)
capistrano-ext
capistrano (~> 3.0)
capistrano-bundler (>= 1.1.0)
capistrano-rails
capistrano-rvm!
capybara
client_side_validations
client_side_validations-simple_form

View file

@ -1,43 +1,48 @@
require 'bundler/setup'
require 'common_deploy'
#
# Capistrano 3 deployment configuration
#
# http://www.capistranorb.com/
# https://semaphoreapp.com/blog/2013/11/26/capistrano-3-upgrade-guide.html
set :application, 'foodsoft'
set :domain, 'foodsoft.com'
set :user, 'foodsoft'
set :default_stage, 'staging' # staging and production are available via (set :stages, ["staging", "production"])
set :keep_releases, 5
set :repository, 'git://github.com/foodcoops/foodsoft.git'
set(:deploy_to) { "/mnt/apps/#{application}_#{stage}" }
# defaults that can be updated from the environment
set :branch, ENV["REVISION"] || ENV["BRANCH_NAME"] || "master"
# you probably want to change these
set :application, 'foodsoft' # application name (whatever you like)
set :domain, 'order.foodcoop.test' # host
set :user, 'deploy' # ssh deploy user
set :keep_releases, 10
set :repo_url, 'git://github.com/foodcoops/foodsoft.git'
set :deploy_to, "/www/apps/#{fetch :application}-#{fetch :stage}"
# resque worker
role :resque_worker, domain
role :resque_scheduler, domain
set :workers, { "foodsoft_notifier" => 1 }
# more settings which are probably ok
set :log_level, :info
set :linked_files, %w{config/database.yml config/app_config.yml config/initializers/secret_token.rb}
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}
# assuming one server for everything, with one user for deploy and one for resque
server fetch(:domain), user: fetch(:user), roles: [:web, :app, :resque, :db]
# rvm
# if you use RVM, uncomment the line in Capfile, and optionally uncomment rvm settings
# set :rvm_ruby_string, :local
# task hooks
namespace :deploy do
server domain, :web, :app, :db
desc 'Restart application'
task :restart do
on roles(:app), in: :sequence, wait: 5 do
# tell mod_passenger to reload the application
execute :touch, release_path.join('tmp/restart.txt')
end
end
# Loads all needed capistrano extensions
load_extensions :bundler,
# :rvm, # if you are using rvm on your server uncomment this line
:passenger,
:multistage,
:resque,
:whenever,
:assets
after :restart, 'resque:restart'
after :finishing, 'deploy:cleanup'
# clean up old releases on each deploy
after "deploy:restart", "deploy:cleanup"
# see lib/capistrano/tasks/plugins.cap
#before 'bundler:install', 'enable_plugins:auto'
# restart resque
after "deploy:restart", "resque:restart"
end
# install rvm and ruby on every deploy
# before 'deploy', 'rvm:install_rvm' # update RVM
# before 'deploy', 'rvm:install_ruby' # install Ruby and create gemset (both if missing)

View file

@ -2,25 +2,40 @@ Deployment
=========
Setup
--------
-----
cp config/deploy.rb.SAMPLE config/deploy.rb
touch config/deploy/staging.rb
touch config/deploy/production.rb
1. Initialise your [Capistrano](http://capistranorb.com/) setup
```sh
bundle exec cap install
sed -i 's|^# \(require.*rails.*\)|\1|' Capfile
cp config/deploy.rb.SAMPLE config/deploy.rb
```
When you're using [RVM](http://rvm.io/) on the server you may want to
uncomment the corresponding line in `Capfile`.
2. Adapt your configuration in `config/deploy.rb` and `config/deploy/*.rb`
Deploy
--------
------
On your first deploy you should run
On your first deploy you should run (choose either staging or production)
bundle exec cap deploy:setup
bundle exec cap deploy:check
bundle exec cap staging deploy:check
This will fail, which is ok, because there is no configuration yet. On your
server, there is a directory `shared/config` for each installation, which
contains the configuration. Create `database.yml`, `app_config.yml` and
`initializers/secret_token.rb` and try again.
(See `lib/capistrano/tasks/deploy_initial.cap` for a way to automate this.)
Deploy to staging
bundle exec cap deploy
bundle exec cap staging deploy
Deploy to production
bundle exec cap production deploy

View file

@ -0,0 +1,101 @@
# Capistrano tasks for the initial setup
namespace :deploy do
desc 'Creates and initialises a new foodsoft instance'
task :initial do
before 'deploy:check:linked_files', 'deploy:initial:touch_shared'
before 'deploy:updated', 'deploy:initial:secret_token'
before 'deploy:updated', 'deploy:initial:app_config'
before 'deploy:updated', 'deploy:initial:db:config'
before 'deploy:updated', 'deploy:initial:db:create'
before 'deploy:migrate', 'deploy:initial:db:load'
end
after :initial, :deploy
namespace :initial do
namespace :db do
desc 'Generate new database.yml with random password'
task :config => ['deploy:set_rails_env'] do
require 'securerandom'
on roles(:app), in: :groups do
db_name = (fetch(:db_user) or fetch(:application))
db_passwd = SecureRandom.urlsafe_base64(24).to_s
db_yaml = {
fetch(:rails_env).to_s => {
'adapter' => 'mysql2',
'encoding' => 'utf8',
'database' => db_name,
'username' => db_name,
'password' => db_passwd,
}
}
execute :mkdir, '-p', shared_path.join("config")
upload! StringIO.new(db_yaml.to_yaml), shared_path.join("config/database.yml")
end
end
# assumes mysql access setup (~/.my.cnf), with permissions
desc 'Create database new database'
task :create => ['deploy:set_rails_env'] do
on roles(:app), in: :sequence do
config = capture :cat, shared_path.join("config/database.yml")
config = YAML.load(config)[fetch(:rails_env).to_s]
# http://www.grahambrooks.com/blog/create-mysql-database-with-capistrano/
execute :mysql, "--execute='CREATE DATABASE IF NOT EXISTS `#{config['database']}`';"
execute :mysql, "--execute='GRANT ALL ON `#{config['database']}`.* TO \"#{config['username']}\" IDENTIFIED BY \"#{config['password']}\";'"
end
end
desc 'Load database schema'
task :load => ['deploy:set_rails_env'] do
on roles(:app), in: :groups do
# workaround nonexistent release_path on first deploy
path = releases_path.join(capture(:ls, releases_path).split("\n").sort.last)
within path do
with rails_env: fetch(:rails_env) do
execute :rake, 'db:schema:load'
end
end
end
end
end
desc 'Writes a new secret token'
task :secret_token do
require 'securerandom'
on roles(:app), in: :groups do
secret = SecureRandom.hex(64)
text = "Foodsoft::Application.config.secret_token = \"#{secret}\""
execute :mkdir, '-p', shared_path.join("config/initializers")
upload! StringIO.new(text), shared_path.join("config/initializers/secret_token.rb")
end
end
desc 'Creates a default app_config.yml'
task :app_config do
on roles(:app), in: :groups do
execute :mkdir, '-p', shared_path.join("config")
# workaround nonexistent release_path on first deploy
path = releases_path.join(capture(:ls, releases_path).split("\n").sort.last)
execute :cp, path.join("config/app_config.yml.SAMPLE"), shared_path.join("config/app_config.yml")
end
end
desc 'Touches the shared configuration files (for initial deploy)'
task :touch_shared do
on roles(:app), in: :groups do
execute :mkdir, '-p', shared_path.join("config/initializers")
execute :touch, shared_path.join("config/initializers/secret_token.rb")
execute :touch, shared_path.join("config/app_config.yml")
execute :touch, shared_path.join("config/database.yml")
end
end
end
end

View file

@ -0,0 +1,51 @@
# Capistrano tasks for enabling/disabling foodsoft plugins in the Gemfile
#
# Please note that the foodsoft plugins should be present already in the
# Gemfile, either commented out or not.
#
# To automatically enable the desired plugins on deployment, create the
# file `config/plugins.yml` in the shared directory, containing the
# key `enabled` with a list of enabled plugin names (without foodsoft_).
# Then add to your `config/deploy.rb`:
# before 'bundler:install', 'enable_plugins:auto'
desc 'Enable only the foodsoft plugins, cap enable_plugins PLUGINS=wiki,messages'
task :enable_plugins do
on roles(:app), in: :groups do
unless env['PLUGINS'].nil?
enable_foodsoft_plugins(ENV['PLUGINS'].split(/,\s*/))
else
raise 'You need to set the PLUGINS environment variable to enable specific plugins'
end
end
end
namespace :enable_plugins do
desc 'Enable the foodsoft plugins specified in shared/config/plugins.yml, if it exists (key `enabled`).'
task 'auto' do
on roles(:app), in: :groups do
text = capture :cat, shared_path.join('config/plugins.yml'), '||true'
if text
plugins = YAML.load(text)
enable_foodsoft_plugins(plugins['enabled']) if plugins and not plugins['enabled'].nil?
end
end
end
end
# need to run in role
def enable_foodsoft_plugins(plugins)
gemfile = capture :cat, release_path.join('Gemfile')
gemfile.gsub! /^\s*(#)?\s*(gem\s+(['"])foodsoft_(.*?)\3)/ do |c|
(plugins.index($4) ? '' : '#') + $2
end
upload! StringIO.new(gemfile), release_path.join('Gemfile')
# since we updated the Gemfile, we need to run bundler in non-deployment mode
new_bundle_flags = fetch(:bundle_flags).split(/\s+/)
new_bundle_flags.reject! {|o| o=='--deployment'}
new_bundle_flags << '--no-deployment'
set :bundle_flags, new_bundle_flags.join(' ')
end

View file

@ -0,0 +1,24 @@
# capistrano-resque could be used, but it does not support running resque as another user.
# If you want to run resque as another user, setup sudo to allow running commands as that user:
# deploy ALL=(foodsoft_user) NOPASSWD: ALL
# and set `:run_user` to the foodsoft user.
namespace :resque do
%w{start stop restart}.each do |action|
desc "#{action.capitalize} Resque workers"
task action => ['deploy:set_rails_env'] do
on roles(:resque), in: :groups do
within current_path do
cmd = command(:rake, "resque:#{action}_workers", "RAILS_ENV=#{fetch(:rails_env)}")
if fetch(:run_user).nil? or fetch(:run_user) == local_user
execute cmd
else
execute 'sudo', '-u', fetch(:run_user), cmd
end
end
end
end
end
end

View file

@ -4,7 +4,7 @@ def run_worker(queue, count = 1)
puts "Starting #{count} worker(s) with QUEUE: #{queue}"
ops = {:pgroup => true, :err => ["log/resque_worker_foodsoft_notifier.log", "a"],
:out => ["log/resque_worker_foodsoft_notifier.log", "a"]}
env_vars = {"QUEUE" => queue.to_s, "PIDFILE" => "tmp/pids/resque_worker_foodsoft_notifier.pid", "VERBOSE" => "1"}
env_vars = {"QUEUE" => queue.to_s, "PIDFILE" => "tmp/pids/resque_worker_foodsoft_notifier.pid"}
count.times {
## Using Kernel.spawn and Process.detach because regular system() call would
## cause the processes to quit when capistrano finishes