🖋️

Google Apps Scriptで本番レベルのバッチ処理を実装する

に公開
2026/10/04

TL;DR

Google Apps Scriptの6分実行制限を克服し、大量データを効率的に処理する方法を解説します。

  • 15種類のタスクを6時間ごとに自動実行
  • コスト0円/月(Apps Scriptの無料枠の範囲)

はじめに

「Apps Scriptって制限多くて使えないんじゃ…?」

そう思ってる人、多いですよね。でも、制約を理解して設計すれば、本番レベルのシステムが作れます。

この記事では、実際に運用しているシステムの実装を紹介します。

Apps Scriptの制限

主な制限

実行時間: 6分/回
トリガー: 20個/スクリプト
同時実行: 30回/ユーザー
URLフェッチ: 20,000回/日
メール送信: 100通/日(無料アカウント)

これらをどう克服するか

答え: 設計で解決する

アーキテクチャ設計

1. タスクを分割する

❌ ダメな例(6分超える):

function processAll() {
  // 15個のタスクを順番に処理
  task1(); // 2分
  task2(); // 2分
  task3(); // 2分
  // ... 合計30分 → タイムアウト!
}

✅ 良い例(分割実行):

function processBatch1() {
  task1(); // 2分
  task2(); // 2分
  task3(); // 2分
  // 合計6分以内
}

function processBatch2() {
  task4();
  task5();
  task6();
}

// トリガーで時間差実行

2. 状態管理をSpreadsheetで行う

課題: Apps Scriptは実行間で状態を保持できない

解決: Spreadsheetを状態保存先に使う

function getLastProcessedId() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('State');
  return sheet.getRange('A1').getValue();
}

function setLastProcessedId(id) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('State');
  sheet.getRange('A1').setValue(id);
}

function processIncrementally() {
  const lastId = getLastProcessedId();
  const nextBatch = getDataAfter(lastId, 100); // 100件ずつ
  
  nextBatch.forEach(item => {
    processItem(item);
  });
  
  setLastProcessedId(nextBatch[nextBatch.length - 1].id);
}

3. バッチ処理で高速化

❌ 遅い(1件ずつ書き込み):

function slowWrite(data) {
  const sheet = SpreadsheetApp.getActiveSheet();
  
  data.forEach(row => {
    sheet.appendRow(row); // 1回ずつAPI呼び出し
  });
}
// 100件 = 100回のAPI呼び出し = 遅い

✅ 速い(バッチ書き込み):

function fastWrite(data) {
  const sheet = SpreadsheetApp.getActiveSheet();
  const lastRow = sheet.getLastRow();
  
  // 1回のAPI呼び出しで全データ書き込み
  sheet.getRange(lastRow + 1, 1, data.length, data[0].length)
    .setValues(data);
}
// 100件 = 1回のAPI呼び出し = 速い

ポイント: 100件なら、1件ずつ書くとAPI呼び出しが100回、まとめて書けば1回で済む

4. エラーハンドリング

function robustProcess() {
  const maxRetries = 3;
  let retries = 0;
  
  while (retries < maxRetries) {
    try {
      // メイン処理
      const result = processData();
      
      // 成功したら記録
      logSuccess(result);
      return;
      
    } catch (error) {
      retries++;
      
      if (retries === maxRetries) {
        // 最終的に失敗したらアラート
        sendErrorEmail(error);
        logError(error);
      } else {
        // リトライ前に待機
        Utilities.sleep(1000 * retries); // 指数バックオフ
      }
    }
  }
}

5. ロギング

課題: Logger.log()は実行後に消える

解決: Spreadsheetにログ保存

function log(level, message, data = null) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Logs');
  
  sheet.appendRow([
    new Date(),
    level,
    message,
    JSON.stringify(data)
  ]);
  
  // 古いログは削除(1000件以上)
  if (sheet.getLastRow() > 1000) {
    sheet.deleteRows(2, 100); // 100件削除
  }
}

// 使用例
try {
  const result = riskyOperation();
  log('INFO', 'Operation successful', { result });
} catch (error) {
  log('ERROR', 'Operation failed', { 
    error: error.message,
    stack: error.stack 
  });
}

実装例:定期バッチ処理

実運用プロジェクトの紹介

私が運用しているシステムを例に、本記事の技術をどう使っているか解説します。

プロジェクト: BLST VPN Navi のデータ更新

https://www.blstweb.jp/network/

15社のVPNのデータを6時間ごとに自動で処理し、サイトに公開するシステムです(速度の数値は実測ではなく編集部の推定値です。料金は各社の公式サイトから取得しています)。

