Fly.io への移行作業とカットオーバー
ジョブ基盤の入れ替えが終わったので、プラットフォームを移します。 設定ファイルの対応、DB の移送、デプロイパイプライン、カットオーバー当日の手順を書きます。
Procfile と fly.toml の対応
移行前の Procfile です。
release: /bin/sh -c 'bundle exec rails db:migrate && bundle exec rails db:seed_fu && fc-cache -fv'
web: bundle exec puma -C config/puma.rb
worker: bin/jobs
これが fly.production.toml ではこうなります。
app = 'myapp-production'
primary_region = 'nrt'
console_command = '/rails/bin/rails console'
[deploy]
release_command = "/bin/sh -c 'bundle exec rails db:migrate && bundle exec rails db:seed_fu && fc-cache -fv'"
[env]
PORT = '3000'
RAILS_LOG_TO_STDOUT = 'true'
RAILS_SERVE_STATIC_FILES = 'true'
JOB_CONCURRENCY = '2'
[processes]
app = 'bundle exec puma -C config/puma.rb'
worker = 'bin/jobs'
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'off'
auto_start_machines = false
min_machines_running = 2 # 本番は 2 台(単一障害点の解消とローリングデプロイ)
processes = ['app']
[[vm]]
memory = '2gb'
cpu_kind = 'shared'
cpus = 8
processes = ['app']
[[vm]]
memory = '2gb'
cpu_kind = 'shared'
cpus = 8
processes = ['worker']
対応関係はほぼ 1 対 1 です。
| Heroku | Fly.io |
|---|---|
Procfile の release: |
[deploy] release_command |
Procfile の web: / worker: |
[processes] |
| dyno サイズ | [[vm]] |
heroku config:set |
fly secrets set |
heroku run rails console |
fly ssh console / console_command |
heroku releases:rollback |
fly releases --image で一覧 → fly deploy -i <image> |
heroku ps |
fly status / fly machine list |
heroku logs --tail |
fly logs |
Heroku との一番の違いは、[[vm]] をプロセスごとに書けることです。
今回は app と worker に同じサイズを割り当てましたが、片方だけメモリを増やすといった調整が設定ファイルだけで完結します。
web プロセスの起動コマンド
[processes] に bundle exec puma -C config/puma.rb と書いているのは、0.0.0.0 にバインドさせるためです。
Heroku 時代の書き方をそのまま引き継ぎました。
Dockerfile の CMD は bin/rails server のままですが、[processes] を定義すると上書きされます。
ステージングと本番の設定ファイル
fly.toml 1 枚ではなく fly.staging.toml と fly.production.toml に分け、デプロイ時に -c で指定します。
flyctl deploy --remote-only -c fly.staging.toml
差分はこうなっています。
| staging | production | |
|---|---|---|
auto_stop_machines |
'stop'(アイドルで停止) |
'off'(常時稼働) |
auto_start_machines |
true |
false |
min_machines_running |
0 | 2 |
| Sentry のトレースサンプリング | 1.0 | 0.1 |
| バックアップ用バケット | 未設定(ジョブは何もしない) | 設定あり |
ステージングを auto_stop_machines = 'stop' にできるのは Fly.io の利点です。
リクエストが来たら起動し、来なければ止まります。
Heroku の Eco dyno と違ってプロセス単位で制御できるので、app は止めるが worker は動かす、といった構成も取れます。
サンプリングレートだけは、移行時に決めた差分ではありません。 最初は両方 0.1 で始め、トレースが薄すぎたので両方 1.0 に上げ、その後 span がクォータを超えたので本番だけ 0.5 に落とし、さらに 0.1 まで下げました。 本番は出発点と同じ値に戻っています。
トラフィックの少ないステージングを 1.0 のままにしておくと、検証中の 1 リクエストがサンプリングから漏れてトレースが出ない、ということが起きずに済みます。
DB の移行
何度か試行錯誤したので手順を残します。
ダンプ
Heroku のダッシュボードからバックアップをダウンロードしました。
PGBackups は pg_dump の custom format で保存するので、落としたファイルをそのまま pg_restore に渡せます。
CLI からでも同じものが取れます。
既定の保存名が latest.dump です。
heroku pg:backups:capture -a <source-app>
heroku pg:backups:download -a <source-app>
自分で pg_dump を叩くなら -Fc を明示します。
-F を省くと既定のプレーン SQL になり、pg_restore は読めません。
pg_dump --no-acl -Fc -f latest.dump "$SOURCE_DATABASE_URL"
Fly Managed Postgres への接続
Fly Managed Postgres(MPG)は外部に公開されていないので、トンネルが要ります。
fly mpg list # CLUSTER ID を確認
fly mpg proxy <CLUSTER_ID> # localhost:16380 にトンネルが張られる
これは別のターミナルで張りっぱなしにしておきます。
ネット上の記事に出てくる --cluster フラグ形式は、現行の CLI では通りませんでした。
CLUSTER ID を位置引数で渡すか、引数なしで実行して対話的に選びます。
フラグの形は fly mpg proxy --help で確かめるのが早いです。
復元
pg_restore --no-acl --no-owner --clean --if-exists -d "$PROXY_DATABASE_URL" latest.dump
フラグにはそれぞれ意味があります。
--no-acl --no-owner:移行元固有の role と権限を持ち込まない。これがないと role 由来のエラーで失敗する--clean --if-exists:既存オブジェクトを drop してから入れ直す。何度でも再実行できる
指定するのは pg_restore の側です。
custom format のアーカイブを書き出すとき、pg_dump に同じフラグを渡しても無視されます。
本番当日に手順を間違えても、同じコマンドをもう一度流せば復旧します。
この手順をまずステージングで通し、そこで確立した手順書のまま本番のカットオーバーに臨みました。 本番の日にコマンドを考えずに済みます。 ステージングの構築を練習と位置づけておくと、多少の手戻りがあっても焦らずに進められます。
環境変数の移行
移行元から一括で吸い出し、不要なものを落として流し込みます。
# シェル形式で取得
heroku config -a <source-app> --shell > source-env.txt
# Fly 側で不要なものを除外(PAPERTRAIL_ は利用していたログ SaaS の prefix)
grep -v "^DATABASE_URL=\|^DB_HOST=\|^DB_USERNAME=\|^DB_PASSWORD=\|^DB_PORT=\|^HEROKU_\|^PAPERTRAIL_\|^RACK_ENV=\|^RAILS_LOG_TO_STDOUT=\|^RAILS_SERVE_STATIC_FILES=\|^PORT=" source-env.txt > fly-env.txt
# 中身を目視で確認してから
cat fly-env.txt
# まとめて投入
xargs fly secrets set --app myapp-staging < fly-env.txt
除外の理由です。
| 種別 | 理由 |
|---|---|
DATABASE_URL DB_* |
fly mpg attach が自動で設定する |
HEROKU_* |
移行元固有のメタ情報 |
| ログ SaaS 関連 | 転送先を変更するため不要 |
RACK_ENV |
RAILS_ENV で足りる |
RAILS_LOG_TO_STDOUT RAILS_SERVE_STATIC_FILES PORT |
fly.toml の [env] に定義済み |
作業が終わったらファイルを消します。
rm source-env.txt fly-env.txt
全 secret が平文で載ったファイルです。
.gitignore に頼らず、リポジトリの外で作業するほうが安全です。
投入後は確認します。
fly secrets list --app myapp-staging
DATABASE_URL、RAILS_MASTER_KEY、AWS 系、SMTP 系、New Relic 系、Sentry 系が入っていることを見ます。
fly secrets list は値を表示せずダイジェストだけを出すので、画面をそのまま共有しても問題ありません。
デプロイパイプライン
Fly.io には push すると自動でデプロイする GitHub Integration がありますが、使っていません。 CI が通ったときだけデプロイしたかったからです。
代わりに GitHub Actions からデプロイトークンで叩きます。
fly tokens create deploy -a myapp-staging -x 8760h
# 出力を GitHub Secrets の FLY_API_TOKEN_STAGING に登録
deploy-staging:
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
concurrency:
group: deploy-staging
cancel-in-progress: false
steps:
- uses: actions/checkout@v7
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy to Fly.io (staging)
run: flyctl deploy --remote-only --depot=false -c fly.staging.toml
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_STAGING }}
デプロイの並走
concurrency でデプロイの並走を防ぐのは定石ですが、cancel-in-progress は false にします。
デプロイを途中でキャンセルすると、release_command(db:migrate)が中断される可能性があります。
マイグレーションが中断されると、どこまで適用されたのかが分からない状態になります。
キャンセルではなく、待たせるほうを選びます。
リモートビルダーの待ち時間
Fly の既定のリモートビルダー(Depot)の確保待ちで、デプロイが Waiting for depot builder... のまま deadline_exceeded する事象が続きました。
Fly ホストのビルダーに切り替えて解消しています。
flyctl deploy --remote-only --depot=false -c fly.production.toml
Depot 側が安定したら外してよいフラグなので、なぜ付けたのかをコメントに残しておきます。 この種の回避策は、理由が失われると外せなくなります。
本番デプロイの条件
本番デプロイのジョブには needs: test を付け、CI のテストが通らないとジョブ自体がスキップされるようにしました。
deploy-production:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
Dockerfile の調整
dockerfile-rails で生成した Dockerfile をベースに、いくつか手を入れています。
アセットビルドの呼び出し
# cssbundling/jsbundling-rails の hook に依存せず明示的に実行
RUN yarn build && yarn build:css
ビルド時に読めない環境変数
assets:precompile はビルドステージで走りますが、その時点では fly secrets の値が入っていません。
ここで環境変数を参照する初期化コードがあると、ビルドが落ちます。
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
移行時に実際にこれで一度落ちました。 ローカルでは通るのに Fly のビルドだけ落ちるときは、まずここを疑うといいかもしれません。
Fly の Rails 向けガイドにも、assets:precompile は設定を読み込むので、実際には使っていない secret が未設定でも失敗しうる、と書かれています。
ダミー値を渡すほかに、初期化コードの側で Rails.application.credentials の有無を見て分岐させる方法も紹介されています。
カットオーバー当日
- Fly.io の本番アプリをデプロイする(DB は空のまま起動確認まで済ませる)
- メンテナンス時間を確保して本番 DB を移行する(前述のバックアップ取得と
pg_restore) - DNS を切り替える
- 旧環境を 1〜2 週間そのまま並行稼働させる
- 安定を確認してから旧環境を廃止する
4 を入れておくと、当日に細かい異常を見つけても、切り戻せる前提で落ち着いて切り分けられます。 旧環境の課金が 1〜2 週間延びるだけで、切り戻し先が確保できます。 実際には戻さずに済みました。
スモークテストの項目
移行対象が SaaS 連携を多く持っていたので、チェックリストを作って潰しました。
- ログイン画面が出る(Web の起動)
- ログインしてダッシュボードが出る(DB 接続とセッション)
- SMS の 2 要素認証(AWS SNS 連携)
- ファイル添付(S3 連携)
- 主要な一覧画面の表示
- Excel と CSV の生成(worker と Solid Queue の疎通)
- 予約メッセージの作成
fly logsにStarted Supervisor/Started Dispatcher/Started Schedulerと、Started Workerが 2 行出ているかStarted Workerのqueues:にsolid_queue_recurringが含まれているか。"default"だけなら定期タスクが滞留する- 翌朝、定期タスクが指定時刻に発火して完了しているか
- New Relic にデータが届いているか
定期実行は当日には検証できないので、翌朝の確認は必ず入れます。
2 本目に書いた solid_queue_recurring の滞留も、この確認がなければ気づくのはもっと後になっていました。
トラブル時の操作
machine の再起動、secret の修正、コンテナへの接続の 3 つをよく使いました。
# 特定の machine だけ再起動
fly machine list --app myapp-production
fly machine restart <machine-id> --app myapp-production
# secret の修正(設定すると machine が自動で再起動する)
fly secrets set FOO=bar --app myapp-production
fly secrets unset FOO --app myapp-production
# コンテナに入る
fly ssh console --app myapp-production
ロールバック
Fly.io には Heroku の releases:rollback に当たるコマンドがありません。
イメージの一覧を出し、戻したいイメージで再デプロイします。
fly releases --image --app myapp-production
fly deploy -c fly.production.toml -i registry.fly.io/myapp-production@sha256:<digest>
デプロイ時のイメージタグが分かっていれば、そちらを直接指定しても同じです。
fly deploy -c fly.production.toml --image registry.fly.io/myapp-production:deployment-<ID>
障害時に直前のイメージへ即座に戻す手段として、実際に使いました。
戻るのはイメージだけで、fly.toml と secrets は現在のものが使われます。
設定側に原因がある障害は、イメージを戻しても直りません。
4 本目の内容
4 本目は、移行後に作り直した運用と、移行後に踏んだ地雷を書きます。 ログの永続化とプラットフォーム外へのバックアップが中心です。