GASの【保守自動化】GASで設定されているトリガーを一括削除する方法

GAS
目次

Google Apps Script(GAS)でスプレッドシートの集計や外部APIとのデータ同期を日常的に運用していると、特定の時間やスプレッドシートの編集をトリガーにして処理を自動起動する場面が数多くあります。手動で管理画面から登録する運用であれば問題は表面化しにくいのですが、深夜の再試行処理や定期バッチの再スケジュールなどをスクリプト内部から自動生成させていると、いつの間にか古いトリガーが消去されずにプロジェクト内に放置され、気づいた時には上限エラーでシステムが完全に沈黙してしまうという事態が起きます。

こうしたトラブルを未然に防ぎ、保守の自動化を進めるために不可欠となるのが、スクリプト側から登録済みトリガーを洗い出してきれいに消し去る処理です。しかし、ただトリガーを消去するスクリプトを走らせれば万事解決かというと、現場では「消してはいけない手動トリガーまで吹き飛んで毎朝の定期配信が止まる」「削除の直後に即座に再登録したはずが、タイミングのずれで同じ関数が2回同時に走ってしまう」といった予期せぬ落とし穴に直面します。この記事では、トリガーの一括削除を自動化する具体的なアプローチと、実務で事故を起こさないための設計手法について深く掘り下げていきます。

動的に生成されたトリガーが上限20個に達して夜間バッチが停止する背景

スプレッドシートを使った現場業務でGASの保守自動化を進める際、もっとも頻繁に遭遇する障害の一つがトリガーの増殖による実行不能エラーです。GASにはプラットフォーム側の制限として、1つのプロジェクトに登録できるトリガーの件数に厳格な上限が設けられています。具体的には、1つのプロジェクトあたり最大20個までしかトリガーを保持できません。この制限は、手動でイベントを設定している間は滅多に引っかかることがないため、多くの開発者が見落としがちなポイントとなっています。

問題が表面化するのは、スクリプト内で動的にタイマートリガーを生成するロジックを組み込んだ時です。例えば、「外部APIのレートリミットを回避するために、残りの処理を10分後に再実行する一時トリガーを発行する」「毎時0分に起動したバッチ処理が、次の実行時刻を動的に計算して新たなトリガーをセットする」といった高度な保守自動化の仕組みを作ったとします。このとき、過去に実行が終わったはずのトリガーオブジェクトは、明示的に削除処理を呼び出さない限り、プロジェクト内に永続的なゴミとして残り続けます。

その結果、数日から数週間かけてひっそりと不要なトリガーが積み上がり、21個目を登録しようとした瞬間に「ScriptApp.newTrigger()」が例外を吐き出してスクリプト全体がクラッシュします。しかも悪いことに、このエラーが発生した時点では既存の古いトリガーも正常に消化されず、夜間の定期実行そのものが完全に止まって翌朝の業務が停止するまで誰も気づかないという深刻なインシデントに発展しがちです。このため、動的なスケジュール運用を行うプロジェクトにおいては、定期的に登録トリガーを監査し、不要になったものを一括削除するクリーンアップルーチンを保守の一環として組み込んでおくことが必須の要件となります。

プロジェクト内の全トリガーを検索して一掃する基本的な消去処理

全トリガーの削除を自動化するにあたり、まず直面する実務上の「ずれ」は、実行中あるいは実行待機中の一時トリガーを削除したつもりでも、参照オブジェクトがメモリ上に残っていたり、削除が完了する前に次のトリガー生成が走って想定外の二重登録を起こしたりする挙動です。GASのトリガー操作はGoogleのインフラ側で非同期的に反映される性質があるため、完全にクリアされたことを確認してから再設定を行う同期的なフローを意識しなければなりません。

以下のコードは、ScriptAppクラスを使って現在のプロジェクトに紐づいているすべてのトリガーを配列として取得し、反復処理によって1件ずつ確実に破棄していく最もシンプルで強力な一括削除の書き方です。

function deleteAllProjectTriggers() {
  // 現在のプロジェクトに設定されているすべてのトリガーを取得
  const triggers = ScriptApp.getProjectTriggers();
  
  // 取得したトリガーの配列をループして1つずつ削除
  triggers.forEach(trigger => {
    ScriptApp.deleteTrigger(trigger);
  });
  
  Logger.log("すべてのトリガーを削除しました。削除件数: " + triggers.length);
}