技術スタック:

  • Google Apps Script(自動処理)
  • Google Spreadsheet(データ保存)
  • REST API(データ公開)

運用コスト: 0円/月(Apps Scriptの無料枠の範囲)

本記事の技術をどう使っているか:

  1. タスク分割 - 15社分の処理を3バッチに分割
  2. 状態管理 - 最後に処理したIDをSpreadsheetに保存
  3. バッチ処理 - 全データを一括書き込み(API呼び出しを1回に)
  4. エラーハンドリング - 3回リトライ + メール通知
  5. ロギング - すべてSpreadsheetに記録

実装のポイント:

シナリオ

15種類の外部APIを6時間ごとに呼び出し、結果をSpreadsheetに保存する。

実装

// 設定
const CONFIG = {
  BATCH_SIZE: 5,
  TIMEOUT_MARGIN: 300000, // 5分(6分制限-1分マージン)
  RETRY_WAIT: 2000
};

// メイン関数
function runBatch() {
  const startTime = Date.now();
  const tasks = getTasks();
  
  for (let i = 0; i < tasks.length; i++) {
    // タイムアウトチェック
    if (Date.now() - startTime > CONFIG.TIMEOUT_MARGIN) {
      log('WARN', 'Approaching timeout, stopping', { 
        processed: i, 
        total: tasks.length 
      });
      break;
    }
    
    try {
      processTask(tasks[i]);
      log('INFO', 'Task completed', { task: tasks[i].name });
    } catch (error) {
      log('ERROR', 'Task failed', { 
        task: tasks[i].name, 
        error: error.message 
      });
    }
  }
}

// タスク処理
function processTask(task) {
  const maxRetries = 3;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      // 外部API呼び出し
      const response = UrlFetchApp.fetch(task.url, {
        muteHttpExceptions: true
      });
      
      if (response.getResponseCode() !== 200) {
        throw new Error(`HTTP ${response.getResponseCode()}`);
      }
      
      const data = JSON.parse(response.getContentText());
      
      // データ保存
      saveData(task.name, data);
      
      return; // 成功
      
    } catch (error) {
      if (attempt === maxRetries) {
        throw error; // 最終試行で失敗
      }
      
      // リトライ前に待機
      Utilities.sleep(CONFIG.RETRY_WAIT * attempt);
    }
  }
}

// データ保存(バッチ処理)
function saveData(taskName, data) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Data');
  
  const rows = data.map(item => [
    new Date(),
    taskName,
    item.value1,
    item.value2,
    item.value3
  ]);
  
  // バッチ書き込み
  const lastRow = sheet.getLastRow();
  sheet.getRange(lastRow + 1, 1, rows.length, 5)
    .setValues(rows);
}

// タスク一覧取得
function getTasks() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Tasks');
  
  const data = sheet.getDataRange().getValues();
  const headers = data[0];
  
  return data.slice(1).map(row => ({
    name: row[0],
    url: row[1],
    enabled: row[2]
  })).filter(task => task.enabled);
}

トリガー設定

function setupTriggers() {
  // 既存のトリガーを削除
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => ScriptApp.deleteTrigger(trigger));
  
  // 6時間ごとに実行
  ScriptApp.newTrigger('runBatch')
    .timeBased()
    .everyHours(6)
    .create();
  
  // エラー監視(日次)
  ScriptApp.newTrigger('checkErrors')
    .timeBased()
    .everyDays(1)
    .atHour(9)
    .create();
}

function checkErrors() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Logs');
  
  const logs = sheet.getDataRange().getValues();
  const errors = logs.filter(row => row[1] === 'ERROR');
  
  if (errors.length > 10) {
    // 1日で10件以上エラーが出たらアラート
    MailApp.sendEmail({
      to: 'your@email.com',
      subject: 'Apps Script エラー多発',
      body: `過去24時間で${errors.length}件のエラーが発生しました。`
    });
  }
}

パフォーマンス最適化

1. キャッシュの活用

const cache = CacheService.getScriptCache();

function getCachedData(key, fetchFunction, ttl = 300) {
  // キャッシュチェック
  const cached = cache.get(key);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // キャッシュミス:データ取得
  const data = fetchFunction();
  
  // キャッシュに保存(TTL秒)
  cache.put(key, JSON.stringify(data), ttl);
  
  return data;
}

// 使用例
function getTaskList() {
  return getCachedData('tasks', () => {
    const sheet = SpreadsheetApp.getActiveSpreadsheet()
      .getSheetByName('Tasks');
    return sheet.getDataRange().getValues();
  }, 3600); // 1時間キャッシュ
}

