Vendure Fly.io + Upstash コスト最適化ガイド¶
概要¶
Fly.io + Upstash での Vendure 運用におけるコスト最適化戦略と具体的な実装方法について説明します。
現在のコスト構造分析¶
月額コスト内訳(本番環境)¶
基本構成コスト:
Vendure Server (2 instances):
- VM: shared-cpu-1x (1GB RAM) × 2
- 月額: $12 ($6 × 2)
PostgreSQL Database:
- Instance: shared-cpu-1x (1GB RAM, 10GB Storage)
- 月額: $15
Upstash Redis:
- Plan: Fixed $3 plan (3GB Max)
- 月額: $3
Volumes:
- vendure_data: 5GB
- 月額: $1.50
Network & Storage:
- 帯域幅: 月100GB程度
- 月額: $2
合計基本コスト: $33.50/月 (約 5,000円)
追加コスト(トラフィック増加時):
Auto-scaling (ピーク時):
- 追加インスタンス 2台
- 月額: $12 (週末・繁忙期のみ)
ストレージ拡張:
- Volume拡張: 10GB → 20GB
- 追加月額: $1.50
予想最大コスト: $47/月 (約 7,000円)
コスト比較(他プラットフォーム)¶
AWS 構成(同等性能):
ECS Fargate: 2 tasks × $50 = $100
RDS PostgreSQL: t3.micro = $25
ElastiCache Redis: t3.micro = $20
ALB: $22
S3 + CloudFront: $10
合計: $177/月 (約 27,000円)
コスト削減効果: 73% 削減 ($144/月 節約)
Google Cloud 構成(同等性能):
Cloud Run: 2 instances = $40
Cloud SQL: db-f1-micro = $35
Memorystore Redis: 1GB = $30
Load Balancer: $18
Cloud Storage + CDN: $8
合計: $131/月 (約 20,000円)
コスト削減効果: 64% 削減 ($84/月 節約)
コスト最適化戦略¶
1. オートスケーリング最適化¶
時間帯別スケーリング設定¶
// auto-scaling-scheduler.ts
export class AutoScalingScheduler {
private readonly schedules = {
// 営業時間外(深夜2-6時)は最小構成
nightTime: {
hours: [2, 3, 4, 5],
minInstances: 1,
maxInstances: 1,
},
// 営業時間(9-18時)は標準構成
businessHours: {
hours: [9, 10, 11, 12, 13, 14, 15, 16, 17],
minInstances: 2,
maxInstances: 4,
},
// ピーク時間(昼休み、夕方)は拡張
peakHours: {
hours: [12, 17, 18],
minInstances: 2,
maxInstances: 6,
},
// 週末は最小構成
weekend: {
days: [0, 6], // 日曜、土曜
minInstances: 1,
maxInstances: 2,
},
};
async adjustScaling() {
const now = new Date();
const hour = now.getHours();
const day = now.getDay();
let config = this.schedules.businessHours; // デフォルト
// 週末チェック
if (this.schedules.weekend.days.includes(day)) {
config = this.schedules.weekend;
}
// 深夜時間チェック
else if (this.schedules.nightTime.hours.includes(hour)) {
config = this.schedules.nightTime;
}
// ピーク時間チェック
else if (this.schedules.peakHours.hours.includes(hour)) {
config = this.schedules.peakHours;
}
await this.updateFlyScaling(config.minInstances, config.maxInstances);
}
private async updateFlyScaling(min: number, max: number) {
try {
// Fly.io CLI を使用してスケーリング設定更新
const command = `flyctl scale count ${min}:${max} -a ritsubi-vendure`;
await this.executeCommand(command);
console.log(`Scaling updated: min=${min}, max=${max}`);
} catch (error) {
console.error('Failed to update scaling:', error);
}
}
private async executeCommand(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve(stdout);
}
});
});
}
}
// Cron job として実行
// 毎時0分に実行: 0 * * * *
const scheduler = new AutoScalingScheduler();
setInterval(() => scheduler.adjustScaling(), 60 * 60 * 1000); // 1時間ごと
リクエストベースオートスケーリング¶
# fly.toml - 高度なオートスケーリング設定
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true # 未使用時自動停止
auto_start_machines = true # 需要時自動起動
min_machines_running = 1 # 常時稼働最小数
[http_service.concurrency]
type = "requests"
hard_limit = 250 # リクエスト上限(メモリ1GBに最適化)
soft_limit = 200 # ソフト制限
# ヘルスチェック最適化
[[http_service.checks]]
grace_period = "10s"
interval = "30s"
method = "GET"
timeout = "5s"
path = "/health"
# 自動停止設定
[http_service.auto_stop_machines]
enabled = true
min_machines_running = 1
2. リソース使用量最適化¶
メモリ使用量最適化¶
// memory-optimization.ts
export class MemoryOptimizer {
private readonly gcInterval = 5 * 60 * 1000; // 5分ごと
constructor() {
this.startMemoryMonitoring();
}
private startMemoryMonitoring() {
setInterval(() => {
this.checkMemoryUsage();
}, this.gcInterval);
}
private checkMemoryUsage() {
const usage = process.memoryUsage();
const heapUsedMB = usage.heapUsed / 1024 / 1024;
const heapTotalMB = usage.heapTotal / 1024 / 1024;
const rssMB = usage.rss / 1024 / 1024;
console.log(
`Memory usage: RSS=${rssMB.toFixed(2)}MB, Heap=${heapUsedMB.toFixed(2)}/${heapTotalMB.toFixed(2)}MB`,
);
// メモリ使用量が80%を超えた場合、ガベージコレクション実行
if (heapUsedMB / heapTotalMB > 0.8) {
console.log('Triggering garbage collection due to high memory usage');
if (global.gc) {
global.gc();
}
}
// メモリ使用量が1GB近くになった場合、アラート
if (rssMB > 900) {
console.warn('High memory usage detected, consider scaling up');
// メトリクス送信
memoryUsageAlert.inc();
}
}
// キャッシュサイズ制限
optimizeCacheSize() {
const maxCacheSize = 100 * 1024 * 1024; // 100MB
// Redis キャッシュクリーンアップ
this.cleanupRedisCache();
// Node.js インメモリキャッシュクリーンアップ
this.cleanupInMemoryCache();
}
private async cleanupRedisCache() {
// 使用頻度の低いキーを削除
const redis = new Redis(process.env.UPSTASH_REDIS_URL!);
// 1週間以上アクセスされていないキーを削除
const keys = await redis.keys('cache:*');
for (const key of keys) {
const lastAccess = await redis.object('idletime', key);
if (lastAccess && lastAccess > 7 * 24 * 60 * 60) {
// 7日
await redis.del(key);
}
}
}
private cleanupInMemoryCache() {
// アプリケーション内キャッシュのクリーンアップ
// 実装依存
}
}
データベースクエリ最適化¶
// database-optimization.ts
export class DatabaseOptimizer {
private slowQueryThreshold = 1000; // 1秒
// スロークエリ監視
trackQuery(sql: string, parameters: any[], duration: number) {
if (duration > this.slowQueryThreshold) {
console.warn(`Slow query detected (${duration}ms):`, {
sql: sql.substring(0, 200),
duration,
});
// スロークエリメトリクス
slowQueriesTotal.inc();
queryDurationHistogram.observe(duration / 1000);
}
}
// 接続プール最適化
optimizeConnectionPool() {
return {
type: 'postgres',
host: process.env.DATABASE_HOST,
port: 5432,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
// 最適化された接続プール設定
poolSize: 5, // 1GBメモリに最適化
connectionTimeoutMillis: 5000,
idleTimeoutMillis: 10000,
maxQueryExecutionTime: 30000,
// キャッシュ設定
cache: {
type: 'redis',
options: {
host: process.env.UPSTASH_REDIS_HOST,
port: 6379,
password: process.env.UPSTASH_REDIS_PASSWORD,
keyPrefix: 'typeorm:',
ttl: 60, // 1分間キャッシュ
},
},
// ログ設定(本番では最小限に)
logging:
process.env.NODE_ENV === 'production' ? ['error'] : ['query', 'error'],
maxQueryExecutionTime: 1000, // 1秒以上のクエリをログ出力
};
}
// インデックス最適化提案
async analyzePerformance() {
const slowQueries = await this.getSlowQueries();
const missingIndexes = await this.suggestIndexes(slowQueries);
console.log('Performance analysis:', {
slowQueriesCount: slowQueries.length,
suggestedIndexes: missingIndexes,
});
return {
slowQueries,
suggestedIndexes,
};
}
private async getSlowQueries(): Promise<any[]> {
// PostgreSQL のスロークエリログ分析
// 実装は環境依存
return [];
}
private async suggestIndexes(slowQueries: any[]): Promise<string[]> {
// クエリパターン分析に基づくインデックス提案
// 実装は環境依存
return [];
}
}
3. ストレージコスト最適化¶
ファイルストレージ戦略¶
// storage-optimization.ts
export class StorageOptimizer {
// 画像最適化・圧縮
async optimizeImages() {
const images = await this.getUnoptimizedImages();
for (const image of images) {
try {
const optimized = await this.compressImage(image);
await this.updateImageRecord(image.id, optimized);
console.log(
`Image optimized: ${image.id}, size reduced by ${this.calculateSavings(image, optimized)}%`,
);
} catch (error) {
console.error(`Failed to optimize image ${image.id}:`, error);
}
}
}
// 古いファイル削除
async cleanupOldFiles() {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 90); // 90日前
// 一時ファイルの削除
await this.deleteTempFiles(cutoffDate);
// 未使用画像の削除
await this.deleteUnusedImages(cutoffDate);
// ログファイルの削除
await this.cleanupOldLogs(cutoffDate);
}
// CDNキャッシュ最適化
optimizeCDNSettings() {
return {
cloudflare: {
// 静的アセットの長期キャッシュ
staticAssets: {
cacheTtl: 31536000, // 1年
browserTtl: 31536000,
patterns: ['*.jpg', '*.png', '*.css', '*.js'],
},
// 商品画像の中期キャッシュ
productImages: {
cacheTtl: 604800, // 1週間
browserTtl: 604800,
patterns: ['/assets/products/*'],
},
// APIレスポンスの短期キャッシュ
apiResponses: {
cacheTtl: 300, // 5分
browserTtl: 0, // ブラウザキャッシュなし
patterns: ['/shop-api/*'],
},
},
};
}
private async compressImage(image: any): Promise<any> {
// 画像圧縮ロジック(Sharp等を使用)
// WebP形式への変換、品質調整など
return image;
}
private calculateSavings(original: any, optimized: any): number {
return Math.round(((original.size - optimized.size) / original.size) * 100);
}
private async deleteTempFiles(cutoffDate: Date): Promise<void> {
// 一時ファイル削除ロジック
}
private async deleteUnusedImages(cutoffDate: Date): Promise<void> {
// 未参照画像削除ロジック
}
private async cleanupOldLogs(cutoffDate: Date): Promise<void> {
// 古いログファイル削除
}
}
4. 開発・ステージング環境のコスト削減¶
環境別リソース設定¶
# 開発環境(fly.dev.toml)
app = "ritsubi-vendure-dev"
primary_region = "nrt"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 512 # 本番の半分
[http_service]
min_machines_running = 1
auto_stop_machines = true
auto_start_machines = true
[http_service.concurrency]
hard_limit = 100 # 本番の半分
soft_limit = 80
# ステージング環境(fly.staging.toml)
app = "ritsubi-vendure-staging"
primary_region = "nrt"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 512
[http_service]
min_machines_running = 0 # 使用時のみ起動
auto_stop_machines = true
auto_start_machines = true
開発環境コスト管理¶
// dev-environment-manager.ts
export class DevEnvironmentManager {
// 非営業時間に開発環境を自動停止
async scheduleDevEnvironmentShutdown() {
const schedule = {
// 平日19時に停止
weekdayShutdown: '0 19 * * 1-5',
// 平日9時に起動
weekdayStartup: '0 9 * * 1-5',
// 土日は停止維持
weekendShutdown: '0 19 * * 5',
weekendStartup: '0 9 * * 1',
};
// cron job として実行
this.scheduleCronJob(schedule.weekdayShutdown, () =>
this.stopDevEnvironment(),
);
this.scheduleCronJob(schedule.weekdayStartup, () =>
this.startDevEnvironment(),
);
}
private async stopDevEnvironment() {
try {
await this.executeCommand('flyctl scale count 0 -a ritsubi-vendure-dev');
await this.executeCommand(
'flyctl scale count 0 -a ritsubi-vendure-staging',
);
console.log('Development environments stopped for cost savings');
} catch (error) {
console.error('Failed to stop dev environments:', error);
}
}
private async startDevEnvironment() {
try {
await this.executeCommand('flyctl scale count 1 -a ritsubi-vendure-dev');
await this.executeCommand(
'flyctl scale count 1 -a ritsubi-vendure-staging',
);
console.log('Development environments started');
} catch (error) {
console.error('Failed to start dev environments:', error);
}
}
private scheduleCronJob(pattern: string, job: () => void) {
// cron job の実装
}
private async executeCommand(command: string): Promise<void> {
// コマンド実行の実装
}
}
5. コスト監視・アラート¶
コスト追跡システム¶
// cost-tracking.ts
export class CostTracker {
private readonly costThresholds = {
daily: 2, // $2/日
weekly: 12, // $12/週
monthly: 50, // $50/月
};
async trackDailyCosts() {
const costs = await this.getFlyIoCosts();
const today = new Date().toISOString().split('T')[0];
// コスト記録
await this.recordCosts(today, costs);
// 閾値チェック
await this.checkThresholds(costs);
return costs;
}
private async getFlyIoCosts(): Promise<CostBreakdown> {
// Fly.io API からコスト情報取得
// 実際のAPIコールの実装が必要
return {
compute: 0,
storage: 0,
network: 0,
postgres: 0,
redis: 0,
total: 0,
};
}
private async checkThresholds(costs: CostBreakdown) {
if (costs.total > this.costThresholds.daily) {
await this.sendCostAlert({
type: 'daily_threshold_exceeded',
actual: costs.total,
threshold: this.costThresholds.daily,
breakdown: costs,
});
}
}
private async sendCostAlert(alert: CostAlert) {
const message = `
🚨 Cost Alert: ${alert.type}
Actual: $${alert.actual.toFixed(2)}
Threshold: $${alert.threshold.toFixed(2)}
Breakdown:
- Compute: $${alert.breakdown.compute.toFixed(2)}
- Storage: $${alert.breakdown.storage.toFixed(2)}
- Network: $${alert.breakdown.network.toFixed(2)}
- PostgreSQL: $${alert.breakdown.postgres.toFixed(2)}
- Redis: $${alert.breakdown.redis.toFixed(2)}
Total: $${alert.breakdown.total.toFixed(2)}
`;
// Slack通知
await this.sendSlackNotification(message);
// メール通知
await this.sendEmailNotification(alert);
}
private async sendSlackNotification(message: string) {
// Slack Webhook 実装
}
private async sendEmailNotification(alert: CostAlert) {
// メール通知実装
}
}
interface CostBreakdown {
compute: number;
storage: number;
network: number;
postgres: number;
redis: number;
total: number;
}
interface CostAlert {
type: string;
actual: number;
threshold: number;
breakdown: CostBreakdown;
}
6. リザーブドインスタンス・長期契約活用¶
年間契約によるコスト削減¶
長期契約オプション(将来的に検討):
Fly.io Volume Discounts:
- 年間契約: 15% 割引
- 2年契約: 25% 割引
- 現在コスト: $47/月 → 年間契約: $40/月
Upstash 年間契約:
- 年間前払い: 20% 割引
- 現在コスト: $3/月 → 年間契約: $29/年 ($2.4/月)
PostgreSQL 最適化:
- 必要に応じて専用インスタンスに変更
- パフォーマンス向上 + コスト予測可能性
7. コスト最適化自動化¶
自動最適化スクリプト¶
// cost-optimizer.ts
export class AutoCostOptimizer {
async runDailyOptimization() {
console.log('Starting daily cost optimization...');
// 1. リソース使用量分析
const usage = await this.analyzeResourceUsage();
// 2. 不要なリソースのクリーンアップ
await this.cleanupUnusedResources();
// 3. スケーリング設定の最適化
await this.optimizeScaling(usage);
// 4. ストレージ最適化
await this.optimizeStorage();
// 5. コスト予測
const forecast = await this.generateCostForecast();
// 6. レポート生成
await this.generateOptimizationReport(usage, forecast);
console.log('Daily cost optimization completed');
}
private async analyzeResourceUsage(): Promise<ResourceUsage> {
return {
cpu: await this.getCPUUsage(),
memory: await this.getMemoryUsage(),
storage: await this.getStorageUsage(),
network: await this.getNetworkUsage(),
};
}
private async optimizeScaling(usage: ResourceUsage) {
// CPU使用率が30%未満の場合、インスタンス数削減を検討
if (usage.cpu < 0.3) {
await this.suggestScaleDown();
}
// CPU使用率が80%以上の場合、スケールアップを検討
if (usage.cpu > 0.8) {
await this.suggestScaleUp();
}
}
private async cleanupUnusedResources() {
// 未使用ボリュームの削除
await this.cleanupUnusedVolumes();
// 古いマシンイメージの削除
await this.cleanupOldImages();
// 未使用ネットワークリソースの削除
await this.cleanupNetworkResources();
}
private async generateCostForecast(): Promise<CostForecast> {
const currentUsage = await this.getCurrentUsage();
const historicalTrends = await this.getHistoricalTrends();
return {
nextMonth: this.calculateNextMonthCost(currentUsage, historicalTrends),
nextQuarter: this.calculateQuarterlyCost(currentUsage, historicalTrends),
yearEnd: this.calculateYearEndCost(currentUsage, historicalTrends),
};
}
private async generateOptimizationReport(
usage: ResourceUsage,
forecast: CostForecast,
) {
const report = {
date: new Date().toISOString(),
resourceUsage: usage,
costForecast: forecast,
optimizations: await this.getOptimizationSuggestions(),
savings: await this.calculatePotentialSavings(),
};
// レポートをファイルに保存
await this.saveReport(report);
// ステークホルダーに送信
await this.sendReport(report);
}
private async getOptimizationSuggestions(): Promise<string[]> {
const suggestions = [];
// 実際の使用量に基づく提案ロジック
// 例: "Memory usage is consistently below 60%, consider downsizing"
return suggestions;
}
}
interface ResourceUsage {
cpu: number;
memory: number;
storage: number;
network: number;
}
interface CostForecast {
nextMonth: number;
nextQuarter: number;
yearEnd: number;
}
コスト最適化チェックリスト¶
月次レビュー項目¶
インフラストラクチャ:
□ CPU使用率が適切な範囲(30-70%)にあるか □ メモリ使用率が効率的か(60-80%) □
ストレージ使用量が適切か □ 不要なボリュームが残っていないか □
開発環境が適切に停止されているか
アプリケーション:
□ スロークエリが増加していないか □ キャッシュヒット率が適切か(>80%) □
未使用の機能・プラグインがないか □
バックグラウンドジョブが効率的に実行されているか
コスト追跡:
□ 月次コスト目標を達成しているか □ 前月比でコスト増加していないか □
コスト内訳が適切に分析されているか □ ROI(投資収益率)が目標値を超えているか
最適化機会:
□ 新しいコスト削減施策の検討 □ リザーブドインスタンスの活用可能性 □
より効率的なアーキテクチャの検討 □ 外部サービス統合によるコスト削減
緊急時コスト削減手順¶
#!/bin/bash
# emergency-cost-reduction.sh
echo "Emergency cost reduction measures"
# 1. 非本番環境の即座停止
flyctl scale count 0 -a ritsubi-vendure-dev
flyctl scale count 0 -a ritsubi-vendure-staging
# 2. 本番環境の最小構成切り替え
flyctl scale count 1 -a ritsubi-vendure
# 3. 不要なボリューム削除
flyctl volumes list | grep -v "attached" | awk '{print $1}' | xargs -I {} flyctl volumes delete {}
# 4. 一時的なキャッシュクリア(Redis メモリ削減)
redis-cli FLUSHDB
# 5. ログファイル削除
find /app/logs -name "*.log" -delete
echo "Emergency cost reduction completed"
echo "Monitor application health after these changes"
ROI測定とコスト効果分析¶
ROI計算フレームワーク¶
// roi-calculator.ts
export class ROICalculator {
calculateInfrastructureROI(): ROIAnalysis {
const costs = {
// 以前のインフラ(仮想的な AWS 構成)
previousMonthly: 177, // $177/month
// 現在のインフラ(Fly.io + Upstash)
currentMonthly: 47, // $47/month
// 移行コスト(初期投資)
migrationCost: 2000, // $2000 (開発工数含む)
};
const monthlySavings = costs.previousMonthly - costs.currentMonthly; // $130
const annualSavings = monthlySavings * 12; // $1560
const paybackPeriod = costs.migrationCost / monthlySavings; // 15.4ヶ月
const threeYearROI =
((annualSavings * 3 - costs.migrationCost) / costs.migrationCost) * 100; // 134%
return {
monthlySavings,
annualSavings,
paybackPeriod,
threeYearROI,
recommendations: this.generateROIRecommendations(
paybackPeriod,
threeYearROI,
),
};
}
calculateOperationalROI(): OperationalROI {
return {
// 運用工数削減
operationalSavings: {
monthlyHours: 10, // 月10時間の運用工数削減
hourlyRate: 50, // $50/時間
monthlySavings: 500, // $500/月
},
// 可用性向上による収益影響
availabilityImpact: {
uptimeImprovement: 0.99 - 0.97, // 97% → 99%
averageHourlyRevenue: 100, // $100/時間の平均収益
monthlyRevenueProtection: 14.4, // $14.4/月の収益保護
},
// 開発速度向上
developmentEfficiency: {
deploymentTimeReduction: 0.75, // 75%のデプロイ時間短縮
weeklyDeployments: 3, // 週3回のデプロイ
timeValuePerDeployment: 30, // 1デプロイあたり30分の時間価値
},
};
}
private generateROIRecommendations(
paybackPeriod: number,
threeYearROI: number,
): string[] {
const recommendations = [];
if (paybackPeriod < 24) {
recommendations.push(
'Excellent payback period - continue current strategy',
);
} else {
recommendations.push('Consider additional cost optimization measures');
}
if (threeYearROI > 100) {
recommendations.push('Strong 3-year ROI - investment justified');
}
recommendations.push('Monitor monthly costs to ensure continued savings');
recommendations.push(
'Evaluate additional Fly.io features for further optimization',
);
return recommendations;
}
}
interface ROIAnalysis {
monthlySavings: number;
annualSavings: number;
paybackPeriod: number;
threeYearROI: number;
recommendations: string[];
}
interface OperationalROI {
operationalSavings: {
monthlyHours: number;
hourlyRate: number;
monthlySavings: number;
};
availabilityImpact: {
uptimeImprovement: number;
averageHourlyRevenue: number;
monthlyRevenueProtection: number;
};
developmentEfficiency: {
deploymentTimeReduction: number;
weeklyDeployments: number;
timeValuePerDeployment: number;
};
}
文書バージョン: 1.0 作成日: 2025年9月17日 コスト見直し: 月次で実施し、四半期ごとに戦略を更新