Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Heroku Release Phase script for managing Rails DB migrations, and playing nice with Review Apps and postdeploy scripts

Heroku Release Phase + Review Apps + Rails

This is a simplified, but fairly thorough, set of scripts and configuration to enable Heroku Release Phase for Rails apps. Further, this particular set up plays nicely with Heroku Review Apps in that the release phase script will:

  1. Fail, loudly, if the DB does not yet exist.
  2. Load the DB schema if the current schema version (as determined by bin/rails db:version) is 0.
  3. Run DB migrations otherwise.

For a "normal" app that usually means it will run the DB migrations. For a Review App, on the first deploy the release phase will bin/rails db:schema:load. And then the postdeploy script will seed data. During subsequent deploys to the Review App, the release phase will bin/rails db:migrate.

{
"name": "your-app-name",
"description": "Configuration for per-Pull-Request Reviews Apps on Heroku.",
"scripts": {
"postdeploy": "LOG_LEVEL=INFO bin/rake review_app:seed"
},
"stack": "heroku-16",
"formation": [
{ "process": "web", "quantity": 1 },
{ "process": "worker", "quantity": 1 }
],
"addons": [
"heroku-postgresql:hobby-dev"
],
"buildpacks": [
{
"url": "heroku/ruby"
}
],
"env": {
"Lots of other Env Vars...": "as you need",
"HEROKU_APP_NAME": {
"required": true
},
"HEROKU_PARENT_APP_NAME": {
"required": true
}
}
}
#!/usr/bin/env bash
#
# Usage: bin/heroku_deploy
. ./shell_colors
set -euo pipefail
schema_version=$(bin/rails db:version | { grep "^Current version: [0-9]\\+$" || true; } | tr -s ' ' | cut -d ' ' -f3)
if [ -z "$schema_version" ]; then
printf "💀${RED} [Release Phase]: Database schema version could not be determined. Does the database exist?${NO_COLOR}\n"
exit 1
fi
if [ "$schema_version" -eq "0" ]; then
printf "\n⏳${YELLOW} [Release Phase]: Loading the database schema.${NO_COLOR}\n"
bin/rails db:schema:load
else
printf "\n⏳${YELLOW} [Release Phase]: Running database migrations.${NO_COLOR}\n"
bin/rails db:migrate
fi
printf "\n🎉${GREEN} [Release Phase]: Database is up to date.${NO_COLOR}\n"
#!/usr/bin/env bash
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NO_COLOR='\033[0m'
CLEAR_LINE='\r\033[K'
web: bundle exec puma -C ./config/puma.rb
worker: DB_POOL_SIZE=${WORKER_CONCURRENCY:-20} bundle exec sidekiq -c ${WORKER_CONCURRENCY:-20} -t ${WORKER_TIMEOUT:-25} -q default,1 -q mailers,1
# === Heroku Release Phase, ignored by `heroku local`. See: https://devcenter.heroku.com/articles/release-phase
release: bin/heroku_release
# frozen_string_literal: true
namespace :review_app do
desc 'Ensure environment is one we shish to spread seed in'
task :ensure_review_app do
abort 'This is not a Heroku Review App' unless review_app?
end
desc 'Seeds a review app with a subset of realistic-looking data'
task :seed, [] => %w[
ensure_review_app
environment
db:seed
seed:administrator
seed:widgets
] do
Rails.logger.tagged('Seed App') { |l| l.info("Finished seeding new Review App: #{ENV['HEROKU_APP_NAME']}") }
end
def review_app?
!!ENV['HEROKU_PARENT_APP_NAME']
end
end
@lastobelus

This comment has been minimized.

Copy link

lastobelus commented Oct 9, 2018

what happens with the following sequence:

  1. I make a branch, with a migration
  2. I do a hotfix on master with a migration, and deploy it
  3. I deploy my branch

I think the db version key needs to be a hash of the output of rails db:migrate:status

@stevenharman

This comment has been minimized.

Copy link
Owner Author

stevenharman commented Mar 13, 2019

