Red Green Repeat Adventures of a Spec Driven Junkie

Vagrant Provisioning with ruby and node commands

I love sharing my adventures in tests and coding as a way to solidify my learning, but also in the off chance it can help you as well.

This time, I’m going through how to set up Vagrant to provision processes related to rvm & ruby and nvm & node. The set up allows the provisioner to even run bundle install or npm install as a provisioning step.

If you want to step up your vagrant (shell) provisioning to another level with ruby and node, this is the article for you!

This article will take you about six minutes or less to read as there are code and vagrant output.

William Tom Warrener - The Englishman source and more details

Introduction

After an embarrassing interview with a candidate where I could not run our awesome test suite in front of them, I wanted to get our project running on a automated build system.

For me, this means setting up our system in an automated manner with Vagrant. I just want to have a consistent environment to work from.

It’s easy to install the system level dependencies, like postgresql or even ruby.

Next Level Automation

Even with the system level dependencies installed, I want to get the project’s dependencies installed. Installing nokogiri with native extensions can feel like forever. I want this process to part of during Vagrant’s provisioning process.

This is tricky because Vagrant’s provisioning system is basically scripting out the root user, so installing Ruby management tools like rvm is not useful, the main user in a Vagrant system is vagrant.

To get around this, use the privileged: false option in the provisioning process. This would be scripting out the vagrant user instead of root.

rvm & ruby

Even with this option, installing rvm is easy, but using rvm in a simple script

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.provider 'virtualbox' do |v|
	v.memory = 512
	v.cpus = 2
  end

  # install rvm
  config.vm.provision :shell, privileged: false, inline: <<-SHELL
	gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
	curl -sSL https://get.rvm.io | bash -s stable
	rvm install 'ruby-2.6.0'
  SHELL
end

$ vagrant provision still errors with:

	default: Primary key fingerprint: 7D2B AF1C F37B 13E2 069D  6956 105B D0E7 3949 9BDB
	default: GPG verified '/home/vagrant/.rvm/archives/rvm-1.29.9.tgz'
	default: Installing RVM to /home/vagrant/.rvm/
	default:     Adding rvm PATH line to /home/vagrant/.profile /home/vagrant/.mkshrc /home/vagrant/.bashrc /home/vagrant/.zshrc.
	default:     Adding rvm loading line to /home/vagrant/.profile /home/vagrant/.bash_profile /home/vagrant/.zlogin.
	default: Installation of RVM in /home/vagrant/.rvm/ is almost complete:
	default:
	default:   * To start using RVM you need to run `source /home/vagrant/.rvm/scripts/rvm`
	default:     in all your open shell windows, in rare cases you need to reopen all shell windows.
	default: <warn>Thanks for installing RVM 🙏</warn>
	default: Please consider donating to our open collective to help us maintain RVM.
	default:
	default: 👉  Donate: <code>https://opencollective.com/rvm/donate</code>
	default: /tmp/vagrant-shell: line 4: rvm: command not found

WHY?

The worst part of is: I can manually log in to the machine, run the same command and it will just work. Why?!

The key: the vagrant provisioner is the vagrant user, but it does not load up ANY profile information, that include bash - why should it?!

Before doing any commands to load up ruby, just execute:

source /home/vagrant/.profile

Making this change in the above script, the whole script becomes:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.provider 'virtualbox' do |v|
	v.memory = 512
	v.cpus = 2
  end

  # install rvm AND ruby
  config.vm.provision :shell, privileged: false, inline: <<-SHELL
	gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
	curl -sSL https://get.rvm.io | bash -s stable
	source /home/vagrant/.profile
	rvm install 'ruby-2.6.0'
  SHELL
end

This will give results:

	default: gpg: Signature made Wed Jul 10 08:31:02 2019 UTC
	default: gpg:                using RSA key 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
	...
	default:
	default: 👉  Donate: <code>https://opencollective.com/rvm/donate</code>
	default: Searching for binary rubies, this might take some time.
	default: Found remote file https://rvm_io.global.ssl.fastly.net/binaries/ubuntu/18.04/x86_64/ruby-2.6.0.tar.bz2
	default: Checking requirements for ubuntu.
	default: Installing requirements for ubuntu.
	default: Updating system

Now in the provisioning script, the provisioner can run ruby commands like bundle, rake, and even rspec!

	...
	default: Running: /var/folders/1g/_2d999fd23s3l50kjmvq8_yc0000gn/T/vagrant-shell20190822-58203-hg86p4.sh
	default: Already installed ruby-2.6.0.
	default: Using rake 12.3.2
	default: Using concurrent-ruby 1.1.5
	default: Using i18n 1.6.0
	...

Now there’s nothing stopping from having a fully built developer machine with Vagrant!

nvm & node

The issue above with rvm also happens with nvm, the node version manager. When nvm is not configured properly in the provisioner, the following error appears:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.provider 'virtualbox' do |v|
	v.memory = 512
	v.cpus = 2
  end

  # install nvm
  config.vm.provision :shell, privileged: false, inline: <<-SHELL
	curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
	nvm install node
  SHELL
end

Like rvm, trying to have nvm install node in the provisioner returns an error:

	default: => Downloading nvm from git to '/home/vagrant/.nvm'
=>  default:
	default: Cloning into '/home/vagrant/.nvm'...
	default: => Compressing and cleaning up git repository
	default: => Appending nvm source string to /home/vagrant/.bashrc
	default: => Appending bash_completion source string to /home/vagrant/.bashrc
	default: => Close and reopen your terminal to start using nvm or run the following to use it now:
	default:
	default: export NVM_DIR="$HOME/.nvm"
	default: [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
	default: [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
	default: /tmp/vagrant-shell: line 2: nvm: command not found

The solution is similar to rvm, load nvm’s configuration into the provision script, the whole script is:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.provider 'virtualbox' do |v|
	v.memory = 512
	v.cpus = 2
  end

  # install nvm AND node
  config.vm.provision :shell, privileged: false, inline: <<-SHELL
	curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
	export NVM_DIR="$HOME/.nvm"
	[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
	[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
	nvm install node
  SHELL
end

Now the output is:

   default: => bash_completion source string already in /home/vagrant/.bashrc
	default: => Close and reopen your terminal to start using nvm or run the following to use it now:
	default:
	default: export NVM_DIR="$HOME/.nvm"
	default: [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
	default: [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
	default: Downloading and installing node v12.9.0...
	default: Downloading https://nodejs.org/dist/v12.9.0/node-v12.9.0-linux-x64.tar.xz...
	...
	default: Now using node v12.9.0 (npm v6.10.2)
	default: Creating default alias: default -> node (-> v12.9.0 *)

Conclusion

In my quest to fully automate vagrant provisioning so a developer box can be a single step, I learned:

  • privileged: false runs scripts as the vagrant user.
  • The privileged: false mode does not load any user settings, like profiles or paths.
  • To have the vagrant provisioner run commands that managed services like rvm or nvm uses, just load the bash profile OR follow its commands.

With this, I am a leap closer to having a fully automated build for developer boxes.

I can’t wait for the next team member to join to show them how easy it is to set up our development environment!

I’m always looking for good people to join my team. Even if you’re not looking for a position now, contact me to stay in touch, because you never know!