2. 並列処理(疑似的)

Apps Scriptは真の並列処理をサポートしていませんが、複数のトリガーで疑似的に実現できます。

function setupParallelTriggers() {
  // Batch 1: 0分に実行
  ScriptApp.newTrigger('processBatch1')
    .timeBased()
    .everyHours(6)
    .create();
  
  // Batch 2: 2分後に実行
  ScriptApp.newTrigger('processBatch2')
    .timeBased()
    .everyHours(6)
    .create();
  
  // Batch 3: 4分後に実行
  ScriptApp.newTrigger('processBatch3')
    .timeBased()
    .everyHours(6)
    .create();
}

3. データ圧縮

// JSON圧縮してProperties Serviceに保存
function saveCompressedData(key, data) {
  const compressed = Utilities.gzip(
    Utilities.newBlob(JSON.stringify(data))
  );
  
  PropertiesService.getScriptProperties()
    .setProperty(key, Utilities.base64Encode(compressed.getBytes()));
}

function loadCompressedData(key) {
  const encoded = PropertiesService.getScriptProperties()
    .getProperty(key);
  
  if (!encoded) return null;
  
  const compressed = Utilities.base64Decode(encoded);
  const decompressed = Utilities.ungzip(
    Utilities.newBlob(compressed)
  );
  
  return JSON.parse(decompressed.getDataAsString());
}

監視とアラート

1. 稼働監視

function healthCheck() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Data');
  
  const lastRow = sheet.getLastRow();
  const lastUpdate = sheet.getRange(lastRow, 1).getValue();
  
  const now = new Date();
  const diff = now - new Date(lastUpdate);
  const hoursSinceUpdate = diff / (1000 * 60 * 60);
  
  if (hoursSinceUpdate > 7) {
    // 7時間以上更新がない(6時間周期+1時間マージン)
    MailApp.sendEmail({
      to: 'your@email.com',
      subject: 'バッチ処理停止の疑い',
      body: `最終更新: ${lastUpdate}\n現在時刻: ${now}`
    });
  }
}

2. パフォーマンス計測

function measurePerformance(fn, name) {
  const start = Date.now();
  
  try {
    const result = fn();
    const duration = Date.now() - start;
    
    log('PERF', `${name} completed`, { 
      duration_ms: duration 
    });
    
    return result;
  } catch (error) {
    const duration = Date.now() - start;
    
    log('PERF', `${name} failed`, { 
      duration_ms: duration,
      error: error.message 
    });
    
    throw error;
  }
}

// 使用例
function runBatchWithMetrics() {
  measurePerformance(() => {
    runBatch();
  }, 'runBatch');
}

デプロイ戦略

1. 環境分離

// 開発環境と本番環境を分離
function getConfig() {
  const scriptId = ScriptApp.getScriptId();
  
  if (scriptId === 'PRODUCTION_SCRIPT_ID') {
    return {
      env: 'production',
      sheetId: 'PRODUCTION_SHEET_ID',
      errorEmail: 'prod-alerts@example.com'
    };
  } else {
    return {
      env: 'development',
      sheetId: 'DEV_SHEET_ID',
      errorEmail: 'dev@example.com'
    };
  }
}

2. バージョン管理

clasp(Command Line Apps Script Projects)を使用:

# インストール
npm install -g @google/clasp

# ログイン
clasp login

# 初期化
clasp create --title "My Project"

# プッシュ
clasp push

# デプロイ
clasp deploy

まとめ

Apps Scriptで本番システムを作るコツ:

  1. ✅ 制約を理解する(6分、20トリガー等)
  2. ✅ 状態をSpreadsheetで管理
  3. ✅ バッチ処理で高速化
  4. ✅ エラーハンドリングを徹底
  5. ✅ ロギングと監視を実装

この設計で:

  • 15タスク × 6時間ごとの処理を、6分の制限内で回せる
  • コスト0円/月(無料枠の範囲)

次のステップ:

  • clasp導入
  • ユニットテスト
  • CI/CD構築

Apps Scriptは「おもちゃ」じゃない。設計次第で本番運用できます。

参考リンク

著者について

横浜でSEOコンサルタント・データアナリストをしています。Google Apps Scriptで本番レベルのシステムを複数運用中。

プロジェクト:

  • BLST VPN Navi - VPN各社の料金を公式サイトから毎日取得して公開

Twitter: @takechiyo0210

Discussion