
Twilio Functions + Momento で実現する、日ごとの SMS 送信 API 呼び出し制限の導入例
はじめに
Twilio で SMS 送信 API を運用する際、送信回数に制限を設けるのは、予期しない大量送信によるコスト増加や悪意のある利用者による乱用を防ぐのに有効な手段となります。本記事では、Twilio Functions と Momento Cache を組み合わせて、 1 日あたりの SMS 送信回数を制限する低コストなソリューションを紹介します。複雑なインフラストラクチャを必要とせず、Twilio + Momento のサーバーレス環境内で完結する制限機能を実現できます。
Twilio について
Twilio は、SMS、音声通話、ビデオ通話などの通信機能を API として提供するクラウドプラットフォームです。Twilio Functions は、Twilio が提供するサーバーレス実行環境で、Node.js ベースのコードを簡単にデプロイ・実行できます。
Momento について
Momento は、フルマネージドなインメモリキャッシュサービスです。Redis のような機能を提供しながら、サーバー管理やスケーリングの複雑さを排除します。
対象読者
- Twilio を使用した SMS 送信に利用上限を実装したい開発者
- サーバーレス環境での低コストなキャッシュ導入に興味がある方
参考資料
システム構成
- Twilio Functions: SMS 送信の proxy として動作し、送信制限のチェックを行う
- Momento Cache: 日次の送信回数をカウンターとして保存
- Twilio Messaging API: 実際の SMS 送信を実行
事前準備
Twilio アカウントの設定
Twilio Console にログインし、以下の情報を控えておきます。
- Account SID
- Auth Token
- SMS 送信用の電話番号
Momento アカウントの設定
Momento Console でアカウントを作成し、以下の手順を実行します。
-
キャッシュの作成
- Cache 名: 任意 (例:
sms-limit
)
- Cache 名: 任意 (例:
-
API キーの作成
- Fine-grained Access Key を作成
- 作成したキャッシュに対する
readwrite
権限を付与
実装手順
Twilio Functions の作成
-
新しい Service の作成
- Twilio Console の Functions and Assets > Services から新しい Service を作成
-
Dependencies の追加
@gomomento/sdk
を Dependencies に追加
-
Momento Auth Token の保存
Twilio Functions の環境変数は 255 字以内という縛りがあるため、今回は Token を Private の Asset として保存します。- Add + > Add Asset(Static text file) で新しい Asset を作成
- ファイル名を
momento-token
とする - Visibilityを Private とする
- 先ほど作成した Momento API キーを入力
- Deploy All を実行してデプロイを完了
- Add + > Add Asset(Static text file) で新しい Asset を作成
SMS 送信制限機能の実装
日次送信制限をチェックしてから SMS 送信するエンドポイント /send-sms
を作成します。
/send-sms
// /send-sms
const { CacheClient, Configurations, CredentialProvider } = require('@gomomento/sdk');
exports.handler = async function(context, event, callback) {
try {
// Private Asset から Momento Auth Token を読み込み
const openTokenFile = Runtime.getAssets()['/momento-token'].open;
const authToken = openTokenFile().trim();
// Momento クライアントの初期化
const cacheClient = new CacheClient({
configuration: Configurations.Laptop.v1(),
credentialProvider: CredentialProvider.fromString({
authToken: authToken
}),
defaultTtlSeconds: 25 * 60 * 60 // 25時間(日付変更を確実にカバー)
});
const cacheName = 'sms-limit';
// 今日の日付をキーとして使用
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD 形式
const countKey = `sms_count_${today}`;
// 現在の送信回数を取得
const currentCountResponse = await cacheClient.get(cacheName, countKey);
let currentCount = 0;
if (currentCountResponse.type === 'Hit') {
currentCount = parseInt(currentCountResponse.valueString()) || 0;
}
// 送信制限をチェック(今回は1日1件)
const dailyLimit = 1;
if (currentCount >= dailyLimit) {
const response = new Twilio.Response();
response.setStatusCode(429);
response.setBody(JSON.stringify({
success: false,
error: 'Daily SMS limit exceeded',
currentCount: currentCount,
limit: dailyLimit,
resetTime: `${today}T23:59:59Z`
}));
response.appendHeader('Content-Type', 'application/json');
return callback(null, response);
}
// リクエストパラメータの取得
const { to, body: messageBody } = event;
if (!to || !messageBody) {
const response = new Twilio.Response();
response.setStatusCode(400);
response.setBody(JSON.stringify({
success: false,
error: 'Missing required parameters: to, body'
}));
response.appendHeader('Content-Type', 'application/json');
return callback(null, response);
}
// SMS 送信
const client = context.getTwilioClient();
const message = await client.messages.create({
body: messageBody,
from: context.TWILIO_PHONE_NUMBER, // Environment Variables で設定
to: to
});
// 送信成功後、カウンターをインクリメント
await cacheClient.set(cacheName, countKey, (currentCount + 1).toString());
const response = new Twilio.Response();
response.setStatusCode(200);
response.setBody(JSON.stringify({
success: true,
messageSid: message.sid,
currentCount: currentCount + 1,
remainingCount: dailyLimit - (currentCount + 1)
}));
response.appendHeader('Content-Type', 'application/json');
callback(null, response);
} catch (error) {
console.error('Error:', error);
const response = new Twilio.Response();
response.setStatusCode(500);
response.setBody(JSON.stringify({
success: false,
error: error.message
}));
response.appendHeader('Content-Type', 'application/json');
callback(null, response);
}
};
エンドポイントのメニューを開き、 Public とします。
Environment Variables の設定
以下の Environment Variables を設定します。
TWILIO_PHONE_NUMBER
: SMS 送信用の Twilio 電話番号
動作検証
テスト用 Node.js プロジェクトの作成
mkdir sms-limit-test
cd sms-limit-test
npm init -y
npm install axios dotenv
環境変数の設定
.env
ファイルを作成し、以下の内容を設定します。
FUNCTION_ENDPOINT=https://your-service-1234.twil.io/send-sms
TO_NUMBER=+819012345678
テストスクリプトの作成
test-sms-limit.js
ファイルを作成します。
test-sms-limit.js
// test-sms-limit.js
const axios = require('axios');
require('dotenv').config();
const functionUrl = process.env.FUNCTION_ENDPOINT;
async function testWithAxios() {
console.log('=== SMS 送信制限テスト ===\n');
const testParams = {
to: process.env.TO_NUMBER,
body: 'テストメッセージです'
};
console.log('送信パラメータ:', testParams);
console.log('Function URL:', functionUrl);
try {
console.log('1回目の送信テスト...');
const response1 = await axios.post(functionUrl, testParams, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
console.log('✅ 1回目の送信結果:');
console.log('Status:', response1.status);
console.log('Data:', typeof response1.data === 'string' ? JSON.parse(response1.data) : response1.data);
console.log('');
// 少し待機
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('2回目の送信テスト...');
const response2 = await axios.post(functionUrl, testParams, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
console.log('2回目の送信結果:');
console.log('Status:', response2.status);
console.log('Data:', typeof response2.data === 'string' ? JSON.parse(response2.data) : response2.data);
} catch (error) {
if (error.response) {
console.log('HTTP エラー:');
console.log('Status:', error.response.status);
let errorData = error.response.data;
if (typeof errorData === 'string') {
try {
errorData = JSON.parse(errorData);
} catch (e) {
// パースできない場合はそのまま
}
}
console.log('Data:', errorData);
if (error.response.status === 429) {
console.log('✅ 制限エラーが正常に発生');
}
} else {
console.error('❌ 予期しないエラー:', error.message);
}
}
console.log('\n=== テスト完了 ===');
}
if (require.main === module) {
testWithAxios().catch(console.error);
}
テスト実行
node test-sms-limit.js
成功時、次のような出力が得られます。
=== SMS 送信制限テスト ===
送信パラメータ: { to: '+819012345678', body: 'テストメッセージです' }
Function URL: https://sms-limit-1234.twil.io/send-sms
1回目の送信テスト...
✅ 1回目の送信結果:
Status: 200
Data: {
success: true,
messageSid: 'SM********',
currentCount: 1,
remainingCount: 0
}
2回目の送信テスト...
HTTP エラー:
Status: 429
Data: {
success: false,
error: 'Daily SMS limit exceeded',
currentCount: 1,
limit: 1,
resetTime: '2025-07-30T23:59:59Z'
}
✅ 制限エラーが正常に発生
=== テスト完了 ===
まとめと今後の展望
本記事では、Twilio Functions と Momento Cache を組み合わせて、1日あたりの SMS 送信回数を制限するシステムを構築しました。日付ベースのキーと TTL 設定により、外部インフラを必要とせずサーバーレス環境内で完結する制限機能を実現できました。この構成により、低コストで効果的な SMS 送信制限機能を迅速に導入することが可能です。
今後の展望
- 機能拡張: ユーザー別制限、時間別制限、ホワイトリスト機能の追加
- 監視強化: 制限到達時の通知機能や使用状況分析ダッシュボードの構築