Working with Capistrano: Environment Variables and Remote Commands
Tags: devops, ruby, bash, tutorial
Published: 2019-12-30 11:51:26

---
title: Working with Capistrano: Environment Variables and Remote Commands
published: true
description: This time around, we're going to write a more involved script composed of many tasks. By themselves, these tasks won't accomplish too much. But the sum of them all will help us automate the installation and upgrading of Ruby throughout the lifetime of our application.
tags: devops, ruby, bash, tutorial
cover_image: https://thepracticaldev.s3.amazonaws.com/i/cx6g2l8piy0nwodm19v3.jpeg
canonical_url: https://piotrmurach.com/articles/working-with-capistrano-environment-variables-and-remote-commands/
---

In the <a href="/piotrmurach/working-with-capistrano-tasks-roles-and-variables-4em6">"Working with Capistrano: Tasks, Roles, and Variables"</a> article, we explored Capistrano's fundamental concepts. We wrote two useful tasks. One for logging into a server and another for accessing Rails console in a production system. This time around, we're going to write a more involved script composed of many tasks. By themselves, these tasks won't accomplish too much. But the sum of them all will help us automate the installation and upgrading of Ruby throughout the lifetime of our application.

Not so long ago, every deployment server that I worked with had only one version of Ruby installed. This system-wide installation forced all Rails applications to use a very specific version of Ruby, for example, 2.1.5. You were not only stuck with a Ruby version but also married to a particular Rails release that had a minimum Ruby requirement. It doesn't take much to realise the shortcomings of this approach. Any Ruby upgrade requires a new production server setup and a costly migration effort. Who has the time and money for this? From a reliability point of view, Ruby becomes a single point of failure. If Ruby were to become corrupted in some way then this would affect all the web applications on the same server.

The way to ease this pain is to use a Ruby version manager. A tool that will allow many different versions of Ruby coexist on a single server. What follows, each Rails application can have its own Ruby environment and be updated independently. Developers can easily change Ruby during the lifetime of their application without affecting other projects - a nice bonus.

There are many great tools such as <a href="https://rvm.io/" target="_blank">RVM</a> or <a href="https://github.com/postmodern/chruby" target="_blank">chruby</a> that can help you manage Rubies. But, `rbenv` is my preferred choice. It strikes a perfect balance between features and simplicity. The project is also maintained by Ruby core team members and that's what I will demonstrate in this article. While we explore rbenv, we will lift the veil and examine more Capistrano techniques to add to your toolbox. 

In the code samples that follow, we'll install files on a server in some places and read them back from others. I'll assume a Unix like operating system such as Linux since that's what I'm most familiar with. You may need to adjust a few bits according to your operating system of choice.

With that out of the way, let's dive into `rbenv` installation.

## Where to Install rbenv?

Before we decide where to install rbenv, we will want to name and organise our new task first. A great feature that Capistrano inherits from Rake build tool is the namespace method. It gives us a way to logically group tasks under one name. A side benefit is that we can reuse similar task names that otherwise could conflict with the built-in ones. I tend to group the tasks based on the overall function that ties them together. The rbenv as a namespace seems to fit well. We'll go ahead and inside *lib/capistrano/tasks* directory create a file called **rbenv.rake** with the `:rbenv` namespace:

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

namespace :rbenv do
  ...
end
```

To install rbenv, we need to decide where we want it installed first. There are two primary ways to install rbenv: *system-wide* and *per user account*. What's the difference? The system-wide installation means that a given Ruby version is installed only once per server in a central location. For example, on a Unix system that would be **/usr/local/rbenv**. Conversely, user account installation restricts a given Ruby to locations accessible to that particular user. Usually, this is a hidden folder inside the user's home directory, for example, **$HOME/.rbenv**. Choosing the latter may lead to having similar Ruby versions exist on the same server. It is a balancing act.
  
  As we discussed before, the system-wide installation suffers from a single point of failure issue. Because of this, by default, we're going to install rbenv for each deployment user account. But we will expose `:rbenv_path` variable to allow configuration of a custom path. To do so, we will create a helper method **rbenv_path** that will hold a rbenv installation path, by default, set to the user's home directory:

```ruby
def rbenv_path
  fetch(:rbenv_path, "$HOME/.rbenv")
end
```

The **fetch** method accepts as a second argument, a default value that will be used when no value is set in your script. There is another way we could do this by using a block.

```ruby
def rbenv_path
  fetch(:rbenv_path) { "$HOME/.rbenv" }
