Heroku PostgresのバックアップをHeroku PGBackupsから自前実装による論理バックアップに切り替えてHeroku Schedulerで定期実行する方法

### Heroku Postgresのバックアップ手法

[Heroku Postgres](https://jp.heroku.com/postgres)のバックアップを行うには以下の方法があります。

- 物理バックアップ
    - [Heroku Postgres Data Safety and Continuous Protection](https://devcenter.heroku.com/articles/heroku-postgres-data-safety-and-continuous-protection)（英語）
    - [Heroku Postgres のデータの安全性と継続的保護](https://devcenter.heroku.com/ja/articles/heroku-postgres-data-safety-and-continuous-protection)（日本語）
- 論理バックアップ
    - [Heroku Postgres Logical Backups](https://devcenter.heroku.com/articles/heroku-postgres-logical-backups)（英語）
    - [Heroku Postgres の論理バックアップ](https://devcenter.heroku.com/ja/articles/heroku-postgres-logical-backups)（日本語）

---

### 物理バックアップの制限

「物理バックアップ」の機能は「Standard」以上のプランの場合のみ利用可能なようなので、今回は「論理バックアップ」にフォーカスして説明していきます。

||
|:-:|
|![スクリーンショット 2025-03-02 8.43.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/568cd6fb-daca-4da3-9be4-77340377ecb3.png)|
|<font color="gray">「Heroku Postgres」の「Durability」の画面にて</font>|


||
|:-:|
|![スクリーンショット 2025-03-02 8.48.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/cab438c4-2fcd-4808-a07b-b0531b0ea381.png)|
|<font color="gray">「Heroku Postgres のデータの安全性と継続的保護」より抜粋</font>|

---

### 論理バックアップの制限

Heroku Postgres には 「Heroku PGBackups」というバックアップ機能が提供されており、バックアップを定期実行する場合も簡単に設定可能です。

- [Heroku PGBackups](https://devcenter.heroku.com/articles/heroku-postgres-backups)（英語）
- [Heroku PGBackups](https://devcenter.heroku.com/ja/articles/heroku-postgres-backups)（日本語）

ただし、「Heroku PGBackups」には「データベースのサイズが20GBまで」という制限があるようです。
今回、「Heroku PGBackups」の利用をやめて自前実装に切り替える理由はこれです。
「Heroku PGBackups」の機能を利用して定期バックアップを行っていたつもりが、実はバックアップに失敗していましたといったことがないように注意しましょう。

||
|:-:|
|![スクリーンショット 2025-03-02 9.11.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/f072db1e-7a71-4050-a666-9e4a86b08332.png)|
|<font color="gray">「Heroku Postgres の論理バックアップ」より抜粋</font>|

||
|:-:|
|![スクリーンショット 2025-03-02 9.12.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/21a9b247-4cc3-4f98-b56d-e6722f03f38c.png)|
|<font color="gray">「Heroku PGBackups」より抜粋</font>|

---

### Heroku PGBackupsの仕組み

自前実装を始める前に、Heroku PGBackupsの仕組みについて理解していきます。

「[Heroku PGBackups を使用した小さなデータベースでの論理バックアップの取得](https://devcenter.heroku.com/ja/articles/heroku-postgres-logical-backups#capturing-logical-backups-on-small-databases-with-heroku-pgbackups)」で説明されているとおり、 `heroku pg:backups` の実体は `pg_dump` と `pg_restore` です。

`heroku pg:backups` でバックアップのIDを取得し、どこに保存されているかを確認すると、Amazon S3に保存されていることがわかります。

```
$ heroku pg:backups --app アプリケーション名
=== Backups

 Id   Created at                Status                              Size   Database 
 ──── ───────────────────────── ─────────────────────────────────── ────── ──────── 
 b001 2025-03-01 23:22:23 +0000 Completed 2025-03-01 23:22:25 +0000 1.93KB DATABASE 

=== Restores

No restores found. Use heroku pg:backups:restore to restore a backup

=== Copies

No copies found. Use heroku pg:copy to copy a database to another
```

```
$ heroku pg:backups:url b001 --app アプリケーション名
https://xxxxx.s3.amazonaws.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/2025-03-01T23%3A22%3A23Z/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=xxxxxxxxxxxxxxxxxxxx%2F20250302%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250302T001750Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

---

### Heroku Postgresの定期バックアップを自前実装するための仕様

Heroku PGBackupsの仕組みを踏まえて、以下のような仕様で実装を行っていきます。

- バックアップ処理はシェルスクリプト(bash)で実装する。
- PostgreSQLのバックアップコマンドである `pg_dump` を利用する。
- `pg_dump` で出力されたダンプファイルを `gzip` コマンドで圧縮する。
- gzip圧縮したファイルファイルをAmazon S3にアップロードする。
- Heroku Schedulerを利用してバックアップ処理を定期実行する。

---

### 事前準備

- Amazon S3の操作が可能なIAMアカウントを準備します。
- 「[Heroku buildpack for the AWS CLI](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-awscli)」の手順に従って、ビルドパックを追加します。
    - この手順内でHerokuのConfig VarsにS3を操作可能なIAMアカウントの「 `AWS_ACCESS_KEY_ID` 」、「 `AWS_SECRET_ACCESS_KEY` 」、「 `AWS_DEFAULT_REGION` 」を追加します。
- HerokuのConfig VarsにS3のバケット名を「 `AWS_S3_BUCKET` 」として追加します。
- HerokuのConfig Varsの登録状況を確認します。
    - ![スクリーンショット_2025-03-02_9_57_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/99783f96-236e-4786-ad05-0cf59dd600b8.png)
    - 「 `DATABASE_URL` 」については、Heroku Postgresを利用している場合は自動的に追加されています。
- [Heroku Scheduler](https://elements.heroku.com/addons/scheduler)をAdd-onsとして追加します。

---

### バックアップスクリプトを実装

- 処理の流れ
    - Config Varsで登録済みの各環境変数の必須入力チェックを行う。
    - `pg_dump` コマンドでダンプファイルを出力する。
        - 「[より大規模なデータベースでの論理バックアップの取得](https://devcenter.heroku.com/ja/articles/heroku-postgres-logical-backups#capturing-logical-backups-on-larger-databases)」では、オプションに「 `-F c` 」が指定されていますが、圧縮については `gzip` コマンドを利用するので、プレーンテキストとするために「 `-F p` 」を指定しました。
        - 「 `-F c` 」を利用すると、ダンプファイルの中身をテキストエディタで開いて確認するといったことができなくなるため、今回は「 `-F p` 」を採用しました。
    - 出力されたダンプファイルを `gzip` コマンドで圧縮する。
    - gzip圧縮されたファイルをAWS CLI( `aws s3 cp` )でAmazon S3にアップロードする。
    - 本番環境の場合であれば、成功・失敗時のSlackなどへの通知処理については実装しておいた方がよいですが、今回は処理を簡素化するために実装内容から割愛しています。

```
$ cat pgbackup.sh 
#!/bin/bash

set -eu
set -o pipefail

# 環境変数の必須入力チェック
if [ -z "${DATABASE_URL:-}" ]; then
    exit 1
fi
if [ -z "${AWS_S3_BUCKET:-}" ]; then
    exit 1
fi
if [ -z "${AWS_ACCESS_KEY_ID:-}" ]; then
    exit 1
fi
if [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then
    exit 1
fi
if [ -z "${AWS_DEFAULT_REGION:-}" ]; then
    exit 1
fi

BACKUP_FILE_NAME="dump_$(TZ=Asia/Tokyo date +%Y%m%d_%H%M%S).sql.gz"
BACKUP_FILE_PATH="/tmp/${BACKUP_FILE_NAME}"

# データベースのダンプ処理
if ! pg_dump -F p --no-acl --no-owner --quote-all-identifiers "${DATABASE_URL}" | gzip > "${BACKUP_FILE_PATH}"; then
    exit 1
fi

# AWS_S3_BUCKETの末尾に「/」が含まれる場合は「%/」により削除する
S3_BUCKET_PATH="${AWS_S3_BUCKET%/}/${BACKUP_FILE_NAME}"

# S3へのアップロード処理
if ! aws s3 cp "${BACKUP_FILE_PATH}" "${S3_BUCKET_PATH}"; then
    exit 1
fi

exit 0
```

---

### バックアップスクリプトを反映

```
$ git add pgbackup.sh 
$ git commit -m "add pgbackup script"
$ git push heroku main
```

---

### バックアップスクリプトを手動実行して動作確認

利用している「[Heroku-24 Stack](https://devcenter.heroku.com/articles/heroku-24-stack)」は「Ubuntu 24.04」をベースにしているため、「 `sh pgbackup.sh` 」と実行するとdashが実行されるはずなので、明示的に「 `bash pgbackup.sh` 」として実行しています。

```
$ heroku run bash pgbackup.sh 
Running bash pgbackup.sh on ⬢ アプリケーション名... up, run.9667
upload: ../tmp/dump_20250302_104213.sql.gz to s3://バケット名/dump_20250302_104213.sql.gz
```

shebangに `#!/bin/bash` と記述しているので、「 `sh pgbackup.sh` 」と実行しても実際には問題ないかもしれませんが、実行権限を付与して以下のように実行することも可能です。

```
$ chmod +x pgbackup.sh
$ heroku run ./pgbackup.sh 
Running ./pgbackup.sh on ⬢ アプリケーション名... up, run.7937
upload: ../tmp/dump_20250302_105126.sql.gz to s3://バケット名/dump_20250302_105126.sql.gz
```

---

### Amazon S3にアップロードされていることを確認

上記の手動実行により、2つのファイルがアップロードされています。

![スクリーンショット_2025-03-02_10_54_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/430831a7-3bf3-45a0-9781-6518966ff9c9.png)

---

### Heroku Schedulerによる定期実行の設定

Heroku Schedulerの画面で「Create Job」をクリックします。

![スクリーンショット 2025-03-02 10.59.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/a8d85b31-cd4f-4a2e-8cf0-106bb235e370.png)


Job Editorの画面で以下のように昼の12時(JST)に実行されるように設定します。

![スクリーンショット 2025-03-02 11.02.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/470ae1df-2177-495b-bc86-36585e3b3db6.png)

Jobの登録に成功すると以下のように一覧画面に表示されます。

![スクリーンショット 2025-03-02 11.04.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/e6407e65-d31e-4d00-bfe7-0a1498d66d70.png)

---

### Heroku Schedulerにより定期実行されていることを確認

指定していた「昼の12時(JST)」になると定期処理が実行され、S3にファイルがアップロードされています。

![スクリーンショット_2025-03-02_12_01_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/921147ad-8a23-40a8-aab9-43fb6a9d6b5d.png)

Heroku SchedulerのJob一覧画面には次回の実行タイミングが更新され「翌日の昼の12時(JST)」が表示されています。

![スクリーンショット 2025-03-02 12.05.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23093/f7eccdc5-7e1e-4616-9c6b-e20f0a7a706e.png)

---

以上がHeroku PGBackupsから自前実装による論理バックアップへの切り替え作業になります。
バックアップ実行時の通知処理の実装やS3のライフサイクル設定などについては割愛しましたが、ひとまずこれで定期バックアップ自体は実現可能かと思いますので、Heroku Postgresのサイズが20GB以上になりそうな場合は参考にしてみてください。