Futuristic Imagination LLC 代表の佐藤です。私は現在、AIオウンドメディアを1人で11サイト運用しており、Next.jsとGemini API、Vercel Cronを組み合わせた自動記事生成パイプラインを日々稼働させています。このシステムは毎日動くため、その成否をいかに効率的に把握するかが運用上非常に重要になります。
手動でVercelのログを確認するのは非効率ですし、何か問題が起きたときに即座に気づけないリスクがあります。そこで私は、Vercel Cronの実行結果をDiscord Webhookで即時通知する仕組みを構築しました。
この記事では、私が実際にどのように実装したか、その具体的なパターンとコード例を交えて解説します。
この記事でわかること
- Vercel Cronの実行結果(成功・失敗)をDiscordに自動通知する方法
- Next.jsのAPIルートとサーバーレス関数を連携させる実践的なパターン
- TypeScriptを用いた堅牢なDiscord Webhook通知の実装例
- エラーハンドリングとログ記録の重要性
- 私がなぜこのシステムを構築したか、その背景にある「自動化への執着」
なぜVercel Cronの通知が必要なのか?
私が運用している11サイトのAIオウンドメディアでは、毎日特定の時間にVercel Cronが発火し、以下の処理を実行しています。
- Gemini APIによる新規記事コンテンツの生成
- 生成された記事のデータベース(RDBまたはNoSQL)への保存
- Next.jsのISR(Incremental Static Regeneration)トリガーによるサイト更新
- SNS(X, LinkedIn, Qiita, Zennなど)への自動投稿
これらの処理は多段階にわたるため、どこかでエラーが発生すると、その日の記事生成やSNS投稿が滞ってしまいます。Vercelのダッシュボードを見に行けばログは確認できますが、11サイト分を毎日チェックするのは手間がかかりますし、リアルタイム性がありません。
「問題が発生したら即座に知りたい」「成功したことも確認して安心したい」という思いから、Discord Webhook通知の実装に至りました。
システム構成の概要
私が実装したシステムは、主に以下の要素で構成されています。
- Vercel Cron: 定期的にNext.jsのAPIルートを呼び出すトリガー
- Next.js APIルート: Cronからのリクエストを受け取り、実際の処理を実行
- Discord Webhook: 処理結果をDiscordチャンネルに送信
概念図は以下のようになります。
+----------------+ +-------------------+ +--------------------+
| | | | | |
| Vercel Cron +------>+ Next.js API Route +------>+ Discord Webhook |
| (定期実行) | | (処理実行&通知) | | (Discordチャンネルへ) |
| | | | | |
+----------------+ +-------------------+ +--------------------+
Discord Webhookの設定
まずはDiscord側でWebhook URLを取得する必要があります。
- Discordサーバーを作成または選択: 通知を受け取りたいサーバーを選びます。
- チャンネルを選択: 通知を送りたいテキストチャンネルを選びます。
- チャンネル設定を開く: 歯車アイコンをクリックします。
- 連携タブを選択: 左メニューから「連携」を選びます。
- ウェブフックを作成: 「ウェブフックを作成」ボタンをクリックします。
-
設定をカスタマイズ:
- 名前:通知の送信元となる名前(例:
AIメディア Cron Bot) - チャンネル:通知を送るチャンネル
- アイコン:Botのアイコン画像(任意)
- 名前:通知の送信元となる名前(例:
- Webhook URLをコピー: 表示されたURLをコピーしておきます。これは後でNext.jsのコードに設定します。
Next.js APIルートの実装
次に、Vercel Cronが呼び出すNext.jsのAPIルートを作成します。このAPIルート内で、実際のビジネスロジックを実行し、その成否に応じてDiscordに通知を送ります。
ここでは pages/api/cron/daily-job.ts というファイルを作成する例で説明します。
// pages/api/cron/daily-job.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
// 環境変数からDiscord Webhook URLを取得
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
// Cron Jobが呼び出す実際のビジネスロジック(ダミー)
async function runDailyJob(): Promise<string> {
// ここにGemini API呼び出し、DB保存、ISRトリガーなどの処理を記述
console.log('Daily job started...');
// 例: 処理が成功した場合
if (Math.random() > 0.2) { // 80%の確率で成功
console.log('Daily job completed successfully!');
// 実際にここで外部API呼び出しやDB操作などを行う
// await someExternalApiService.callGemini();
// await someDbService.saveArticle();
// await someVercelService.triggerISR();
return 'Daily job completed successfully.';
} else { // 20%の確率で失敗
throw new Error('Failed to fetch data from external API.');
}
}
// Discordに通知を送信する関数
async function sendDiscordNotification(
title: string,
description: string,
color: number, // Discord Embedの色 (RRGGBBの16進数を10進数に変換)
) {
if (!DISCORD_WEBHOOK_URL) {
console.error('DISCORD_WEBHOOK_URL is not set.');
return;
}
try {
await axios.post(DISCORD_WEBHOOK_URL, {
embeds: [
{
title: title,
description: description,
color: color,
timestamp: new Date().toISOString(),
footer: {
text: 'Futuristic Imagination LLC',
icon_url: 'https://www.futuristicimagination.co.jp/logo.png', // 会社のロゴなど
},
},
],
username: 'AIメディア Cron Bot', // Webhookで設定した名前を上書きすることも可能
avatar_url: 'https://www.futuristicimagination.co.jp/favicon.ico', // Botのアイコン
});
console.log('Discord notification sent successfully.');
} catch (error) {
console.error('Failed to send Discord notification:', error);
}
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Vercel Cronからのリクエストであることを確認
// Vercel CronはデフォルトでAuthorizationヘッダーにトークンを付与するが、
// 環境変数と照合してより厳密にチェックすることも可能
if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) {
// 開発環境では認証をスキップするか、ダミーのCRON_SECRETを設定
if (process.env.NODE_ENV !== 'development' || !process.env.CRON_SECRET) {
// 開発環境でもCRON_SECRETを定義しない場合はスキップ
console.warn('CRON_SECRET is not set or authorization header is missing/invalid.');
// productionでは401を返す
if (process.env.NODE_ENV === 'production') {
return res.status(401).json({ message: 'Unauthorized' });
}
} else if (process.env.NODE_ENV === 'production') {
// 本番環境でシークレットが一致しない場合
return res.status(401).json({ message: 'Unauthorized' });
}
}
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method Not Allowed' });
}
try {
const result = await runDailyJob();
// 成功通知
await sendDiscordNotification(
'✅ Daily Cron Job Succeeded',
`メディア運用Cronジョブが正常に完了しました。\n\n**詳細:**\n${result}`,
3066993, // 緑色 (R:46 G:204 B:113 -> #2ECC71)
);
res.status(200).json({ message: 'Daily job executed and notification sent.' });
} catch (error: any) {
console.error('Daily job failed:', error);
// 失敗通知
await sendDiscordNotification(
'❌ Daily Cron Job Failed',
`メディア運用Cronジョブでエラーが発生しました。\n\n**エラーメッセージ:**\n\`\`\`\n${error.message}\n\`\`\`\n\n詳細ログを確認してください。`,
15158332, // 赤色 (R:231 G:76 B:60 -> #E74C3C)
);
res.status(500).json({ message: 'Daily job failed', error: error.message });
}
}
コードのポイント
-
環境変数
DISCORD_WEBHOOK_URL: Discordで取得したWebhook URLを設定します。Vercelのプロジェクト設定で環境変数として追加してください。 -
runDailyJob()関数: ここが実際のビジネスロジックを実行する部分です。Gemini APIの呼び出し、データベース操作、ISRトリガーなどが含まれます。Promise<string>を返すことで、成功時のメッセージを通知に含めることができます。 -
sendDiscordNotification()関数: Discord WebhookにPOSTリクエストを送る共通関数です。-
embedsを使うことで、より見栄えの良い通知が可能です。title,description, ``color,timestamp`, `footer`などを設定できます。 -
colorはRRGGBBの16進数を10進数に変換した値を使います。成功は緑、失敗は赤など、視覚的に分かりやすく設定しましょう。 -
usernameやavatar_urlを設定することで、Webhookで設定したデフォルト値ではなく、コード側でカスタマイズしたBot名とアイコンで通知できます。
-
-
エラーハンドリング:
try...catchブロックでrunDailyJob()の実行を囲み、エラーが発生した場合はcatchブロックでDiscordに失敗通知を送ります。エラーメッセージを含めることで、迅速な原因特定に役立ちます。 -
Vercel Cronの認証: Vercel Cronは
AuthorizationヘッダーにBearerトークンを付与してリクエストを送ってきます。process.env.CRON_SECRETと照合することで、外部からの不正なAPI呼び出しを防ぎます。開発環境でのテスト用に条件分岐を入れると便利です。
.env.local の設定例
DISCORD_WEBHOOK_URL="YOUR_DISCORD_WEBHOOK_URL_HERE"
CRON_SECRET="YOUR_STRONG_SECRET_HERE" # Vercel Cron設定と一致させる
これらの環境変数は、Vercelのプロジェクト設定にも追加することを忘れないでください。
Vercel Cron の設定
最後に、Vercelプロジェクトの vercel.json にCron設定を追加します。
// vercel.json
{
"crons": [
{
"path": "/api/cron/daily-job",
"schedule": "0 0 * * *" // 毎日UTC午前0時(日本時間午前9時)に実行
}
]
}
-
path: 先ほど作成したNext.js APIルートのパスを指定します。 -
schedule: Cron形式で実行スケジュールを設定します。上記の例では毎日午前0時(UTC)に実行されます。VercelのCronドキュメントを参照して、適切なスケジュールを設定してください。
重要: Vercel Cronのセキュリティ強化のため、vercel.json に設定するCronジョブには、VercelのUIで指定するシークレットトークンを環境変数 CRON_SECRET として設定する必要があります。これにより、Authorization ヘッダーのトークンが CRON_SECRET と一致しない限り、Cronジョブが実行されないように保護されます。
実際に動かしてみて分かったこと(失敗談と改善)
このシステムは私の11サイトで実際に動いていますが、実装初期にはいくつかの試行錯誤がありました。
- 通知が多すぎる問題: 最初はCronの実行たびに詳細なログを通知していたのですが、成功時まで詳細ログをDiscordに流すと、逆に重要なエラー通知が埋もれてしまうことがありました。そこで、成功時は簡潔に、失敗時はエラーメッセージとスタックトレースの一部を強調表示するように調整しました。
- Webhook URLの管理: 複数のサイトで同じWebhook URLを使うと、どのサイトからの通知か分かりにくくなります。プロジェクトごとに異なるDiscordチャンネルを用意するか、通知内容にサイト名を明記するなどの工夫が必要だと感じました。私は、通知メッセージにどのメディアに関するCronか分かるように明記しています。
-
タイムゾーンの罠:
scheduleの設定はUTCベースです。日本時間と勘違いして設定すると、意図しない時間に実行されてしまいます。私の場合は毎日日本時間の午前9時に記事を公開したいので、UTCの午前0時に設定しています。 -
CRON_SECRETの重要性: 開発中にCRON_SECRETを設定せずにローカルでテストしていたら、本番デプロイ後にうまく動かない、という初歩的なミスを踏みました。Vercel Cronのセキュリティ対策はしっかり理解して設定すべきです。
これらの経験から、「ログは詳細に、通知は要約して」、そして**「環境変数は正しく、厳密に」**という運用ポリシーを確立しました。session_log.md に失敗談を記録する習慣も、このようなトラブルシューティングに役立っています。
まとめ
Vercel CronとDiscord Webhookを連携させることで、定期実行されるAI自動化ジョブの監視が格段に楽になります。私が1人で11サイトを運用できるのも、こうした自動化と監視の仕組みが盤石だからです。
- 即時性: 問題発生時にリアルタイムで通知を受け取れるため、迅速な対応が可能。
- 効率性: 手動でのログ確認が不要になり、運用負荷を大幅に軽減。
- 信頼性: システムの稼働状況が可視化され、安心して自動化を進められる。
このパターンは、AIを活用したオウンドメディアの自動運用だけでなく、日次バッチ処理、データ同期、レポート生成など、Vercel Cronを利用するあらゆるシーンで応用できます。
Futuristic Imagination LLCでは、AIを活用したオウンドメディアの構築から運用自動化、既存WordPressサイトのNext.jsへの移行、Gemini APIを活用したコンテンツ生成パイプラインの構築など、多岐にわたる受託開発を行っています。今回ご紹介したような「実際に自分で動かしている」実績に基づいたシステム構築にご興味があれば、ぜひお気軽にお問い合わせください。
→ Futuristic Imagination LLC サービスページ