ITエンジニアによるITエンジニアのためのブログ

HerokuでのPostgresアップグレード

先日、HerokuでPostgresデータベースのバージョンアップを行ったので、その手順を備忘も兼ねて共有します。

私が元々使っていたPostgresバージョンは12だったので、バージョン12から13へのアップグレードという形で記載していますが、ここで紹介している手順は他のバージョンでも同様なはずです。

なお、私が使っているデータベースのプランと構成は以下の通りです。プランが低価格プランだったり、フォロワーがない場合は手順が変わる可能性がありますのでご了承ください。

・Heroku Postgresの使用プランはStandard 0です。

・プライマリーDBが1つ、フォロワーDBが1つの構成です。

また、ここに記載した手順を本番環境に適用する前に、ステージング環境で一度試してみることをおすすめします。手順に慣れておく必要がありますし、予期せぬエラーが起こる可能性もあるからです。

ローカル環境での事前確認

Heroku環境でデータベースのアップグレードを行なう前に、ローカル環境でアップグレードしてみて、アプリケーションが正常に動作するかを確認します。

私の場合はDocker Composeを使ってPostgresを使っていたので、以下のようにdokcer-compose.ymlファイルでバージョンを変更しました。

version: '2.1'
services:
  postgres:
    image: postgres:13
    ports:
      - "5432:5432"

なお、Postgresイメージのバージョンを上げると今まで使っていたコンテナが動作しなくなりますので、以下のコマンドでコンテナを再構築する必要があります。

docker-compose up --force-recreate

ローカル環境で無事にアプリケーションが動作するか、手動・自動テスト両方で確認できたらOKです。

事前準備:herokuスケジューラー

herokuスケジューラーを使っている場合、アップグレード作業に入る前に各スケジューラーの実行時間帯をチェックします。Postgresアップグレードの最中にスケジューラーが実行されると、古いデータベース上のレコードが変化し、新しいデータベースに反映されない可能性があるからです。もしアップグレード作業時間と被るスケジューラーがあったら、削除するか時間をずらしておきます。

メンテナンスモードの有効化

アップグレード中にアプリケーションにアクセスされてデータが変わってしまうことのないように、まずはメンテナンスモードにしてアクセス出来ないようにします。

heroku maintenance:on -a your_app_name

実際にアプリケーションにアクセスしてみて、メンテナンス画面に切り替わったことを確認します。

これで新規アクセスはできなくなりましたが、処理中のリクエストやジョブキューからデータベースへの書き込みが行われないように、webとworkerのdyno数を0にしてデータベースへのコネクションをなくします。

アップグレード後にDynoの数は元に戻すので、dynosの数を以下のコマンドで確認しておきます。

heroku ps -a your_app_name

次のようなアウトプットが画面にされるので、web, workerそれぞれのdyno数を示している$RAILS_ENVの後の括弧の中の数字をメモします。

=== web (Standard-1X): bundle exec rails server -p $PORT -e $RAILS_ENV (2)

web.1: up 2024/04/22 15:19:09 +0900 (~ 19h ago)

web.2: up 2024/04/22 14:57:29 +0900 (~ 20h ago)

=== worker (Standard-1X): bundle exec sidekiq (1)

worker.1: up 2024/04/22 13:43:02 +0900 (~ 21h ago)

そして、dynoの数を一旦すべて0にします。

heroku ps:scale web=0 worker=0 -a your_app_name

dyno数が0になったことを以下のコマンドで確認できたらOKです。

heroku ps -a your_app_name

データベースアップグレード

データベースの構成はプライマリーが1台、フォロワーが1台ですが、アップグレードの対象となるのはフォロワーの方です。フォロワーのバージョンをアップグレードし、フォロワーを新しいプライマリーとして入れ替えると、古いプライマリーは旧バージョンのままアプリケーションから切り離され、最終的には削除されます。

まず、フォロワーのデータがプライマリーにキャッチアップしていることを以下のコマンドで確認します。

heroku pg:info -a your_app_name

プライマリーとフォロワーの様々な情報が表示されますが、フォロワーのBehind Byが0 commitsになっていればプライマリーにキャッチアップしているという意味です。もしこの数字が0でない場合はキャッチアップするまでしばらく待ちます。

念のため、アップグレード前にデータベースのバックアップを作成します。

heroku pg:backups:capture -a your_app_name

そして、アップグレードするバージョンを指定してフォロワーをアップグレードします。

heroku pg:upgrade HEROKU_POSTGRESQL_COLOR --version 13 -a your_app_name

データベースの大きさによってアップグレードにかかる時間は変化します。herokuの公式情報では30分前後が目安のようです。ちなみに私の場合はデータベースがそこまで大きくなかったので10-15分程度でした。

あまり時間がかかると本当に進んでいるのかと心配になりますが、アップグレード完了前の進捗は以下のコマンドでモニタリングすることができます。

heroku pg:wait -a your_app_name

アップグレードが終わったら、フォロワーをプライマリーDBにプロモートします。

heroku pg:promote HEROKU_POSTGRESQL_COLOR -a your_app_name

これで、フォロワーが新しいプライマリーに切り替わり、アプリケーションからのDBアクセス先になります。また、以前のプライマリーはアプリケーションから自動的に切り離されます。このフォロワーがプライマリーに切り替わるのはAttach DATABASEリリース、旧プライマリーが切り離されるのはDetach DATABASEリリースとして記録されます。また、Detach DATABASEリリースは失敗したリリースとして記録されますが、アップグレードの標準プロセスの一環なので心配する必要はありません。これらのリリースは以下のコマンドで確認できます。

heroku releases -a your_app_name

ただ、以前のプライマリーが降格したからといって新しいフォロワーになるわけではないので、手動で新しいフォロワーを作成する必要があります。フォロワーのバージョンは指定しなくても、自動でプライマリーと同じバージョンで作成されます。

heroku addons:create heroku-postgresql:standard-0 --follow DATABASE_URL -a your_app_name 

念のため、以下のコマンドでStatusがAvailableになりBehind Byが0になるまで待ちます。

heroku pg:info -a your_app_name

これでデータベース関連の作業は完了となりますので、次はアプリケーションを元に戻す作業に移行します。

メンテナンスモードの解除

dynoの数が現状は0になっているので、メンテナンスを解除する前に元の数に戻す作業が必要です。メンテナンスモードを有効化した際にメモしておいた元の数を、以下のコマンドに代入します。

heroku ps:scale web=2 worker=1 -a your_app_name

念のため、dynoの数が元に戻ったことを確認します。

heroku ps -a your_app_name

最後にメンテナンスモードを解除します。

heroku maintenance:off -a your_app_name

これで再びアプリケーションにアクセス出来ますので、ジョブキューを含む主要機能を動かしてみて、webとworker両方のdynoが無事に動いていることを確認します。

旧プライマリーデータベースの削除

念のため数日間待ち、アプリケーションに問題がないことを確認出来たら、元のプライマリーDBを完全に削除します。

heroku addons:destroy HEROKU_POSTGRESQL_COLOR -a your_app_name

これでPostgresのアップグレード作業は全て完了です。

実際に作業を行ってみた感想ですが、自前でサーバーを立てている場合と比べると、アップグレード作業はかなり楽な印象です。こういうインフラメンテの手間を軽減することでアプリケーション開発にフォーカス出来るのが、若干割高でもPaaSだったりマネージドDBサービスを使う大きな理由の一つかなと再確認できました。