8 min read

In part 1, we covered Capistrano and why you would use it. We also covered mixins, which provide the base for what we will do in this post, which is to deploy a sample project using Capistrano.

For this project, suppose our user interface is a combination of two applications,app1 and app2. They should be deployed to servers do and ec2. And we’ll provide two environments,production and cert.

Make sure Ruby and Bundler are installed before you start. First, we create a new directory for our project, and add a Gemfile to it with capistrano as a dependency. Then we will create the Capistrano directory structure:

mkdircapsample
cd capsample
bundle init
echo "gem 'capistrano'" >>Gemfile
bundle
bundle exec cap install STAGES="do_prod_app1,do_prod_app2,do_cert_app1,do_cert_app2,ec2_prod_app1,ec2_prod_app2,ec2_cert_app1,ec2_cert_app2"

This will create nine files under config/deploy, one for each server/environment/application group. This is just to demonstrate the idea. We’ll completely override their entire content later on. It will also create a Capfile file that works in a similar way to a regular Rakefile. With Rake, you can get a list of the available tasks with rake -T. With Capistrano you can get the same using:

bundle exec cap -T

Behind the scenes, cap is a binary distributed with the capistrano gem that will run Rake with Capfile set as the Rakefile and supporting a few other options like —roles.Now create a new file,lib/mixin.rb, with the content mentioned in the Using mixins section in part 1. Then add this to the top of the Capfile:

$: . unshiftFile.dirname(__FILE__)
require'lib/mixin'

Each of the files under config/deploy will look very similar to each other. For instance, ec2_prod_app1 would look like this:

mixin 'servers/ec2'
mixin'environments/production'
mixin'applications/app1'

Then config/mixins/servers/ec2.rb would look like this:

server 'ec2.mydomain.com', roles: [:main]
set :database_host, 'ec2-db.mydomain.com'

This file contains definitions that are valid (or default) for the whole server, no matter what environment or application we’re deploying. In this example the database host is shared for all applications and environments hosted on our ec2 server.

Something to note here is that we’re adding a single role named main to our server. If we specified all roles, like [:web, :db, :assets, :puma], then they would be shared with all recipes relying on this server mixin. So, a better approach would be to add them on the application’s recipe, if required. For instance, you might want to add something like set :server_name, ‘ec2.mydomain.com’ to your server definitions. Then you can dynamically set the role in the application’s recipe by calling role :db, [fetch(:server_name)] and so on for all required roles.

However, this is usually not necessary for third-party recipes as they let you decide which role the recipe should act on. For example, if you want to deploy your application with Puma you can write set :puma_role, :main.

Before we discuss a full example for the application recipe, let’s look at what config/mixins/environments/production.rb might look like:

set :branch, 'production'
set :encoding_key, '098f6bcd4621d373cade4e832627b4f6'
set :database_name, 'app_production'
set :app1_port, 3000
set :app2_port, 3001
set :redis_port, 6379
set :solr_port, 8080

In this example, we’re assuming that the ports for app1 and app2 , Redis and Solr will be the same for production in all servers, as well as the database name.

Finally, the recipes themselves, which tell Capistrano how to set up an application, will be defined byconfig/mixins/applications/app1.rb. Here’s an example for a simple Rails application:

Rake :: Task['load:defaults'].invoke
Rake::Task['load:defaults'].clear
require'capistrano/rails'
require'capistrano/puma'
Rake::Task['load:defaults'].reenable
Rake::Task['load:defaults'].invoke

set :application, 'app1'
set :repo_url, '[email protected]:me/app1.git'
set :rails_env, 'production'
set :assets_roles, :main
set :migration_role, :main
set :puma_role, :main
set :puma_bind, "tcp://0.0.0.0:#{fetch :app1_port}"

namespace :railsdo
desc'Generate settings file'
task :generate_settingsdo
on roles(:all) do
template ="config/templates/database.yml.erb"
dbconfig=StringIO.new(ERB.new(File.read template).result binding)
upload! dbconfig, release_path.join('config', 'database.yml')
end
end
end

