Sometimes we are faced with the challenge of upgrading old Django-based projects. The task can be daunting, as a lot has happened in Django within the last few years. Since Django 1.1.1, Django has been through 15 micro releases and 4 minor releases. The term "minor" seems deceptive as a lot of changes occur between Django 1.x and 1.[x+1]. The ideal situation would be to incrementally keep one's Django application up to date as new releases are issued; that's not always possible for a multitude of reasons. For example:
- There is a time and dollar cost to each upgrade.
- The project has "just worked" in the past.
- Older projects changed hands.
- Upgrading requires a disruption to production.
However, there are a variety of very good reasons to keep up to date:
- Security - Since Django 1.1, Django has received a plethora of security fixes, including improvements to CSRF protection, protections against clickjacking, updated password hashing logic, protections against HTTP header poisoning, protections against certain types of denial of service attacks, and more. The Django team stays very current when it comes to potential security problems and is quick to issue micro releases to address security issues.
- Support - The Django team supports several prior minor versions of Django with critical fixes. That means that any given minor version of Django will receive support in the form of fixes for about two years after its initial release (roughly based on recent timetables). Even more important is that many of the Django modules in the 3rd party ecosystem gradually start dropping support for older versions of Django. Sometimes, older required third party tools are no longer available or the API has changed significantly in newer releases. This limits modules that can be used for development on the application and can make working with older projects difficult. Old code develops code rot.
- Features - It goes without saying that each new version of Django includes a variety of new features that simplify project setup, make coding easier, and extend what is possible in Django.
The Django documentation is quite thorough and is an excellent starting point for the upgrade process. However, when upgrading Django sites, there is a lot of documentation to go through and, for production sites, a lot of planning that needs to take place. Recently, we've had to go through this upgrade process for several older sites. I haven't seen a lot of posts talking about this topic, so I thought I'd share one way to do it. This is one of many approaches; I'd be curious to hear about other people's war stories.
First, we took some time to plan out the upgrade. In one case here at Imagescape, it made sense to upgrade the infrastructure along with the Python code. Since we were jumping several Python versions, Django versions, and Postgres versions, it made sense to spin up a completely new environment on which to perform the upgrade. The plan was to apply the upgrades to the new environment, thoroughly test the upgraded environment, run the current production environment and new environments in parallel until the new environment is stable, sync the data and media, and switch over to the new environment.
Second, we went through all of the Django Release Notes since the Django version from which we were upgrading. Based on the release notes, we made a single "impacts list" of potential changes that we'd need to make and focused our attention on the "Backward Incompatible Changes" sections of the various Release Notes documents. This helped us anticipate potential upgrade problems, plan solutions for them, and estimate the work involved. We had made sure to include some time in the estimate to account for potential unknown upgrade issues.
Third, we took a first pass at the Django project that we were upgrading and identified points in the application that would need updates based on the compiled list. Additionally, we reviewed the third party dependencies,using 'pip freeze' and INSTALLED_APPS, to get an idea of what python modules were being used and what their versions were. We took a cursory glance at each module's project website or source code repository to see if there were any clear changes in the module's api and to get a rough idea of how much effort would be involved in upgrading that module. Identifying any upstream and downstream system/data dependencies was also valuable; this would inform the types of impacts that we'd expect. It was also important to note which version of Python on which the application was currently running; a change in Python version could involve a lot more work. We've found that Python modules that are compiled into C code will often need to be recompiled when upgrading the Python version. Furthermore, it was important to take a quick look at any in-house Python code that would be impacted by the upgrade. From this analysis, we were able to make a good hypothesis about estimation of effort.
Fourth, after the research and estimation process was complete, we found it very helpful to actually try the upgrade on the new environment and see what problems we would encounter. The new Django version required a new Python version, but since we had configured a new infrastructure environment, an updated Python came with it. After upgrading the Django version, and the versions of select third party modules, we began to progress through our upgrade impacts list and apply the changes. After making all of the identified changes, and after committing them to a new Git upgrade branch, we inevitably encountered other upgrade issues. These issues were mostly site-specific errors and needed to be dealt with on a case-by-case basis.
Finally, once the basic site was up and running, the next step was to test the site functionality extensively. This is obviously approached differently from site-to-site, but should cover as much of the site functionality as possible. In our case, we performed extensive click-throughs of the front end interfaces, we ran relevant management commands and tested scheduled jobs, we tested search index updates, tested integration with external web services, ran unit tests, rehearsed specific deployment steps to bring data and static media up to date, and much more. At each point, as issues were discovered, we iterated around to fix them.
When all of the bugs were worked out and the new environment was in a stable state, it was time to prepare for and schedule the final switchover to the new environment. In this particular case, we decided upon a deployment window early in the morning when site usage would be low. Just like any other feature deployment, we made a thorough list of deployment steps, specifying the actual commands that we would be run. This list included commands to curtain the site, backup the database, sync the data and media to the new server, run any database migrations, execute any other one-time scripts, and more. On the morning of deployment, we executed this plan which helped ensure a smooth deployment.
General suggestions to make upgrades easier:
- Document the site set up extensively and keep the documentation up to date.
- Use a Python virtualenv for every Django site.
- Make the code easy to develop and deploy, automate what you can, and follow DRY where possible.
- Upgrade Django and third party modules incrementally if possible.
- Maintain an up-to-date pip requirements file, including required version numbers.
- As much as possible, use third party code that is well-maintained.
- Keep up to date on release notes announcements on the Django Blog, read the discussion on the django-developers mailing list, and generally keep active within the Django community.
I've shared the "impacts list" that we compiled when progressing through the Django upgrade and have posted it as a Gist. This list is compiled from the Django release notes, the Django source code, and various other blog posts and articles. I've found this list helpful when completing this process and continue to add to it; however, it's far from a complete list. Please reply with your feedback or fork the Gist and I'd be glad to make updates.