end
```

The difference is that the default value provided via the block will be lazy evaluated. This means that the evaluation of default value will only happen when the `rbenv_path` method is first used. This comes handy in a situation when you have, for example, a dynamic value that depends on the evaluation of other methods. We are going to stick with the second argument option here.

One aside: when working with the filesystem paths, it's better to represent them as data types rather than strings. What does this mean? A string is a poor representation for a system path. It doesn't give you any extra information, for example, whether a path is relative or absolute. Ruby's standard library includes **Pathname** that can wrap path string representation. Unlike string, the pathname object can tell us information about the path it represents and enhance our path operations. We can build paths in a system-independent way. Joining two paths is as easy as adding them together. The pathname object will automatically use the correct file path separator. If you want to learn more, I recommend <a href="https://robm.me.uk/ruby/2014/01/18/pathname.html" target="_blank">Paths aren't strings</a> article.

Armed with this knowledge, we change `rbenv_path` to return a pathname object instead:

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

require "pathname"

namespace :rbenv do
  def rbenv_path
    Pathname.new fetch(:rbenv_path, "$HOME/.rbenv")
  end
end
```

Now, we need to find a way to tell Capistrano to use the `rbenv_path` method to find rbenv executable and make it available for other tasks to use.

## The PATH Variable

When deploying applications, Capistrano uses what is called a *non-login* shell. In other words, Capistrano doesn't assume anything about the underlying system apart from establishing an SSH session. There is no visual terminal attached to this session. None of the shell configuration files, like a Bash profile file, are loaded. This is very much desirable behaviour that makes our deployments safe and repeatable. But it also means that we cannot rely on any configuration environment variables to be preloaded and available for our scripts.

Specifically, we cannot rely on the system to tell our scripts the location of rbenv binary installation. How can we work around this? We need to roll up our sleeves and show Capistrano where rbenv is installed ourselves. The way we can do this is by modifying the **$PATH** environment variable. This variable holds a list of directories delimited by a colon that the Unix shell uses to search for executable files when running commands. To check the value of your path, you can run the following in your shell:

```shell
$ echo $PATH
```

Your path may look something like this:

```shell
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
```

This begs the question...how can we instruct Capistrano to change what locations does the $PATH variable point to? Remember, we're trying to make rbenv available for the non-login shell.

## Mapping Binaries with SSHKit

The heart of Capistrano - the <a href="https://github.com/capistrano/sshkit" target="_blank">sshkit</a> Ruby gem - is a toolkit for working with servers via SSH. The sshkit is responsible for doing the heavy lifting in terms of defining and executing commands on a server. Besides, it provides access to the current SSH session configuration with the `SSHKit.config` method. In particular, the `config` exposes the `default_env` method that, by default, returns an empty hash. When a command gets executed, all the keys and their values are converted into the corresponding environment variables. Bear in mind that only the keys provided as symbols will change to an uppercase name:

```ruby
SSHKit.config.default_env[:path] = "$HOME/.rbenv/bin:$PATH"
```

This is equal to:

```ruby
SSHKit.config.default_env["PATH"] = "$HOME/.rbenv/bin:$PATH"
```

Both examples will result in a command being prefixed with a path:

```shell
$ PATH="$HOME/.rbenv/bin:$PATH"
```

The `default_env` is exactly the place to change the `$PATH` variable to include the location of rbenv installation path. Specifically, we want to add two directories with executables:

* `$HOME/.rbenv/bin`
* `$HOME/.rbenv/shims`

Now, we're ready to get our hands dirty and make Capistrano locate rbenv. We begin by adding a task called **map_bins** that will make common Ruby binaries use rbenv executable - but we're getting ahead of ourselves. Let's change the $PATH variable first.

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

require "pathname"

namespace :rbenv do
  desc "Map Ruby binaries to use rbenv"
  task :map_bins do
    ...
  end

  def rbenv_path
    Pathname.new fetch(:rbenv_path, "$HOME/.rbenv")
  end
end
```

With the task defined, we read the current value for the `:path` key to preserve what other tasks may have already configured:

```ruby
desc "Map Ruby binaries to use rbenv"
task :map_bins do
  path = SSHKit.config.default_env[:path]
