Working with Capistrano: Tasks, Roles, and Variables
Tags: devops, ruby, capistrano, rails
Published: 2019-11-17 19:22:20

---
title: Working with Capistrano: Tasks, Roles, and Variables
published: true
description: capistrano 
tags: devops, ruby, capistrano, rails
cover_image: https://thepracticaldev.s3.amazonaws.com/i/ktgfj65u7dk2nha7j83q.jpeg
canonical_url: https://piotrmurach.com/articles/working-with-capistrano-tasks-roles-and-variables/
---

<a href="https://capistranorb.com/" target="_blank">Capistrano</a> is a great tool for automating the deployment and maintenance of web applications. I use it to deploy Ruby on Rails and other Rack-based applications. It takes care of all the tedious bits such as installing dependencies, compiling assets or migrating database schemas. The alternative would be to manually log into a server via SSH and run all the necessary commands ourselves. This would open a path to terrible sins and consume a lot of time and patience. Thankfully we don’t have to.

The modular design of Capistrano means that there are plenty of plugins and scripts available. They will make even the thorniest deployment needs possible. Even if you don’t find what you want off the shelf, Capistrano flexible nature will help you write easily your own tasks. You will be able to tell Capistrano exactly how you want your deployment to look like. It will then prepare the server and continue to help you maintain updates to your application. 

In this series of articles, my goal is to provide core concepts and explore more advanced features of the Capistrano deployment tool. To do so, we will dive into different real-life examples of solutions to common problems maintaining web applications in Ruby. These examples will teach you the ins and outs of how to write Capistrano scripts to get the job done quickly. You should also develop a better sense of the internals of Capistrano.

All the code samples in this article assume Capistrano 3. At the time of this writing, this means installing Capistrano 3.11.2. Please read the <a href="https://capistranorb.com/documentation/getting-started/installation/" target="_blank">official guides</a> on how to set up Capistrano in your project.

With that said, let's start!

## Capistrano Tasks

At its core, Capistrano uses tasks provided by the <a href="https://github.com/ruby/rake" target="_blank">Rake</a> build tool to describe various actions that will update and deploy your application. It then supercharges these tasks with its own intuitive DSL that helps execute commands in Unix shell on a local machine or a remote server.

For a long time, I used to manually log into different servers over an SSH connection to perform one-off updates. This, in turn, meant that I needed to know and type in the shell the deployment user and server name each time I wanted to, for example, access a staging server. This is tedious and error-prone!

Surely, there is a better way?

Ideally, I would like to have a consistent way to login to any server based on the deployment environment without needing to know the details. The equivalent of saying "I want to login into the staging server". In Capistrano, this would translate into executing the following command:

```shell
$ cap staging login
```

Let's see how we could implement this command as a task.

By default, Capistrano will automatically load files from `lib/capistrano/tasks` directory. We'll go ahead and create **login.rake** file inside that directory. Notice the `*.rake` extension which is typically used to name Ruby files that contain Rake tasks. Rake provides a method called **desc** that can be used to add a task description. The description will be displayed when the user runs **cap - -tasks** to see all available tasks. Rake also exposes a method called **task** that can be used to express the behaviour of a task. Immediately after the description, we use the `task` block to register a new, albeit empty for now, `:login` task:

```ruby
# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  ...
end
```

So far the login task will do nothing. To be able to log in, we need to gather information about our application server. This requires us to talk about the concept of roles.

## Capistrano Roles

To give us a fine-grained control over which tasks are run on which servers, Capistrano uses a concept of a role. For example, you may want to apply a task only to a database server and skip it when deploying to a web server. The role acts as a filter that tells Capistrano to group tasks with the same role together before executing them on a server matching a specific role.

There are three common roles used in Capistrano tasks:

* The **app** role is used by any task that runs on an application server, a server that generates dynamic content. In Rails this is `puma` server. Capistrano's built-in tasks `deploy:check`, `deploy:publishing` or `deploy:finished` are all run in this role.
* The **db** role is used by tasks that execute against a database server. For example, the <a href="https://github.com/capistrano/rails" target="_blank">capistrano-rails</a> plugin provides the `deploy:migrate` task for migrating the Rails database schema.
* The **web** role is used by tasks that deal with web servers that serve static content, think `Nginx` here. A community created <a href="https://github.com/treenewbee/capistrano3-nginx">capistrano3-nginx</a> plugin uses this role in all of its tasks like `nginx:start`, `nginx:reload` or `nginx:site:disable`.

We can also define custom roles of our own. For example, a role called **redis** that will only run tasks related to `Redis` database instance:

```ruby
role :redis, "redis-server"
```

If we want our task to match any server and we don't care what role a server has, we can use the **all** role. Capistrano's built-in tasks use this role to stay flexible.

In our case, we will be deploying to a single machine that hosts the application, web and database servers. To apply all three roles `app`, `db` and `web` to our server, we use a shorthand definition via the **server** method:

```ruby
# config/deploy/staging.rb

server "staging-server.example.com", roles: %w[app db web], primary: true
```

If you examine the above server definition, you'll notice a property called **primary**. This property will let Capistrano know the order of precedence when running tasks. The tasks with roles associated with the primary server will run first. This becomes especially useful when your application is split between many hosts. If that's the case, then we could expand our definition to focus on roles instead:

```ruby
# config/deploy/staging.rb

role :app, "app-server.example.com"
role :db, "db-server.example.com", primary: true
role :web, "static-server.exmaple.com"
```

Moving ahead, we’ll see how to apply our defined roles in a task.

## Getting Host Configuration

To access our server configuration, we will use the **roles** helper method. This method accepts one or more role definitions and returns a list of all matching host instances. We specify an `:app` role to get, in our case, a list with only a single server configuration instance of `Capistrano::Configuration::Server`.

Among many innovations, Capistrano 3 introduces more modular architecture. It offloads management of SSH session to another dependency, a Ruby gem called <a href="https://github.com/capistrano/sshkit" target="_blank">sshkit</a>. The `SSHKit` via its DSL provides an **on** method that allows us to run commands described in a block scope on many servers. As an argument, the `on` takes an array of host configuration objects and then uses `SSHKit::Coordinator` to run commands in parallel on each host:

```ruby
# lib/sshkit/dsl.rb

module SSHKit
  module DSL
    def on(hosts, options={}, &block)
      Coordinator.new(hosts).each(options, &block)
    end
  end
  ...
end
```

If you have more than one host configured, it's probably better to instruct Capistrano to login to each server in a sequence:

```ruby
on roles(:app), in: :sequence do |server|
  ...
end
```

Inside the `on` scope we gain access to the host instance that contains information about our server. Essentially, we're saying "on this server do the following things":

```ruby
# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    ...
  end
end
```

## Capistrano Variables

To `ssh` into a server we need to know the user name, the server name and the path to log the user into. For your configuration needs, Capistrano provides a method called **set** that allows you to configure variable like settings globally or for specific tasks and make them available to other parts of your script. For example, we can set the `:user` and `:deploy_to` variables in the following way:

```ruby
# config/deploy/staging.rb

set :user, "deploy-user"
set :deploy_to, "/path/to/deploy/directory"
```

To help us read our configuration variables, Capistrano makes available a method called **fetch**. For example, we can use it to get the user name and deployment path:

```ruby
# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)
    ...
  end
end
```

Now that we know the user name and path to login into, we can construct a URI that we will use as an argument to `ssh` command utility. We build the URI string by concatenating the user name, server name and port number. The code will handle the cases where the user name or port are missing:

```ruby
# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)
  
    uri = [
      user,
      user && ‘@’,
      server.hostname,
      server.port && “:”,
      server.port
    ].compact.join
  end
end
```

## The SSH Command

We're now ready to run the **ssh** command with the URI and deployment path. Before we do that though, there are a few interesting things to note here. In particular, the **-t** flag which instructs **ssh** utility to run in teletype mode. What is teletype mode? By default, Capistrano deploys via SSH connection without attaching a visual terminal. There is no need to show beautiful interfaces as nobody can see anything anyway. But, in our case we want the user to have access to all the shell capabilities.

After the **-t** flag we specify the URI location of the server. Then in quotes enclose shell commands to run once logged in. We string together two shell commands. First, we navigate to the website root directory and then execute a **$SHELL** variable. The **$SHELL** is an environment variable that holds the location for the default shell, usually **Bash**. Why do we need to start the shell? Without executing the shell, the ssh command would finish and log us out. But we want to remain logged in, that’s the whole point of this task! In case of `bash`, we use **-l** flag to invoke it as if a user logged in. We do this to preload all the configurations in hidden files such as the Bash profile.

The final command string will look like this:

```ruby
"ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'"
```

We use Ruby's `exec` to run our `ssh` command locally:

```ruby
exec("ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'")
```

Putting everything together, the entire task looks like this:

```ruby
desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)

    uri = [
      user,
      user && '@',
      server.hostname,
      server.port && ":",
      server.port
    ].compact.join

    exec("ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'")
  end
end
```

We talked about logging quickly into a server from our machine. But this is not the only way to enhance our everyday workflow with Capistrano.

## Remote Rails Console

Often I need to jump straight into Rails console on a server and run some queries against a live database. I could use the login task and then once logged in, type in a full command to open the Rails console. But this would get annoying quickly. How about we script Capistrano to log us straight into a production Rails console instead? The way we would like to do this is by running the following command:

```shell
$ cap production console
```

To implement the above, inside the same **login.rake** file, we write a bare-bones `:console` task with a description:

```ruby
# lib/capistrano/tasks/login.rake

desc "Open Rails console on a server based on the deployment stage"
task :console do
    ...
end
```