この処理を使うタイミングは、主に2つのパターンがあります。1つ目は、プロジェクト全体の運用設定を初期化(リセット)したい場合です。開発環境から本番環境へスクリプトを移行した直後や、テスト実行で無数に作られてしまったダミートリガーを一瞬で消し去りたいときに、この関数を手動または初期化用メニューから単発実行します。

2つ目は、定周期バッチ処理の開始直前です。例えば毎時バッチの冒頭でこの処理を走らせ、前回までに残った一時トリガーをすべて破棄したうえで、処理完了後に改めて次回のトリガーを1つだけ再登録するという「クリア&リセット」のサイクルを回すことで、トリガーが20個の上限に達するリスクを完全にゼロにできます。ただし、この処理はプロジェクトに紐づく「すべて」のトリガーを容赦なく消し去るため、後述するように消してはいけない定常トリガーが同居している環境では細心の注意が必要です。

全消去が引き起こす手動トリガー喪失の罠と特定関数に絞ったフィルタリング

前述の全削除コードは非常に強力ですが、実務でそのまま運用に投入すると新たなトラブルを生む原因になります。それが「手動で丁寧に設定したイベントトリガーの巻き添え削除」です。通常、業務で使用されるスプレッドシートのGASプロジェクトでは、動的に作成されるタイマートリガーだけでなく、「スプレッドシートを開いた時(onOpen)」や「編集時(onEdit)」、あるいは「毎朝9時に必ず実行したい日次レポート配信」といった、常設しておくべき静的なトリガーが混在していることが大半です。

前節のコードを何も考えずに実行してしまうと、こうした常設の重要トリガーまで一瞬で全滅します。結果として、「一時バッチのゴミを消すつもりが、毎朝の請求書自動送付トリガーまで消滅してしまい、数日間にわたって顧客へのメールが送られていなかった」というような致命的な運用事故につながります。したがって、保守自動化の観点から本当に求められるのは、「何でもかんでも全消去する」ことではなく、「特定の再試行関数やバッチ関数に紐づいたトリガーだけを識別して消す」というフィルタリングの視点です。

トリガーオブジェクトには、それがどの関数を実行するために作られたものかを取得できるメソッドが備わっています。これを利用して、特定のプレフィックスを持つ関数や、あらかじめ定めた一時処理用の関数だけをターゲットにして削除を実行するようにスクリプトを改良することで、手動でセットした日次トリガーを保護しながら、上限圧迫の原因となる動的トリガーだけを安全に一掃することが可能になります。全削除を実行するルーチンを組む際は、対象となるプロジェクト内に消えては困るトリガーが存在しないかを必ず棚卸ししなければなりません。

特定の関数名だけを狙い撃ちして安全にリセットする保守パターンの実装

手動で設定された常設トリガーを保護しつつ、動的に増殖した一時トリガーだけを安全に削除するためには、トリガーが呼び出す関数名を判定条件として組み込みます。これにより、毎時バッチの再登録時などに既存の特定トリガーのみを確実にクリアする、実務に耐えうるリセット処理が完成します。

以下のコードは、指定した関数名と完全に一致するトリガー、あるいは特定の命名規則を持つトリガーだけを抽出して削除する、より堅牢な保守用の実装例です。

function deleteSpecificFunctionTriggers(targetFunctionName) {
  const triggers = ScriptApp.getProjectTriggers();
  let deletedCount = 0;
  
  triggers.forEach(trigger => {
    // トリガーが呼び出す対象の関数名を取得
    const handlerFunction = trigger.getHandlerFunction();
    
    // 対象の関数名と一致する場合のみ削除を実行
    if (handlerFunction === targetFunctionName) {
      ScriptApp.deleteTrigger(trigger);
      deletedCount++;
    }
  });
  
  Logger.log(`関数 [${targetFunctionName}] のトリガーを ${deletedCount} 件削除しました。`);
}

// 実際に毎時バッチ等から呼び出す際のラッパー関数例
function refreshHourlyBatchTrigger() {
  const TARGET_FUNCTION = "executeHourlySync";
  
  // 1. 既存の同名トリガーをすべてクリアして二重起動を防ぐ
  deleteSpecificFunctionTriggers(TARGET_FUNCTION);
  
  // 2. 次回のトリガーを1件だけ新規に設定する
  ScriptApp.newTrigger(TARGET_FUNCTION)
    .timeBased()
    .after(60 * 60 * 1000) // 1時間後に実行
    .create();
    
  Logger.log(`関数 [${TARGET_FUNCTION}] のトリガーを再登録しました。`);
}