end
```

You may have noticed that we haven't used the **on** scope. The scope is only needed when we want to execute remote commands on a server or access the server's configuration. None of this is necessary for the task at hand.

Next, we alter the path with the location of rbenv directories. We use `rbenv_path` helper to add our two rbenv locations. If the path isn't configured, we prepend the two directories directly to the $PATH variable. If the path is configured, we prepend on top of it:

```ruby
desc "Map Ruby binaries to use rbenv"
task :map_bins do
  path = SSHKit.config.default_env[:path]
  path_env = "#{rbenv_path.join('shims'}:#{rbenv_path.join('bin')}:" +
             (path.nil? ? "$PATH" : path)
end
```

Finally, we override the SSHKit `default_env` hash with a new modified environment path:

```ruby
desc "Map Ruby binaries to use rbenv"
task :map_bins do
  path = SSHKit.config.default_env[:path]
  path_env = "#{rbenv_path.join('shims'}:#{rbenv_path.join('bin')}:" +
             (path.nil? ? "$PATH" : path)
  SSHKit.config.default_env.merge!(path: path_env)
end
```

But, we're not done yet. If we're going to do anything useful a server like installing Rails, we need to have a way to run Ruby commands like `gem` or `bundle`. This is necessary so that we can later issue the following commands to bootstrap a Rails application:

```shell
$ gem install bundler
$ bundle install
```

Let's say we want to run `"bundle install"` command but there are many Ruby versions installed already. Which version of Ruby would provide the `bundle` executable? We need to somehow tell our shell which Ruby version to use. Again the `$PATH` variable comes to the rescue. We can extend the `$PATH` with the correct Ruby version before running our executable. For example, to use bundle executable from the Ruby 2.6.3 installation, we would run the following in the Unix shell:

```shell
$ PATH="$HOME/.rbenv/versions/2.6.3/bin:$PATH" bundle install
```

But, this command is long, ugly, and hard to automate. Thankfully, the rbenv provides **rbenv exec** command that can detect the currently loaded Ruby version and include it in our path for us. Since we have already configured Capistrano to find rbenv binary, we can use `exec` to rewrite our previous command like this:

```shell
$ rbenv exec bundle install
```

Now we know how to write our commands by hand. But how do we tell Capistrano to prepend common Ruby executables like `bundle` with the `rbenv exec` command for us? Before we answer this question, it's helpful to understand how an **execute** method works first.

## Running Remote Commands

The `sshkit` provides an **execute** method for running any shell command on a server. This is a powerful method that behaves differently depending on the arguments passed in. A peek under the covers reveals a rather succinct and short implementation:

```ruby
# lib/sshkit/backends/abstract.rb

def execute(*args)
  options = args.extract_options!
  create_command_and_execute(args, options).success?
end
```

The above snippet doesn't look like much but there is a whole machinery hidden behind the `execute` method. Conceptually, the process of running any command is split into two parts. The first part takes the command and converts it into an `SSHKit::Command` object. This object handles assembling a full, shell-ready command from various bits like prefixes, flags and arguments. The second part takes care of executing the command which can be carried away by any of the `SSHKit::Backends`. This approach lets you create command once and run it using different backends on many servers. This is such a great design decision!

When run with a 'simple' string without any whitespace, the execute hands over the command first to `SSHKit::CommandMap`. Internally, the `CommandMap` holds a hash of commands, empty by default, that allow additional configuration:

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

module SSHKit
  class CommandMap
    def initialize(value = nil)
      @map = CommandHash.new(value || defaults)
    end
  end
end
```

By default, if there are no custom prefixes specified, a command is prefixed with **"/usr/bin/env"**. Except for `if`, `test`, `time` and `exec`, commands that receive no prefixes. For example, if we were to check Ruby version using ruby executable as the first argument:

```ruby
execute :ruby, "--version"
```

This would result in the default lookup and the following output:

```shell
/usr/bin/env ruby --version
```

If, on the other hand, we were to provide a command as a raw string with whitespace:

```ruby
execute "ruby --version"
```

It would be executed as-is without any prefixing, shell escaping or directory changes. Basically no involvement from Capistrano or `SSHKit` whatsoever:

```shell
ruby --version
```

This often tricks newcomers to Capistrano who scratch their head in puzzlement as to why a command fails to run on a server. So consider yourself warned.

## Prefixing Commands

To change commands before they are run, we can use the **command_map** method exposed by the `SSHKit` configuration object. We can then take the `CommandMap` and chain it together with the **prefix** method to add new prefixes. Like `CommandMap`, the returned `PrefixProvider` stores a hash of command to prefix mappings. By default, any key lookup for non-existing command returns an empty array. Knowing this, we can override a default ruby command prefix:

```ruby
SSHKit.config.command_map.prefix[:ruby] << "rbenv exec"
```

