You use a PaaS because you want all the underlying infrastructure and configuration of your application to be hidden from you. However, there are times when you are forced to look deeper into the stack. In this article I want to share how simple it is to run a patched version of Ruby on Heroku.
Context#
It all started while trying to upgrade Ruby in an application. Unfortunately, every newer version I tried made the application break. After some searching around, I came across a bug report from 3 years ago in Ruby upstream.
The issue was actually not in Ruby but in Onigmo, the regular expressions library that Ruby uses under the hood. All versions since 2.4 where affected i.e. all supported versions including 2.5.8, 2.6.6 and 2.7.1 at the moment of writing. Lucky for me, Onigmo had been patched upstream but the patch will only land in Ruby 2.7 later this year.
This meant that, I was going to have to patch Ruby myself. For local development this is not a big deal, but I wasn’t sure if it was possible to do on Heroku. I remembered from CloudFoundry and Jenkins-X that the part of the platform taking care of the build and installation of the language were the buildpacks, so I decided to investigate about buildpacks on Heroku.
Heroku’s Ruby Buildpack#
Heroku’s Ruby buildpack is
used to run your application whenever there’s a Gemfile
and Gemfile.lock
file. From parsing these, it figures out which version of Ruby it’s meant to
use.
Once it knows which version of Ruby to install, it runs
bin/support/download_ruby
, to download a pre built package and extracts it to
be available for execution to your application. As a quick hack, I decided to
modify this file to do what I did in my development environment to patch Ruby.
First download the Ruby source code from upstream instead of the pre built version by Heroku.
curl --fail --silent --location -o /tmp/ruby-2.6.6.tar.gz https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.6.tar.gz tar xzf /tmp/ruby-2.6.6.tar.gz -C /tmp/src cd /tmp/src/ruby-2.6.6
Then apply a patch from a file I placed under
bin/support/
(probably not the best place but OK while I was figuring things out).patch < "$BIN_DIR/support/onigmo-fix.diff"
And finally build and install Ruby
autoconf ./configure --disable-install-doc --prefix "$RUBY_BOOTSTRAP_DIR" --enable-load-relative --enable-shared make make install
You can find an unpolished but working version of what I did here
Using the Buildpack in Your Application#
Now all that is left is to tell your application to use your custom buildpack instead of Heroku’s supported one. You can do this in the command line by running
heroku buildpacks:set https://github.com/some/buildpack.git -a myapp
Or by adding a file called app.json
at the root directory of your application
sources (not in the buildpack sources). I ended up using this form since
I prefer to have as much of the platform configuration in code.
{
"environments": {
"staging": {
"addons": ["heroku-postgresql:hobby-dev"],
"buildpacks": [
{
"url": "https://github.com/some/buildpack.git"
}
]
}
}
}
Now every time a deployment is made to this environment, the Ruby application will download the Ruby sources, patch, build and install them.
This of course is not very optimal since you’ll be wasting a lot of time building Ruby. Instead you should do something similar to what Heroku is doing by pre building the patched version of Ruby, and downloading it from an S3 bucket. {: .notice–warning}
Conclusion#
Using a patched version of Ruby comes with a heavy price tag, the maintenance. You should still apply updates until that patch is fixed upstream (at least security updates). And you also need to use the patched version in all your environments e.g. production, staging, et al. including your CI. Whether all this extra work is worth it, is something you’ll need to analyze. In the cases when the benefits outweigh the costs, it’s great to know that you don’t have to give up all the benefits of a platform like Heroku to run your own version of Ruby.
Reply by Email