Cloud Functions から月 4,300 円のサーバーへ移した:1 日 3,700 円の課金、EEA で全滅した Yahoo!、移行で踏んだ 5 つの罠

English version

トレカダッシュ 開発の記録 / 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. 段階 1:読むだけ(書き込みも通知もしない)で、取得の件数を、Cloud Functions と比べる。
  2. 段階 2:書くが、通知は Cloud Functions に任せる。
  3. 段階 3:Cloud Functions の、スケジュール関数 132 個だけを削除する。
  4. 段階 4:VPS を、通知 ON に切り替える。

どちらか一方だけが、通知を送る状態を、常に保ちます。同時に両方が送ると、利用者に、同じ通知が 2 回届きます。

分かったこと

待つだけの処理に、時間で課金されるサービスは、合わない。

この話に出てくるもの

ほかの記録

コメント

感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。

押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはあつかいに書いています。