GCPで60万円請求されクラウド破産しかけた

Reactの学習のため、GoogleのMaps APIを使ったかんたんなアプリを作っていたとき、誤って無限ループのコードを書いてしまい、その結果約60万円もの料金を請求されてしまいました。

幸い、Googleに電話&メールし、免除してもらえることになりましたが、反省と自戒のため記事にまとめました。

60万円の請求書

高額請求に気づいたのは8月1日の朝。目が覚めてスマホを見ると、GoogleからGCPの請求書が届いていました。

GCPに登録した昨年から毎月届くものの、いつも0ドルのため最近はあまり確認することはなかったのですが、添付のPDFを開くと目を疑うような数字が。

カナダドルで7277ドル、ということは日本円換算で約60万円

詳細を確認すると、GoogleマップのGeocoding APIの数字がすごいことに。120万って。

課金アラートが来ていた

8月1日に、もともと設定していた10ドルの課金予算アラートのメールが届いていたのですが、その時は10ドルならいいかと思い、詳しく確認しないままでした。

また、課金予算アラートが届いたからといって、自動的に停止するわけではありません。これに関しては別件で運用しているGoogle広告と勘違いしていました…。

まずは電話

GCPの電話サポートは、英語だけではなく日本語にも対応しているとのことですが、電話した時間帯は日本では深夜。日本語対応の時間帯(平日9〜17時)になるまで待とうかとも思いましたが、いても立ってもいられないので英語の番号に電話しました。

電話では、

  • 自分のミスで無限ループしてしまいました
  • 事前に課金アラートを設定していたが、それを一瞬で上回ってしまったようです
  • 従量課金制であることは承知しているが、今回の支払いを免除してもらえませんか

といった内容を話しました。

担当の方からは「状況を理解しました。Mapsの担当者からメールであらためて連絡します」とのことでした。

メールで経緯の説明と再発防止策

翌日、Google Maps Platform Billing Supportから、下記の質問に対する回答を求めるメールが届きました。

  • アプリの使用目的
  • APIの利用目的
  • 特定した原因
  • 問題の解決方法と、実施した再発防止策
  • 利用規約の確認 https://cloud.google.com/maps-platform/terms
  • 今後発生する料金の支払い義務があることの確認

特に3番目については、実際にコードを貼り付けて無限ループが発生した理由を説明し、また4番目では割り当て上限を設定したスクリーンショットも貼り付けました。

原因はuseEffectの無限ループ

恥ずかしながら原因を…

作っていたのは航空券の価格検索アプリ。空港名を入れて検索すると、その区間の最安価格の航空券を表示してくれるというものです。画面右に到着空港の周辺地図が表示されるようにしたのですが、これが問題でした。

検索ボタンをクリックするたびに、テキストフォームに入力された到着空港の名称を受け取り、Geocoding APIにリクエストすることで、返ってきたその空港の座標を地図上に表示するというものです。

  const [location, setLocation] = useState({
    lat: 0,
    lng: 0,
    name: '',
    address: '',
    url: ''
  });

  useEffect(() => {
    axios.get(`https://maps.googleapis.com/maps/api/geocode/json?address=${props.name}&key=API_KEY`)
    .then(res => {
      const data = res.data.results[0];
      setLocation({
        lat: data.geometry.location.lat,
        lng: data.geometry.location.lng,
        name: data.address_components[0].long_name,
        address: data.formatted_address,
        url: 'https://www.google.com/maps/place/?q=place_id:' + data.place_id,
      });
    })
    .catch(error => {
      alert(error);
    })
  }, [location])

親コンポーネントから渡ってきた空港の名称(例:「バンクーバー国際空港」)で、Geocoding APIを叩き、その空港の地図座標や住所などの情報を受け取ります。その情報は、useStateでこのコンポーネントにセットします。

親コンポーネントで検索ボタンがクリックされるたびに実行…しているつもりだったのですが、あろうことかuseEffectの第2引数に、同じコンポーネント内のuseStateの変数であるlocationを設定してしまっていました。

これではAPIリクエスト→結果をuseStateで保存→locationに変更があったことを検知→useEffectが実行→APIリクエスト・・・の無限ループになってしまいます。自分でもなぜこんなコードを書いたのか… 親コンポーネント上で使用していた変数名と混同していたようです。

また、一連のコードを、以前作っていた別のアプリからよく確認しないままコピペしたのもいけなかったです。その際はHooksではなくクラスコンポーネントのComponentDidUpdateを使っていました。

開発中は随時コンソールを確認しているのですが、膨大な数のリクエストが発生していたであろうにも関わらず、このAPIに関するエラーは(記憶している限りでは)表示されませんでした。もちろん、その際にネットワークのタブも見ていれば、異常な値に気づけたと思います。

返金を確認

回答メールを送って数日後、無事クレジットカードへの返金手続きがされたことを確認しました。あぁよかった…

2度とこのようなことを起こさないよう、今では開発中に常にGCPのダッシュボードを開いてリクエスト数を確認しています。

また、時間・日毎の割り当て上限を設定したところ、何度かリロードを繰り返すと上限に達しエラーが返ってくることも確認しました。

参考になりましたら幸いです。

タイトルとURLをコピーしました