Then run our ruby command again:

```ruby
execute :ruby, "--version"
```

But this time, we get a very different result:

```shell
rbenv exec ruby --version
```

That was a long but necessary diversion from the `map_bins` task implementation. Now, we know how to prefix common executables like `bundle`, `gem`, or `rake` with `rbenv exec`. First, we will add **`rbenv_map_bins`** method to allow for the configuration of Ruby binaries defaulting them to the most common:

```ruby
def rbenv_map_bins
  fetch(:rbenv_map_bins, %w[bundle gem rails ruby rake])
end
```

Armed with our knowledge of customising commands, we can prefix each binary and ensure we don't change any existing prefixes:

```ruby
rbenv_map_bins.each do |exe|
  SSHKit.config.command_map.prefix[exe.to_sym].unshift("rbenv exec")
end
```

Having prefixed Ruby binaries, the complete task for mapping commands will look like this:

```ruby
desc "Map Ruby binaries to use rbenv"
task :map_bins do
  path = SSHKit.config.default_env[:path]
  path_env = "#{rbenv_path.join('shims')}:#{rbenv_path.join('bin')}:" +
             (path.nil? ? "$PATH" : path)
  SSHKit.config.default_env.merge!(path: path_env)

  rbenv_map_bins.each do |exe|
    SSHKit.config.command_map.prefix[exe.to_sym].unshift("rbenv exec")
  end
end
```

Now running `"bundle install"` (and similar) commands will work because they will be executed with the correct Ruby version. We can apply our task by running:

```shell
$ cap [stage] rbenv:map_bins
```

What we have configured so far is for the non-login shell. But, if a user wants to log into a server, the rbenv executable won't be available and none of the Ruby binaries will work. Let's change this!

## Modifying Shell Startup File

It would be annoying to force anyone who logs into a server to manually run rbenv setup instructions before they could run, for example, a bundle command.

So what's the solution?

When a user logs into an operating system, this triggers loading of the shell startup files. A shell startup file allows for execution of arbitrary shell scripts, setting environment variables and so on. Sounds like just what we need. We could modify the shell startup file to set rbenv paths for us. This would make Ruby and any related executables available inside the logged-in user shell.

Many Linux distributions like Debian, typically, use a Bash shell as their default shell. When started as an interactive shell, Bash reads a startup file called `bash_profile`. The following Bash script is all we need to insert into a bash startup file to load rbenv automatically:

```shell
if [ -d ~/.rbenv ]; then
  export PATH="$HOME/.rbenv/bin:$PATH"
  eval "$(rbenv init -)"
fi
```

This snippet of code first checks that rbenv directory exists. When it does, it adds rbenv binary to the `$PATH` variable and makes it available in the shell. It then runs the rbenv initialisation script.

Now, we have everything we need to create a task called **`modify_shell_file`** that will, as you've probably guessed, modify a shell startup file. We use the **on** scope with **all** role to specify the servers affected by our changes:

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

namespace :rbenv do
  desc "Map Ruby binaries to use rbenv"
  task :map_bins do
    ...
  end

  desc "Change shell startup file to setup rbenv"
  task :modify_shell_file do
    on release_roles(:all) do
    end
  end
end
```

## Testing Remote Commands

What we don't want to do is to add rbenv initialisation script to a startup file more than once. If the script is already present, we want to skip adding it. We could use the `execute` method to run this check but that wouldn't get us far. Why is that? The `execute` method, though powerful, is used for running commands where we don't care about capturing the final output. We only care that a command succeeds and the script continues or it fails and terminates a task. But, in a situation when we need to decide if a command ran successfully or failed without stopping a task, Capistrano provides the **test** method. This method is equal to `execute` with the difference that it doesn't raise an error for non zero exit code. There is even `:raise_on_non_zero_exit` option you can use to get this behaviour with execute method:

```ruby
execute ..., raise_on_non_zero_exit: false
```

To scan a file for matching content, we will use the **grep** command. It is a basic Unix utility that should already be installed on a server. If a match is found, the `grep` will finish with a zero exit code - exactly what we need. To skip the task and prevent further execution, we call **next**. If this task is part of many, the script will continue with the next task. This is a common idiom used in Capistrano plugins. It's worth noting that `break` or `exit` would immediately terminate the entire script and raise an error.

So the check to see if a startup file contains rbenv initialisation script looks like this:

```ruby
desc "Change shell startup file to setup rbenv"
task :modify_shell_file do
  on release_roles(:all) do
    next if test "grep -qs 'rbenv init' ~/.bash_profile"
  end