このパターンを導入することで、万が一ネットワーク障害やスクリプトの一時エラーによって複数の一時トリガーが生成されてしまっても、次回実行時に自分自身の古い影をすべて消し去ってからクリーンな状態で再スケジュールを行えます。上限20個の枠を圧迫することがなくなり、他の機能が設定しているトリガーを巻き込む心配もありません。保守を自動化する上では、単に「消す」だけでなく「対象を限定して安全に置き換える」設計が極めて重要になります。

複数ユーザー環境と権限の壁によるトリガー削除漏れのメカニズム

GASのトリガー一括削除を自動化する上で、多くの開発者が最後に突き当たる最も不可解な現象が「スクリプト上ではすべてのトリガーを削除したはずなのに、なぜか管理画面に消えないトリガーが残っており、幽霊のように勝手に実行される」という問題です。これは不具合ではなく、GASのセキュリティと実行権限の仕様に起因して発生する明確な動作のずれです。

GASのプロジェクトトリガーには、「そのトリガーを作成したユーザーの権限でしか取得・削除ができない」という根本的な制限が存在します。スプレッドシートをチームで共有し、複数のメンバーがそれぞれのGoogleアカウントでスクリプトエディタを開いてトリガーを作成した場合、あるいは過去の退職者が作成したトリガーが存在する場合、現在の実行ユーザーが「ScriptApp.getProjectTriggers()」を呼び出しても、他のユーザーが作成したトリガーは配列の中に一切含まれてきません。つまり、自分以外の人間が作ったトリガーは、プログラムからは「存在しないもの」として扱われ、削除ループを素通りして生き残ってしまうのです。

この仕様を理解していないと、「トリガーを一括削除する関数を走らせたのに、別の人のアカウントで作られた古いバッチトリガーが裏で動き続けていてデータが二重計上された」という極めて追跡が難しい障害に直面します。この問題を回避するためには、保守運用のルールとして「トリガーを動的生成・削除する処理は、必ず特定のサービスアカウントまたは専用の運用管理用Googleアカウント1つに集約して実行させる」という運用設計を徹底する必要があります。共有シートで各ユーザーが勝手にトリガーを作れる状態を放置したままでは、いかに完全な一括削除スクリプトを書いてもシステム全体の整合性を保つことはできません。

エラー発生時のリトライとトリガー全消去を組み合わせる際の安全設計

大規模なスプレッドシート処理や外部通信を伴うGASプロジェクトでは、スクリプトが途中でタイムアウト(6分制限)したり、APIが500エラーを返したりした際に、数分後にトリガーを発行して再試行させるリカバリーパターンを構築することがあります。このような「エラーリカバリー」と「トリガー一括削除」を組み合わせる場合、例外処理の書き方を一歩間違えるとシステムが無限ループに陥る危険性があります。

よくある失敗例として、try-catch構文のcatchブロック内でトリガー一括削除を実行しようとする設計が挙げられます。エラーが起きた原因がGoogleのサービス自体の一時的なダウンやScriptAppの一時的障害であった場合、トリガー削除のAPIコール自体が失敗してしまい、エラーハンドリングの中でさらにエラーが発生してスクリプトが異常終了します。その結果、古いトリガーは残ったままになり、再試行用の新しいトリガーだけが増え続け、瞬く間に上限20個を食いつぶして完全に再起不能になります。

これを回避するためには、トリガーの削除と生成をひとつの不可分なトランザクションのように扱うのではなく、各ステップで確実にエラーをハンドリングし、削除に失敗した場合は再登録を行わずに管理者にアラートメールを送信して処理を安全に停止させるような防御的なコーディングが求められます。また、PropertiesService(スクリプトプロパティ)を併用し、「現在アクティブな一時トリガーのID」をプロパティストアに退避させておくことで、仮に一括削除APIの挙動が不安定な場合でも、個別のIDを指定してピンポイントで削除成否を検証する二重の安全策を講じることも現場では有効です。自動化の度合いを高めれば高めるほど、プラットフォームの制約と失敗時の挙動を想定した堅牢なガードレールを用意しておくことが、安定運用の分水嶺となります。

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