software engineering Dec 20, 2018

Ruby, RVM, and Bundler Demystified

Developing Ruby applications can be simple, but when you begin to work on multiple projects that require multiple language versions / different dependencies this quickly becomes very complicated. As a Ruby engineer, developing just one application at a time, or working entirely alone on a project, is unrealistic. Ensuring applications are developed in a consistent environment across machines and developers becomes imperative for success. Additionally, the environment consistency, language version, and dependency management patterns described here apply to other languages as well, just using different tools.

Whether you are working locally, in the cloud, or in a container, the moving parts of a Ruby development environment need to be understood before consistency can be achieved. This post walks through some concepts that make consistent, flexible Ruby development possible.

On the Right Track with Ruby, RVM, and Bundler

To better understand the Ruby development environment, imagine a Ruby application as a train locomotive. Like any train locomotive, it needs tracks on which to travel (Ruby runtime), some form of energy bits (Ruby gems, application dependencies), and, of course, an engineer (You)!

Everything is pretty simple to start. The train can run on pre-laid tracks (default system Ruby runtime) and those tracks can supply the energy bits to the train from something like a third rail (by installing any application dependencies as they are encountered, e.g. `sudo gem install foo`).

Now, let’s add an additional train to the system. That seems easy, we can run it down the same tracks as our first train, thereby using the same energy source too. But what happens when this new train needs to run on newer or older tracks (a different Ruby version) than the first train?

This can happen if you are trying to retrofit an older train to fit new tracks, or if the latest tracks work better or have better features than the old tracks. Sometimes a new energy bit may even require the new tracks! And more, the energy bits are bound to those various tracks as well.

Enter RVM, the ruby version manager. This nifty command-line utility allows the engineer to use more than one track by trivially switching the tracks as they engineer each train (`rvm use 2.4.1`). Now the trains can safely travel their appropriate tracks. But what about the energy source? What happens if this new train needs different energy bits — or worse, similar but not-exactly-the-same bits (different application dependency versions). RVM provides a concept of a ‘gemset’, which allows the engineer to switch or share the third rail as well. RVM makes it easy to remove tracks and can keep all of the tracks safe from collision with one another, or with the pre-installed system tracks.

So this is great! The engineer (you) can now drive two or more trains (applications) on different tracks (Ruby versions) using the energy source provided by either each tracks’ third rail (default gemset) or by an alternate third rail (gemset). All without trains crashing into each other or polluting each others’ energy sources (e.g. `rvm use 2.4.1@newtracks`).

However, the energy bits have been added one at a time via ‘gem install’ to each third-rail as each train has needed it. Manually installing dependencies as they are encountered makes experimenting with new energy bits, testing different tracks, or enlisting additional engineers tedious and difficult. Locomotives can also cause damage to shared tracks or third-rails inadvertently, leaving other locomotives inoperable! So, how can we make the trains run reliably in different places? How do we ensure that the energy needs are easily communicated? If only there were a way to declare what energy bits each train requires…

Enter Bundler, energy storage for the train’s locomotive. Bundler installs and keeps track of the exact application dependencies and versions that are needed for an application. With Bundler, the train itself declares its specific energy requirements using a manifest file (Gemfile). An engineer can use the manifest file to ensure that the train runs smoothly on any tracks and that the energy bits are delivered from the train itself, rather than from the tracks. This energy car also ensures that only the energy bits of this train are available for use by this train only.

Now we’ve reached the station safely. Trains run on proper tracks using isolated and properly declared energy bits making it simpler to upgrade, deprecate, innovate, and communicate with other engineers! Once finished with the tracks, it is trivial to remove them entirely with a single command. There are some alternatives and additional advantages to the tools presented here, find these details in the appendices to follow.

Hopefully, the moving parts of the Ruby environment are now understood, allowing application development in a flexible and consistent manner. Apply the environment consistency, language version, and dependency management patterns described here to development in any new languages as well!

Appendix A: Ruby Management Tool Alternatives

There are alternatives to RVM (e.g. rbenv and chruby) and the choice is up to the individual developer or team. There are advantages and disadvantages to each, but they all seek to solve the same fundamental challenge.

Appendix B: Additional Ruby Management Tool Advantages

Ruby managers also provide a few other advantages:

  • Ruby runtimes and gems are installed in “user space”, removing the need for `sudo` to install gems.
  • Leaves system Ruby in a pristine state (when used with discipline).
  • Some can be configured to be largely invisible and automatic (Appendix E).

Appendix C: Additional Bundler Advantages

Bundler also provides a few additional windfalls.

  • Better runtime performance. Bundler only exposes specified gems to your application, regardless of the number of gems installed allowing for faster file system scanning and gem loading.
  • Gemfile.lock file ensures that what you have tested is what is communicated to other systems that use `bundle install`. This requires an intentional `bundle update` and commit to find and use newer versions of required gems.
  • You can use Bundler to deliver energy within your locomotive or use it to enhance the management of the externally provided energy source as well.

Appendix D: More on RVM & Bundler

Appendix E: Configuring RVM for Invisibility

  • Add rvm directives to your project — using the Gemfile comment approach keeps everything together.
  • Create an .rvmrc configuration file in your user home directory:
rvm_install_on_use_flag=1 # Install a missing Ruby version when accessed rvm_project_rvmrc_default=1 # Use files in the project to determine ruby version and gemset
rvm_autoinstall_bundler_flag=1 # Install bundler automatically in each ruby/gemset
rvm_gemset_create_on_use_flag=1 # Create missing gemsets when accessed
Jacob Aleksynas
Lead Software Engineer, Reliability and Automation, Capital One

DISCLOSURE STATEMENT: These opinions are those of the author. Unless noted otherwise in this post, Capital One is not affiliated with, nor is it endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are the ownership of their respective owners. This article is © 2018 Capital One.