before 'deploy:migrate', 'rails:generate_settings'
# Create directories expected by Puma default settings:
before 'puma:restart', 'create_log_and_tmp'do
on roles(:all) do
within shared_pathdo
execute :mkdir, '-p', 'log', 'tmp/pids'
end
end
end

Make sure you remove the lines that set application and repo_url on the config/deploy.rb file generated bycap install. Also, if you’re deploying a Rails application using this recipe you should also add the capistrano-rails andcapistrano3-puma gems to your Gemfile and run bundle again. In case you’re running rbenv or rvmto install Ruby in the server, make sure you include either capistrano-rbenv or capistrano-rvm gems and require them on the recipe. You may also need to provide more information in this case. For rbenv you’d need to tell it which version to use with set :rbenv_ruby, ‘2.1.2’ for example.

Sometimes you’ll find out that some settings are valid for all applications under all environments in all servers. The most important one to notice is the location for our applications as they must not conflict with each other. Another setting that could be shared across all combinations could be the private key used to connect to all servers. For such cases, you should add those settings directly to config/deploy.rb:

set :deploy_to, -> { "/home/vagrant/apps/#{fetch :environment}/#{fetch :application}" }
set :ssh_options, { keys: %w(~/.vagrant.d/insecure_private_key) }

I strongly recommend connecting to your servers with a regular account rather than root. For our applications we use userbenv to manage our Ruby versions, so we’re able to deploy them as regular users as long as our applications listen to high port numbers. We’d then setup our proxy server (nginx in our case) to forward the requests on port 80 and 443 to each application’s port accordingly to the requested domains and paths. This is set up by some Chef recipes. Those recipes run as root in our servers. To connect using another user, just pass it in the server declaration. To connect to [email protected], this is how you’d set it up:

server '192.168.33.10', user: 'vagrant', roles: [:main]
set :ssh_options, { keys: %w(~/.vagrant.d/insecure_private_key) }

Finally, we create a config/database.yml that’s suited for our environment on demand, before running the migrations task. Here’s what the template config/templates/database.ymlcould look like:

production:
adapter: postgresql
encoding: unicode
pool: 30
database: <%= fetch :database_name %>
host: <%= fetch :database_host %>

I’ve omitted the settings for app2 , but in case it was another Rails application, we could extract the common logic between them to another common_rails mixin.

Also notice that because we’re not requiring capistrano/rails and capistrano/puma in the Capfile, their default values won’t be set as Capistrano has already invoked the load:defaults task before our mixins are loaded. That’s why we clear that task, require the recipes, and then re-enable and re-run the task so that the default for those recipes have the opportunity to load.

Another approach is to require those recipes directly in the Capfile. But unless the recipes are carefully crafted to only run their commands for very specific roles, it’s likely that you can get unexpected behavior if you deploy an application with Rails, another one with Grails, and yet another with NodeJS. If any of them has commands that run for all roles, or if the role names between them conflict somehow you’d be in trouble. So, unless you have total control and understanding about all your third-party recipes, I’d recommend that you use the approach outlined in the examples above.

Conclusion

All the techniques presented here are used to manage our real complex scenario at e-Core, where we support multiple applications in lots of environments that are replicated in three servers. We found that this allowed us to quickly add new environments or servers as needed to recreate our application in no time. Also, I’d like to thank Juan Ibiapina, who worked with me on all these recipes to ensure our deployment procedures are fully automated—almost. We still manage our databases and documents manually because we prefer to.

About the author

Rodrigo Rosenfeld Rosas lives in Vitória-ES, Brazil, with his lovely wife and daughter. He graduated in Electrical Engineering with a Master’s degree in Robotics and Real-time Systems. For the past five years Rodrigo has focused on building and maintaining single page web applications. He is the author of some gems includingactive_record_migrations,rails-web-console, the JS specs runner oojspec, sequel-devise, and the Linux X11 utility ktrayshortcut. Rodrigo was hired by e-Core (Porto Alegre-RS, Brazil) to work from home, building and maintaining software for Matterhorn Transactions Inc. with a team of great developers. Matterhorn’s main product, the Market Tracker, is used by LexisNexis clients .

LEAVE A REPLY

Please enter your comment!
Please enter your name here