Upgrading Ruby on Rails applications
Every year, we spend some of our time upgrading Ruby on Rails applications. Some of them have been created by us (and thus we know everything about them) and some of them haven’t. In any case, we have the same approach despite their difference (dependencies, versions, etc.).
Why upgrading Ruby on Rails apps?
Rails is a framework that moves quite fast. Following the Semantic Versioning, the Rails core team releases a major version every 3 years on average, a minor version update every year on average, and about a dozen bugfixes versions a year. With every major release of the framework, the oldest major release of the framework reaches its EOL (End Of Life). For example, when Rails 7 was released, Rails 5 reached its EOL. This means it won’t receive any updates newer versions will receive (in this case Rails 6 and Rails 7). So having an old Rails version will mean your application won’t receive patches for security and severe security issues (as per the maintenance policy), and might be vulnerable to attackers. Also, not upgrading Rails often means that it will be eventually harder to upgrade Rails in the future.
At the same time, Ruby on Rails versions support specific Ruby versions. For instance, if you want to upgrade your Rails 5 application to Rails 6 and you’re currently running on Ruby 2.3, you need to upgrade to at least Ruby 2.5 (as per this table). But it’s not that simple. Ruby has its release cycle and their different versions have their own EOL. In this case, Ruby 2.5 has already reached its EOL (See ruby branches), so it’s recommended to upgrade to a newer version.
Assessment
The first step of the upgrade is to gather information. Digging into the application to answer the following questions:
What’s the current Ruby version?
What’s the current Ruby on Rails version?
What’s the desired Ruby on Rails version? That is, what version do we want to upgrade to? In any case, is recommended to upgrade the Rails version in baby steps, ie in small changes. From Rails 5.0.x to Rails 5.1.x, then to Rails 6.0.x, then 6.1.x, etc. and not from Rails 5.0.x to 6.1.x directly.
Where’s the app running? This question is really important as different vendors might support different Ruby versions. Or it could even be bare metal, in which case we might have more control over the software we’re installing (but we might have other problems).
Are we using a database? If so, which one? Which version? Sometimes an upgrade to the DB version is also needed, either for security and/or new features needed for newer versions of Rails or a dependency. For example, Rails 6 dropped support for PostgreSQL < 9.3 in 2018. This DB upgrade might also be coupled to a data migration.
Does the app have tests? Having tests is a great plus as it will allow us to quickly determine if the upgrade didn’t break anything. However, that doesn’t mean it won’t break.
The answers to these questions will help us determine what are the latest versions we can upgrade the interpreter, the framework, and the dependencies. It’s really important to note that the infrastructure where the app is running will determine the latest versions we can upgrade to. ie. It’s not the same as deploying a container, a Heroku app, an AWS Elastic Beanstalk app, or any other vendor-specific setup.
Upgrade
After gathering this information, we will try to upgrade the interpreter, the framework, and the dependencies to their latest versions possible, without compromising the functionality of the application (again, tests are a great plus). We will do so by upgrading one Rails version at a time. There’s a little process for that:
Upgrade the Rails version.
Upgrade the Ruby version to the maximum possible to the Rails version we’re upgrading.
Upgrade dependencies (if needed and possible). This is one of the most painful steps and one that can make things hard. Some projects use dependencies that are no longer maintained or maintained up to a certain Ruby/Rails version and past that version is not possible to upgrade. We might need to come up with alternatives to these dependencies, which in most cases will require extra work.
Run tests (if any) and check everything is working as expected. If the application has no tests, or even if it has, it’s recommended to at least check the critical paths are still working as expected, manually. Sometimes, there are small changes in methods (from Rails or a dependency) that break or give false positives. It’s important to manually check on these critical paths so nothing important is broken.
Deploy the upgraded application to a new environment.
These steps need to be repeated as many times as Rails versions we need to upgrade.
While most upgrades are the same for a local environment, they differ in a key part of the architecture: the infrastructure. It’s not the same as deploying a rails app to bare metal having to provision everything (operating system users, web server, application server, database server, interpreter, etc) and scale manually, and deploying to a platform such as AWS Elastic Beanstalk, Heroku or other vendors where provisioning and scaling is handled by the platform.
Upgrading apps running on bare metal
Upgrading apps running on bare metal usually takes a bit longer as building a new environment might involve manual provisioning. It’s extremely difficult to even try and describe how to do it as applications running on bare metal have as many setups as one can imagine, so it’s up to the readers to figure out how to upgrade the infrastructure of their application.
Upgrading AWS Elastic Beanstalk applications
AWS Elastic Beanstalk comes with a constraint: AWS Elastic Beanstalk for Ruby supports two Ruby versions at the moment: Ruby 2.7 and Ruby 3.0. So these are the maximum ruby versions you can aspire to. It’s recommended to upgrade to the latest one.
Normally, if you use Elastic Beanstalk for your application, you would upgrade your app often as it only supports two versions of the interpreter. Such upgrades consist mostly of upgrading the platform the application is running on. As part of the upgrade, we will need a different environment. Ideally, a staging environment using the same database as the current staging database.
Upgrading Heroku applications
Upgrading a Heroku application is probably the easiest of these examples as it doesn’t require any action other than upgrading the code. Heroku is smart and it knows exactly what version of Ruby you are upgrading to (by reading the .ruby-version
file).
Outcome
The outcome of an upgrade is normally a git branch containing the upgrade, and a new environment containing such a branch, ready to be merged.
Stay tuned for the next posts in the series where we’ll talk about processes and actions to help make upgrades smaller, less painful, and with reduced risks.