@lastobelus I'm not sure I follow. We're only keying on the db version to determine if it's 0 or not. In the case it's 0, either:

a. this is the initial deploy, and no migrations have yet been run
b. this is a fresh DB, like in the case of a Review App

Either way, we're OK to run db:schema:load. In all other cases, default to whatever bin/rails db:migrate would do. Which presumably handles the case you've outlined, no?

@pebneter

This comment has been minimized.

Copy link

pebneter commented May 10, 2019

Great job - Thanks for sharing.
Helped me a lot in understanding the release phase approach and was a good start to build my own.

@joshm1204

This comment has been minimized.

Copy link

joshm1204 commented Nov 20, 2019

What does your environment rake task do for you? Any way to postcode?

@stevenharman

This comment has been minimized.

Copy link
Owner Author

stevenharman commented Nov 28, 2019

@joshm1204 That's the built-in Rails environment task, which basically boots the Rails app to make sure expected constants, DB connections, etc... are available.

@softwaregravy

This comment has been minimized.

Copy link

softwaregravy commented Feb 2, 2020

thank you

@pebneter

This comment has been minimized.

Copy link

pebneter commented Mar 16, 2020

I have one issue that we run in quite frequently: We have two separated apps that share the same code and database. Now when we promote a new build simultaneously to both apps, we get an error sometimes because heroku runs the database migration at the same time, resulting the app deploy to fail on for the app that comes in a split second later (because the migrations are locked).
Any idea how to solve this? It might be possible to specify the app name to run the migration on, but that would be a bit static.

@stevenharman

This comment has been minimized.

Copy link
Owner Author

stevenharman commented Mar 16, 2020

@pebneter My initial reaction is to dig into why you have two different apps, running the same code, sharing a database. Could you provide any context there?

Ignoring that for now, ultimately the setup you're describing is... unusual. And without more context as to the why, I can only offer generic suggestions, which might be Bad Ideas™, depending on the missing context. e.g., pick one App to be the "leader" which manages the DB, while the other is a "follower," totally dependent on the DB maintenance of the former. Something like an ENV Var to determine this is probably doable, with your Release Phase script checking that an NOOP-ing in the follower case.

Or you could look into something truly terrible like a global, distributed locking mechanism, complete with all of the sharp edges and broken corners that come with them. e.g., PostgreSQL Advisory Locks, or a Redis lock, etc...

I'm quite curious to hear more about the why, so please do share if you can!

@pebneter

This comment has been minimized.

Copy link

pebneter commented Mar 19, 2020

So about the why: We are running a home automation platform (apilio.com), and we face the challenge of having to expose parts of the functionality to the public, while wanting to keep other parts restricted to certain sources to protect them from attacks.
Due to some restrictions of Heroku, we came up with an architecture that involves having two instances of the same app running as separate applications (so it's not one application scaled up, but two separate applications).

I think of the possible solutions (probably all with caution):

  • Pick a "leader"
  • Staged deployments: Always deploy to one first and to the other afterwards
  • Make sure that the deployment script checks if a migration is already running and then wait for it
  • Abandon the idea to automatically execute migration during deployments
@stevenharman

This comment has been minimized.

Copy link
Owner Author

stevenharman commented Mar 25, 2020

while wanting to keep other parts restricted to certain sources to protect them from attacks

Meaning, some private networking? Heroku Private Spaces offer that kind of thing. Though it might be more complexity than is worth it for you right now depending on your needs, teams size, and run rate constraints.

But if you need to run two discrete instances, my gut says, in your case, the "pick a leader" strategy is probably the safest route. I'd go with something like an ENV var which simply says "yes, you are the leader" so that only that app actually does migrations during release phase. Then all other apps, by default, would no-op. Then you could add that magical ENV var to your Review App config so they'd still auto-migrate.

@pebneter

This comment has been minimized.

Copy link

pebneter commented Mar 26, 2020

Yes, that was also my favorite strategy so far!
I guess in the long term, we will have to separate code and database changes completely, since it would also be nice to use the true no-downtime-deployment option of Heroku (preboot)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.