end
```

We need to jump through some Unix shell hoops to programmatically insert a multiline string. When executing a multiline command, Capistrano replaces every newline character with a semicolon to essentially collapse a command into a single line. To prevent this and insert rbenv script line by line into the bash startup file, we're going to use the **printf** utility. If you used Ruby's <a href="https://ruby-doc.org/core-2.7.0/Kernel.html#method-i-printf" target="_blank">printf</a> method, the Unix `printf` utility is going to be very familiar. They both use a format string followed by input arguments, the difference being shell uses whitespace to split inputs.

Let's append the rbenv initialisation script to the Bash profile file:

```ruby
desc "Change shell startup file to setup rbenv"
task :modify_shell_file do
  on release_roles(:all) do
    next if test "grep -qs 'rbenv init' ~/.bash_profile"

    execute %q{printf "%s\n  %s\n  %s\n%s" 'if [ -d ~/.rbenv ]; then' 'export PATH="$HOME/.rbenv/bin:$PATH"' 'eval "$(rbenv init -)"' 'fi' >> ~/.bash_profile}
  end
end
```

Having configured rbenv for the login and non-login shell alike, we're ready to install rbenv itself. It was quite a bit of a setup but I promise it was worth it.

## Installing Rbenv & Rbenv Build

At this point, we're not going to reinvent the wheel and instead use a <a href="https://github.com/rbenv/rbenv-installer" target="_blank">rbenv installer</a>. The rbenv installer includes a doctor script that will verify that rbenv is correctly configured. If it isn't then the installation script won't proceed any further and return non-zero exit code. This will stop the installation process in its tracks.

The benefit of using rbenv installer is that it will also install the <a href="https://github.com/rbenv/ruby-build">rbenv-build</a> tool for compiling and installing Ruby. Rbenv by default doesn't include any scripts for installing Ruby, it limits its functionality to only managing Ruby versions. However, in our case, we want to install Ruby as well.

Let's modify the `rbenv.rake` file and add a new **run_installer** task:

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

namespace :rbenv do
  desc "Map Ruby binaries to use rbenv"
  task :map_bins do
    ...
  end
  
  desc "Change shell startup file to setup rbenv"
  task :modify_shell_file do
    ...
  end

  desc "Install rbenv and rbenv-build tools" 
  task :run_installer do
  end
end
```

Similar to the task for modifying a shell startup file, we want to skip installing rbenv if it's already setup. To do so, we're going to use bash shell to check for rbenv directory existence:

```ruby
desc "Install rbenv and rbenv-build tools"
task :run_installer do
  on release_roles(:all) do
    next if test("[ -d #{rbenv_path} ]")
  end
end
```

We start by saving the rbenv installer location in a method `rbenv_installer_url`:

```ruby
def rbenv_installer_url
  "https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer"
end
```

One word of caution here. Before using a third-party script, it's best to read through and verify that a script doesn't contain any malicious code first. For extra measure, it's advisable to store such script on an internal network and only download it from there.

Then, installing rbenv is just a matter of downloading the installation script using the **wget** utility in quiet mode. Once the installation script is downloaded, we're going to pipe its content into the Bash shell to perform the installation process:

```ruby
desc "Install rbenv and rbenv-build tools"
task :run_installer do
  on release_roles(:all) do
    next if test(" [ -d #{rbenv_path} ] ")

    execute :wget, "-q", rbenv_installer_url, "-O-", "|", :bash
  end
end
```

And with that we have successfully installed rbenv utility on our server. This way we have opened the doors for automating Ruby installation.

## Summary

In this article, we wrote a few fairly complex tasks that gave us a much bigger taste of Capistrano capabilities. We modified environment variables, updated a shell startup file and installed rbenv utility. As we did all this, we explored more of the Unix shell concepts and learnt how rbenv works.

As a side benefit, this article demonstrated to us the extreme flexibility that Capistrano architecture gives us. It lets you provision the application infrastructure the way you need. You describe your requirements using task definitions and, viola! Not sure about you, but I'm impressed with Capistrano design. It embraces Ruby's dynamic nature and object model to provide you with an intuitive way of expressing deployment concepts. 

Nice. We’re another level up on our path to Capistrano proficiency. In the next article, we will continue to explore how to combine all the tasks to install a brand-new Ruby.

---

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

Photo by <a href="https://unsplash.com/@tinchofranco">Tincho Franco</a> on Unsplash