Cloud Functions から月 4,300 円のサーバーへ移した:1 日 3,700 円の課金、EEA で全滅した Yahoo!、移行で踏んだ 5 つの罠
トレカダッシュ 開発の記録 / 2026年8月
iOS アプリ「トレカダッシュ」の裏側は、もともと、Google の Cloud Functions(関数ごとに、動いた時間だけ課金されるサービス)で動いていました。2026 年 8 月 16 日に、無料のお試しの期間が終わると、8 月 17 日から、1 日約 3,700 円(月に直すと約 106,000 円)の実費がかかり始めました。
この記事は、そのコードをほとんど書き換えずに、月約 4,300 円のサーバー(VPS)に移した記録です。決めたこと、あとで直したこと、踏んだ罠を書きます。
数字は、2026 年 8 月 20 日の作業の記録から書いています。サーバーの住所や接続の情報は、書いていません。
なぜ移したか:待ち時間にも、料金がかかっていた
Cloud Functions の料金のうち、約 97% が、CPU の時間(vCPU 秒)でした。ところが、このアプリの関数は、ほとんど全部が、スクレイピング先や AI の返事を待っているだけです。その待ち時間も、フルに課金されていました。
定額のサーバーに移せば、この構造的な無駄は消えます。
Cloudflare Workers も検討しました。ただ、Firestore の管理用の道具(Admin SDK)が動かず、数百か所を別の API に書き換える必要があったので、やめました。VPS なら、今のコードが、そのまま動きます。
選んだもの、と、最初に間違えた場所
| 項目 | 内容 |
|---|---|
| 提供元 | Hetzner Cloud |
| スペック | 2 vCPU、4GB メモリ、80GB SSD |
| 場所 | シンガポール |
| 料金 | 月 €26.99(約 4,300 円) |
最初は、ヘルシンキ(月 €5.99)に作りました。そして、作り直しました。
理由は、Yahoo! ショッピングの API が、欧州経済領域(EEA)とイギリスからのアクセスを遮断していることでした。ヘルシンキから叩くと、API の返事ではなく、「EEA およびイギリスからご利用のお客様へ」という案内のページが、HTTP 403 で返ります。15 ジャンルが全滅しました。これは、ボット対策ではなく、法的な理由の地域ブロックなので、何度試しても、通りません。シンガポールからは、HTTP 200 で、普通に取れます。
ヘルシンキのサーバーは削除し、かかった実費は €0.02(約 3 円)でした。
Hetzner の料金は、欧州の 3 拠点だけ、月 €7 の割引があります。さらに、いちばん安い種類(CX シリーズ)は、欧州でしか売っていません。つまり、「安い場所 = EEA = Yahoo! が死ぬ場所」という関係です。安さで、欧州に戻してはいけません。
シンガポールにした副次効果もありました。日本のサイトが近くなり、ポケモンカードの新着チェックが、52.7 秒から 15.8 秒(3.3 倍速)になりました。
コードを書き換えずに動かす:require を横取りする
実行役のプログラム(runner.js)を作りました。もとのコード(index.js)は、一切書き換えていません。
定期実行の定義 132 個を、手で書き写す代わりに、require(読み込み)を横取りして、firebase-functions を、自分のものに差し替えました。定期実行を登録する呼び出しを、「一覧に登録するだけの関数」にして、そのあとで、もとのコードを読み込みます。すると、全部の定義が、自動で集まります。回収した数は、次のとおりです。
定期実行 132 件 / HTTP のエンドポイント 256 件 / Firestore のトリガー 9 件
手書きしなくて、正解でした。ある定期実行の間隔は、`every ${X_CHECK_INTERVAL_MINUTES} minutes` というテンプレート文字列で書かれています。目で書き写していたら、間違えていたはずです。132 件を機械的に突き合わせて、全件一致を確かめました。
Firestore のトリガー(データの変化で動く関数)9 件は、VPS では発火しません。これらは、Cloud Functions 側に残しました(イベントで動くので、費用はほぼゼロです)。HTTP のエンドポイント 256 件も、アプリと管理画面が叩いているので、Cloud Functions に残しました。
移行で踏んだ 5 つの罠
1. 秘密の値は、5 本ではなく 15 本だった
移行前の記録には、「秘密の値は 5 本」と書いてありました。実際にコードの中の環境変数を全部数えると、15 本ありました。足りていなかった 10 本は、楽天、Google マップ、Yahoo!、X の認証、課金の連携、会員サイトのセッション、デバッグ用などです。
移し忘れは、エラーにならず、静かに、取得が 0 件になるので、気づきにくいものです。環境変数の名前を、コードから機械的に抜き出して数えます。
2. 秘密の値の中に、改行が入っていた
会員サイトのセッションの値は、「1 行に 1 つのサイト」の形式で、値そのものに改行が入っています。素直に設定ファイルへ書くと、2 行目以降が、別の変数として扱われ、4 つのサイトのうち 3 つが、静かに死にます。設定ファイルの読み込みを、改行の書き方(\n)に対応させました。
3. 書き込み禁止の試運転で、無限ループになった
楽天や Yahoo! の API には、呼び出しの間隔を守るための待ち(レート制限)があります。「最後に呼んだ時刻」を Firestore に書き、それが進むまで、ずっと待つ作りです。試運転で、書き込みを全部握りつぶすと、時刻が永遠に進まず、ループから抜けられません。(しかも、Cloud Functions のほうが 1.5 秒ごとに時刻を更新し続けるので、「他が更新したから、まだ待つ」を、無限に繰り返しました。楽天市場のジョブが、15 分以上帰ってこなくなって、発覚しました。)試運転でも、この待ちの記録だけは、書き込みを通すようにしました。
4. 「書き込みあり・通知なし」は、通知を静かに消す
通知をまとめて送る処理は、送れたと見なすと、送信待ちの一覧を削除します。通知だけを偽物にして、書き込みを許すと、送らずに消すことになり、通知が失われます。そこで、試運転の段階では、通知に関わる 7 つの定期実行を、VPS 側に、登録しないようにしました。外してよいのは、通知を ON にするときだけです。
5. 出口の IP が変わると、取得が 1 つ、静かに止まる
あるスーパーのサイトは、素のリクエストで取得していて、予備の手段がありませんでした。日本からは 200、シンガポールからは 403 になりました。全 75 ホストを、日本とシンガポールの両方から叩いて突き合わせると、「日本で見えて、シンガポールで見えない」のは 6 ホスト。そのうち 5 つは、もともと取得サービス経由か、予備つきで無事でした。壊れたのは 1 件だけでした。「シンガポールから 403」だけでは、壊れている証拠になりません。取得サービス経由かどうかを、必ず確かめます。
移行の順番:通知を二重に送らないために
- 段階 1:読むだけ(書き込みも通知もしない)で、取得の件数を、Cloud Functions と比べる。
- 段階 2:書くが、通知は Cloud Functions に任せる。
- 段階 3:Cloud Functions の、スケジュール関数 132 個だけを削除する。
- 段階 4:VPS を、通知 ON に切り替える。
どちらか一方だけが、通知を送る状態を、常に保ちます。同時に両方が送ると、利用者に、同じ通知が 2 回届きます。
分かったこと
待つだけの処理に、時間で課金されるサービスは、合わない。
- 課金の単位を見る。待ち時間にも課金される構造だと、スクレイピングや AI の呼び出しは、割高になります。
- 置き場所には、法的な制約がある。安い場所が、使えない場所のこともあります。
- コードを書き換えずに移す道を探す。横取りと機械的な突き合わせで、手書きの誤りを避けました。
- 設定の数は、機械で数える。5 本ではなく 15 本でした。
- 段階を踏んで、通知の二重送信を避ける。
この話に出てくるもの
- トレカダッシュ — トレカの抽選・予約・先着の情報を集める iPhone アプリ
- 配備のたびに、日次の処理が丸ごと走り直していた — 移行後の VPS での失敗
- 同時実行の枠を増やす前に見るもの — VPS の 2 コアの話
- Firestore の費用を円で出したら、読み取りの 91% が 1 か所だった — 別の費用の話
ほかの記録
- 広告が出ないと、実機チェックの 25 項目が止まった:開発者モードだけテスト広告を使い、賞品名を直書きしない — 配信前は広告の在庫が無く、実機チェックリスト 163 項目のうち 25 項目が確認できなかった。開発者モードのときだけ、Google のテスト用の広告枠に切り替えた。賞品名は 1 つの設定から読み、チェックリストにも直書きしない。
- 「これベイブレードじゃん」が、関門を入れても戻ってきた:弾いた値が、別の形で漏れた — 「商品名に玩具の名前があれば作らない」関門を入れても、AI が返す商品名から玩具の名前が消えて、すり抜けた。判定を告知の本文に移し、「カードゲームの語が 1 つも無いときだけ」弾く条件にした。
- 「次回へ」と出続けていた 25 本の点検は、一度も走っていなかった:3 つの時間予算を、同じ開始時刻から数えていた — 点検の処理は 3 段構えで、予算が同じ開始時刻から数えられていたため、持ち回りの 25 本は毎回 0 本だった。ログの「18 件を次回へ」は全部の意味だった。制限時間から割り付ける形に直し、各ステップの所要時間をログに出した。
コメント
感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。
押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはあつかいに書いています。