Automating Yii Migrations on Heroku

May 2, 2013

Yii migrations are quite useful, allowing you to keep your database version controlled along with your code. It's all pretty easy to work with locally: you write your migration, migrate up, and then write the code to take advantage of that new database structure.

When it comes time to push your code to a remote server, however, you run into a catch 22. Your migration is stored in the code, and you've already written code against the new structure of the database. So if you push the code and then use some sort of web GUI to migrate, then there could be a time between when the code is pushed and the migration is run where the database and code are not on the same version. In the same way, if you run the migration before you push the code, your code and database are out of sync again.

The ideal solution to this problem is to have the migration execute and the code go live at the exact same time.

How Heroku Works

As we move toward our solution, it's important to understand how Heroku works. When you push your code to Heroku, it spins up a new server, waits for it to come online, and then routes all requests over to that server, taking your old one offline. This ensures that you have no downtime while your new code is going up. It's actually a pretty slick process.

The Heroku server uses a script called boot.sh to set variables and start the Apache server. This is what we're going to hook into to execute our migrations as the server starts up, before traffic is directed to the new server.

Procfile

The Heroku Procfile is basically the set of instructions that Heroku uses to know what processes it needs to run as it's booting up. The standard (web only) procfile for PHP looks like this

web:    sh boot.sh

That's it. All it does is define the web process as boot.sh, which is the boot script supplied by Heroku. At the time of writing, that file looks like this:

for var in `env|cut -f1 -d=`; do
  echo "PassEnv $var" >> /app/apache/conf/httpd.conf;
done
touch /app/apache/logs/error_log
touch /app/apache/logs/access_log
tail -F /app/apache/logs/error_log &
tail -F /app/apache/logs/access_log &
export LD_LIBRARY_PATH=/app/php/ext
export PHP_INI_SCAN_DIR=/app/www
echo "Launching apache"
exec /app/apache/bin/httpd -DNO_DETACH

It passes in some config variables, sets up some logs, and launches Apache. We're going to modify this file to also run our migrations.

Modified boot.sh

In Yii, the migration command is

./yiic migrate

That command comes back with the "do you want to migrate?" question, to which you have to respond "Y/N". For the remote server, we need to run this automatically without the question. There is a flag we can use to turn this off; by setting "interactive" to "0", it will run automatically. (http://stackoverflow.com/questions/7811839/yii-automatic-db-migrations)

Now our command looks like this:

./yiic migrate --interactive=0

It'll happily migrate without your intervention. One step closer. Now we need to put this into our modified boot.sh. Copy the standard boot.sh and name it something clever, like web-boot.sh. Create a file called "Procfile" in the root of your app, and change the name of the boot script to web-boot.sh, like so:

web:    sh web-boot.sh

The last thing we have to do is modify our new web-boot.sh to include the Yii migrate command. This is what you should end up with:

for var in `env|cut -f1 -d=`; do
  echo "PassEnv $var" >> /app/apache/conf/httpd.conf;
done
touch /app/apache/logs/error_log
touch /app/apache/logs/access_log
tail -F /app/apache/logs/error_log &
tail -F /app/apache/logs/access_log &
export LD_LIBRARY_PATH=/app/php/ext
export PHP_INI_SCAN_DIR=/app/www
echo "Launching apache"
bin/php www/protected/yiic.php migrate --interactive=0
exec /app/apache/bin/httpd -DNO_DETACH

Notice the new line

bin/php www/protected/yiic.php migrate --interactive=0

That's the line that uses PHP to run the migrate action in the yiic.php file without interactions. Now, every time you push code to the server, your migrations will automatically run. Programmer's paradise.