Experiment record
Apps Scriptの時間主導トリガーだけでOpen-Meteoが429になった原因調査
同じApps Script関数が手動実行ではHTTP 200、時間主導トリガーではHTTP 429となり、外向き通信経路も異なることを比較試験で確認した。運用対策としてCloudflare Workerを中継した。
結論
障害の原因は、収集プログラム内に「自動実行専用の別処理」があったためではありませんでした。同じ collectOpenMeteoHourly() 関数でも、Apps Scriptエディタからの手動実行では成功し、Google Apps Scriptの時間主導トリガーから実行するとHTTP 429で失敗しました。
一時的な診断ログを追加したところ、両者の外向き通信経路も同一ではありませんでした。成功した手動実行では外部IP確認サービスからIPv6アドレスが観察され、失敗した時間主導トリガーでは別のIPv4アドレスが観察されました。同時に、Open-Meteoは自動実行に対して日次APIリクエスト上限の429を返しました。
この結果だけで、Open-Meteoが具体的に何をキーとしてレート制限しているかまでは証明できません。ただし、アプリケーションの主要コード経路が同じである一方、実行コンテキストと通信経路が異なっていたことは確認できました。運用上の対策として、Apps ScriptからOpen-Meteoを直接呼ばず、Cloudflare Workerを中継する構成へ変更しました。操作者からは、この変更後に自動収集の問題が解消したと報告されています。
発生した症状
気象データ収集処理は、Google Apps ScriptからOpen-Meteoへアクセスし、欠けている時間別レコードをスプレッドシートへ追記する構成でした。
手動実行は成功する一方、時間主導トリガーでは失敗しました。自動実行時の代表的なログは次のとおりです。
executionType=TRIGGER
OpenMeteo HTTP status=429
Daily API request limit exceeded. Please try again tomorrow.
同じ調査中の手動実行では次の結果でした。
executionType=MANUAL
OpenMeteo HTTP status=200
正確な外向きIPアドレス、地点座標、スプレッドシートID、アカウント識別子、家庭内の収集データは公開記録から除外しています。
コード経路の確認
構成変更を行う前に、収集コードを確認しました。
重要だったのは「自動実行時だけ余分なOpen-Meteoアクセスを行う分岐が存在しなかった」という否定的証拠です。手動実行も時間主導実行も、同じ収集関数と同じ取得関数を通っていました。
また、HTTP 429を受け取った場合は、その時点で追加リトライを停止する実装でした。
if (status === 429) {
const err = new Error('Open-Meteo HTTP 429');
err.openMeteoRateLimited = true;
throw err;
}
// リトライループ内
if (err && err.openMeteoRateLimited) break;
したがって、「自動実行が429になった後に4回連続アクセスし、自分で制限を悪化させていた」という構造ではありませんでした。
診断用ログの追加
collectOpenMeteoHourly() にイベントオブジェクトを受け取らせ、手動実行とトリガー実行をログ上で識別しました。
function collectOpenMeteoHourly(e) {
const executionType =
e && e.triggerUid ? 'TRIGGER' : 'MANUAL';
console.log('executionType=' + executionType);
console.log('triggerUid=' +
(e && e.triggerUid ? e.triggerUid : ''));
}
さらに、診断期間だけ UrlFetchApp.fetch() から外部IP確認サービスを呼び、観察された外向きアドレスを記録しました。同時にOpen-MeteoのHTTPステータスも記録しました。
比較結果
得られた結果は次のとおりです。
| 実行方法 | 観察された外向き経路 | Open-Meteo応答 |
|---|---|---|
| エディタからの手動実行 | IPv6 | HTTP 200 |
| 時間主導トリガー | 別のIPv4経路 | HTTP 429、日次リクエスト上限 |
正確なIPアドレスは、原因の再現に必須ではなく、非公開環境のインフラを識別し得るため公開していません。
この試験から確認できたこと
確認済み
- 手動と自動は同じ収集関数を実行していました。
- 手動実行はHTTP 200でした。
- 時間主導トリガー実行はHTTP 429でした。
- 429のレスポンス本文は日次APIリクエスト上限を示していました。
- 一時的な外部IP診断では、手動と自動で異なる外向きアドレスが観察されました。
- 自動実行だけOpen-Meteoを余分に呼ぶコードはありませんでした。
- 429時には追加リトライを停止していました。
推論
得られた証拠から、障害はアプリケーションロジックの差ではなく、実行コンテキストまたはネットワーク経路に依存していた可能性が高いと判断しました。
時間主導トリガーが、すでに制限対象となっているネットワーク識別子または経路を通ってOpen-Meteoへ到達し、手動実行では別経路を通った、という説明は観察結果と整合します。ただし、Open-Meteoの正確なレート制限キーまではこの試験では確定していません。
未確定
- Open-Meteoが送信元IP、より広いネットワーク識別子、その他の仕組みのどれを直接レート制限キーとしているか。
- IP確認サービスとOpen-Meteoが常に完全に同一の出口アドレスを観察するか。
- 対策後の時間主導実行ログそのものは、この公開記録には保存していません。
対策
直接アクセスする構成、
Apps Script -> Open-Meteo
を、次の構成へ変更しました。
Apps Script -> Cloudflare Worker -> Open-Meteo
これにより、Apps Scriptの時間主導実行で選択されるGoogle側の外向き経路へ直接依存しない構成にしました。
既存のデータ処理ロジックは維持しています。具体的には、日時キーによる重複排除、未登録レコードだけの追記、直近72時間の限定バックフィルです。
原因特定後は、外向きIPを取得するためだけに追加した診断用通信は本番コードから削除できます。
再利用できる切り分け手順
外部APIが「手動では動くがスケジューラだけ失敗する」場合は、次の順序で確認できます。
- 手動と自動が本当に同じ関数・同じコード版を呼んでいるか確認する。
- 実行種別をログへ明示する。
- 提供者から返るHTTPステータスと短いレスポンス本文を記録する。
- 自動実行だけ追加アクセスや異なる再試行を行っていないか確認する。
- 共有型の外向き通信基盤を使う環境では、必要に応じて一時的に外向きネットワーク識別子を比較する。
- 「確認できた事実」と「提供者側の制限方式に関する推測」を分離する。
- 実行基盤の出口経路が運用上の問題なら、管理可能な中継層をスケジューラと外部APIの間に置く。
- 原因特定後は診断用の追加通信を削除する。
関連する実験
本件は OPENMETEO-MSM-001 の後続記録です。以前の実験では、気象履歴収集をスプレッドシート数式依存から直接API取得へ変更しました。今回は、その直接API方式で後から確認された「時間主導トリガー固有の429」を扱っています。