Google Apps Scriptで本番レベルのバッチ処理を実装する
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 のデータ更新
15社のVPNのデータを6時間ごとに自動で処理し、サイトに公開するシステムです(速度の数値は実測ではなく編集部の推定値です。料金は各社の公式サイトから取得しています)。
技術スタック:
- Google Apps Script(自動処理)
- Google Spreadsheet(データ保存)
- REST API(データ公開)
運用コスト: 0円/月(Apps Scriptの無料枠の範囲)
本記事の技術をどう使っているか:
- タスク分割 - 15社分の処理を3バッチに分割
- 状態管理 - 最後に処理したIDをSpreadsheetに保存
- バッチ処理 - 全データを一括書き込み(API呼び出しを1回に)
- エラーハンドリング - 3回リトライ + メール通知
- ロギング - すべて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で本番システムを作るコツ:
- ✅ 制約を理解する(6分、20トリガー等)
- ✅ 状態をSpreadsheetで管理
- ✅ バッチ処理で高速化
- ✅ エラーハンドリングを徹底
- ✅ ロギングと監視を実装
この設計で:
- 15タスク × 6時間ごとの処理を、6分の制限内で回せる
- コスト0円/月(無料枠の範囲)
次のステップ:
- clasp導入
- ユニットテスト
- CI/CD構築
Apps Scriptは「おもちゃ」じゃない。設計次第で本番運用できます。
参考リンク
著者について
横浜でSEOコンサルタント・データアナリストをしています。Google Apps Scriptで本番レベルのシステムを複数運用中。
プロジェクト:
- BLST VPN Navi - VPN各社の料金を公式サイトから毎日取得して公開
Twitter: @takechiyo0210
Discussion