Like the login task, to access our server, we need to know the user name and deployment path. Besides, to open the Rails console we also need to know the environment that the Rails application runs in. But, we can glean this from the `:rails_env` configuration variable which is configured by the <a href="https://github.com/capistrano/rails" target="_blank">capistrano-rails</a> gem:

```ruby
# lib/capistrano/tasks/login.rake

desc "Open Rails console on a server based on the deployment stage"
task :console do
  on roles(:app) do |server|
    env = fetch(:rails_env)
    user = fetch(:user)
    path = fetch(:deploy_to)
    ...
  end
end
```

## Finding Rails Executable

Before we can open Rails console via SSH, we need to deal with one extra issue. Because we don't have access to a pseudo-terminal, none of the user profile configuration files will be loaded. We need to somehow tell Capistrano where to find the **rails** executable and in turn where to find Ruby installation. For me, the preferred way is to use the <a href="https://github.com/rbenv/rbenv" target="_blank">rbenv</a> utility to manage Ruby installation and the <a href="https://github.com/bundler/bundler" target="_blank">Bundler</a> for gems installation. If you use another Ruby manager please adjust your paths appropriately.

For the `rbenv` path, we use the deployment user local installation location of **$HOME/.rbenv**. Rbenv has a concept of shims, which are lightweight executables that map installed Ruby commands such as `gem`, `irb` or `rails` to `rbenv exec` command. The `rbenv exec` will then prepend a specific command with a correct Ruby installation path. To execute the `rails` command, we need to use `bundle` shim which will ensure all our dependencies are loaded. We can access the `bundle` executable in **$HOME/.rbenv/shims** directory. As a last step, we inform the `rails console` command about the environment we're using via the **-e** flag. The full command to open rails console looks like this:


```ruby
console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"
```

With the final piece of the puzzle assembled, we can `ssh` into the current Rails application directory and open the Rails console:

```ruby
"ssh -t #{uri} 'cd #{path}/current && #{console_cmd}'"
```

Now let's tie everything together in the final script:

```ruby
desc "Open Rails console on a server based on the deployment stage"
task :console do
  on roles(:app) do |server|
    env  = fetch(:rails_env)
    user = fetch(:user)
    path = fetch(:deploy_to)

    uri = [
      user,
      user && '@',
      server.hostname,
      server.port && ":",
      server.port
    ].compact.join

    console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"

    exec("ssh -t #{uri} 'cd #{path}/current && #{console_cmd}'")
  end
end
```

## Removing Duplication

Now all you eagle-eyed readers probably noticed that we have a bit of repetitive code between the tasks. Let’s tackle it so we can finish strong and improve maintainability. One thing I like to do before I remove any clutter is to repeat code verbatim, like I did in the second task. This is so that I can visually highlight the parts that are the same. We can see that apart from the rails environment variable and ssh command to run, the tasks are identical.

In the spirit of removing duplication, we will move the common part of setup and running ssh command into its own method called `run_ssh_with`. The method will accept as arguments the server configuration and command to run:

```ruby
# lib/capistrano/tasks/login.rake

def run_ssh_with(server, cmd)
  user = fetch(:user)
  path = fetch(:deploy_to)

  uri = [
    user,
    user && "@",
    server.hostname,
    server.port && ":",
    server.port
  ].compact.join

  exec("ssh -t #{uri} 'cd #{path}/current && #{cmd}'")
end
```

Thanks to the `run_ssh_with`, we can simplify both tasks into:

```ruby
# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    run_ssh_with(server, "exec $SHELL -l")
  end
end

desc "Open console on a remote server based on the deployment stage"
task :console do
  on roles(:app) do |server|
    env  = fetch(:rails_env)
    console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"
    run_ssh_with(server, console_cmd)
  end
end
```

That's so much nicer! Before we finish though, there is one more thing we can do to appease our programmer nature that craves optimization in every keyboard keystroke. We can create aliases for our two tasks! Rake doesn't provide an alias feature per se but we can fake it. The way to do it is to define a new task whose execution will depend on invoking another task first. Let’s abbreviate both tasks to single letters:

```ruby
# lib/capistrano/tasks/login.rake

task :c => :console
task :l => :login
```

## Summary

We've finished a whirlwind tour of Capistrano. It's a lot to take in if you've never written a Capistrano script before. Even if you have, hopefully, this has clarified for you some of the Capistrano's features. This article should give you a general understanding of how a task works, how to configure variables, and how to run any command locally.

The login and console tasks provide a quick way of automating rudimentary jobs. One important side effect of the tasks is consistency across Rails projects. You don’t need to know the details to be able to quickly search the project files or query database on a remote server. These little efficiencies accumulate over time and create a smooth development workflow. If you have similar useful Capistrano tasks please share them!

---

This article was originally published on <a href="https://piotrmurach.com/articles/working-with-capistrano-tasks-roles-and-variables/" target="_blank">PiotrMurach.com</a>.

Photo by <a href="https://unsplash.com/@rocknrollmonkey">Rock'n Roll Monkey</a> on Unsplash