Calculate and compare costs across cloud providers Features: - Multi-provider comparison (AWS, GCS, Azure, B2, Wasabi) - Storage tier analysis (15 tiers total) - Monthly/annual cost projections - Savings calculations vs S3 Standard baseline - Tiered lifecycle strategy recommendations - JSON output for reporting/automation Providers & Tiers: AWS S3: Standard, IA, Glacier Instant/Flexible, Deep Archive GCS: Standard, Nearline, Coldline, Archive Azure: Hot, Cool, Archive Backblaze B2: Affordable alternative Wasabi: No egress fees Perfect for: - Budget planning - Provider selection - Lifecycle policy optimization - Cost reduction identification - Compliance storage planning Example savings: S3 Deep Archive saves ~96% vs S3 Standard
397 lines
13 KiB
Go
397 lines
13 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"dbbackup/internal/catalog"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
costDatabase string
|
|
costFormat string
|
|
costRegion string
|
|
costProvider string
|
|
costDays int
|
|
)
|
|
|
|
// costCmd analyzes backup storage costs
|
|
var costCmd = &cobra.Command{
|
|
Use: "cost",
|
|
Short: "Analyze cloud storage costs for backups",
|
|
Long: `Calculate and compare cloud storage costs for your backups.
|
|
|
|
Analyzes storage costs across providers:
|
|
- AWS S3 (Standard, IA, Glacier, Deep Archive)
|
|
- Google Cloud Storage (Standard, Nearline, Coldline, Archive)
|
|
- Azure Blob Storage (Hot, Cool, Archive)
|
|
- Backblaze B2
|
|
- Wasabi
|
|
|
|
Pricing is based on standard rates and may vary by region.
|
|
|
|
Examples:
|
|
# Analyze all backups
|
|
dbbackup cost analyze
|
|
|
|
# Specific database
|
|
dbbackup cost analyze --database mydb
|
|
|
|
# Compare providers for 90 days
|
|
dbbackup cost analyze --days 90 --format table
|
|
|
|
# Estimate for specific region
|
|
dbbackup cost analyze --region us-east-1
|
|
|
|
# JSON output for automation
|
|
dbbackup cost analyze --format json`,
|
|
}
|
|
|
|
var costAnalyzeCmd = &cobra.Command{
|
|
Use: "analyze",
|
|
Short: "Analyze backup storage costs",
|
|
Args: cobra.NoArgs,
|
|
RunE: runCostAnalyze,
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(costCmd)
|
|
costCmd.AddCommand(costAnalyzeCmd)
|
|
|
|
costAnalyzeCmd.Flags().StringVar(&costDatabase, "database", "", "Filter by database")
|
|
costAnalyzeCmd.Flags().StringVar(&costFormat, "format", "table", "Output format (table, json)")
|
|
costAnalyzeCmd.Flags().StringVar(&costRegion, "region", "us-east-1", "Cloud region for pricing")
|
|
costAnalyzeCmd.Flags().StringVar(&costProvider, "provider", "all", "Show specific provider (all, aws, gcs, azure, b2, wasabi)")
|
|
costAnalyzeCmd.Flags().IntVar(&costDays, "days", 30, "Number of days to calculate")
|
|
}
|
|
|
|
func runCostAnalyze(cmd *cobra.Command, args []string) error {
|
|
cat, err := openCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cat.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get backup statistics
|
|
var stats *catalog.Stats
|
|
if costDatabase != "" {
|
|
stats, err = cat.StatsByDatabase(ctx, costDatabase)
|
|
} else {
|
|
stats, err = cat.Stats(ctx)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if stats.TotalBackups == 0 {
|
|
fmt.Println("No backups found in catalog. Run 'dbbackup catalog sync' first.")
|
|
return nil
|
|
}
|
|
|
|
// Calculate costs
|
|
analysis := calculateCosts(stats.TotalSize, costDays, costRegion)
|
|
|
|
if costFormat == "json" {
|
|
return outputCostJSON(analysis, stats)
|
|
}
|
|
|
|
return outputCostTable(analysis, stats)
|
|
}
|
|
|
|
// StorageTier represents a storage class/tier
|
|
type StorageTier struct {
|
|
Provider string
|
|
Tier string
|
|
Description string
|
|
StorageGB float64 // $ per GB/month
|
|
RetrievalGB float64 // $ per GB retrieved
|
|
Requests float64 // $ per 1000 requests
|
|
MinDays int // Minimum storage duration
|
|
}
|
|
|
|
// CostAnalysis represents the cost breakdown
|
|
type CostAnalysis struct {
|
|
TotalSizeGB float64
|
|
Days int
|
|
Region string
|
|
Recommendations []TierRecommendation
|
|
}
|
|
|
|
type TierRecommendation struct {
|
|
Provider string
|
|
Tier string
|
|
Description string
|
|
MonthlyStorage float64
|
|
AnnualStorage float64
|
|
RetrievalCost float64
|
|
TotalMonthly float64
|
|
TotalAnnual float64
|
|
SavingsVsS3 float64
|
|
SavingsPct float64
|
|
BestFor string
|
|
}
|
|
|
|
func calculateCosts(totalBytes int64, days int, region string) *CostAnalysis {
|
|
sizeGB := float64(totalBytes) / (1024 * 1024 * 1024)
|
|
|
|
analysis := &CostAnalysis{
|
|
TotalSizeGB: sizeGB,
|
|
Days: days,
|
|
Region: region,
|
|
}
|
|
|
|
// Define storage tiers (pricing as of 2026, approximate)
|
|
tiers := []StorageTier{
|
|
// AWS S3
|
|
{Provider: "AWS S3", Tier: "Standard", Description: "Frequent access",
|
|
StorageGB: 0.023, RetrievalGB: 0.0, Requests: 0.0004, MinDays: 0},
|
|
{Provider: "AWS S3", Tier: "Intelligent-Tiering", Description: "Auto-optimization",
|
|
StorageGB: 0.023, RetrievalGB: 0.0, Requests: 0.0004, MinDays: 0},
|
|
{Provider: "AWS S3", Tier: "Standard-IA", Description: "Infrequent access",
|
|
StorageGB: 0.0125, RetrievalGB: 0.01, Requests: 0.001, MinDays: 30},
|
|
{Provider: "AWS S3", Tier: "Glacier Instant", Description: "Archive instant",
|
|
StorageGB: 0.004, RetrievalGB: 0.03, Requests: 0.01, MinDays: 90},
|
|
{Provider: "AWS S3", Tier: "Glacier Flexible", Description: "Archive flexible",
|
|
StorageGB: 0.0036, RetrievalGB: 0.02, Requests: 0.05, MinDays: 90},
|
|
{Provider: "AWS S3", Tier: "Deep Archive", Description: "Long-term archive",
|
|
StorageGB: 0.00099, RetrievalGB: 0.02, Requests: 0.05, MinDays: 180},
|
|
|
|
// Google Cloud Storage
|
|
{Provider: "GCS", Tier: "Standard", Description: "Frequent access",
|
|
StorageGB: 0.020, RetrievalGB: 0.0, Requests: 0.0004, MinDays: 0},
|
|
{Provider: "GCS", Tier: "Nearline", Description: "Monthly access",
|
|
StorageGB: 0.010, RetrievalGB: 0.01, Requests: 0.001, MinDays: 30},
|
|
{Provider: "GCS", Tier: "Coldline", Description: "Quarterly access",
|
|
StorageGB: 0.004, RetrievalGB: 0.02, Requests: 0.005, MinDays: 90},
|
|
{Provider: "GCS", Tier: "Archive", Description: "Annual access",
|
|
StorageGB: 0.0012, RetrievalGB: 0.05, Requests: 0.05, MinDays: 365},
|
|
|
|
// Azure Blob Storage
|
|
{Provider: "Azure", Tier: "Hot", Description: "Frequent access",
|
|
StorageGB: 0.0184, RetrievalGB: 0.0, Requests: 0.0004, MinDays: 0},
|
|
{Provider: "Azure", Tier: "Cool", Description: "Infrequent access",
|
|
StorageGB: 0.010, RetrievalGB: 0.01, Requests: 0.001, MinDays: 30},
|
|
{Provider: "Azure", Tier: "Archive", Description: "Long-term archive",
|
|
StorageGB: 0.00099, RetrievalGB: 0.02, Requests: 0.05, MinDays: 180},
|
|
|
|
// Backblaze B2
|
|
{Provider: "Backblaze B2", Tier: "Standard", Description: "Affordable cloud",
|
|
StorageGB: 0.005, RetrievalGB: 0.01, Requests: 0.0004, MinDays: 0},
|
|
|
|
// Wasabi
|
|
{Provider: "Wasabi", Tier: "Hot Cloud", Description: "No egress fees",
|
|
StorageGB: 0.0059, RetrievalGB: 0.0, Requests: 0.0, MinDays: 90},
|
|
}
|
|
|
|
// Calculate costs for each tier
|
|
s3StandardCost := 0.0
|
|
for _, tier := range tiers {
|
|
if costProvider != "all" {
|
|
providerLower := strings.ToLower(tier.Provider)
|
|
filterLower := strings.ToLower(costProvider)
|
|
if !strings.Contains(providerLower, filterLower) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
rec := TierRecommendation{
|
|
Provider: tier.Provider,
|
|
Tier: tier.Tier,
|
|
Description: tier.Description,
|
|
}
|
|
|
|
// Monthly storage cost
|
|
rec.MonthlyStorage = sizeGB * tier.StorageGB
|
|
|
|
// Annual storage cost
|
|
rec.AnnualStorage = rec.MonthlyStorage * 12
|
|
|
|
// Estimate retrieval cost (assume 1 retrieval per month for DR testing)
|
|
rec.RetrievalCost = sizeGB * tier.RetrievalGB
|
|
|
|
// Total costs
|
|
rec.TotalMonthly = rec.MonthlyStorage + rec.RetrievalCost
|
|
rec.TotalAnnual = rec.AnnualStorage + (rec.RetrievalCost * 12)
|
|
|
|
// Track S3 Standard for comparison
|
|
if tier.Provider == "AWS S3" && tier.Tier == "Standard" {
|
|
s3StandardCost = rec.TotalMonthly
|
|
}
|
|
|
|
// Recommendations
|
|
switch {
|
|
case tier.MinDays >= 180:
|
|
rec.BestFor = "Long-term archives (6+ months)"
|
|
case tier.MinDays >= 90:
|
|
rec.BestFor = "Compliance archives (3+ months)"
|
|
case tier.MinDays >= 30:
|
|
rec.BestFor = "Recent backups (monthly rotation)"
|
|
default:
|
|
rec.BestFor = "Active/hot backups (daily access)"
|
|
}
|
|
|
|
analysis.Recommendations = append(analysis.Recommendations, rec)
|
|
}
|
|
|
|
// Calculate savings vs S3 Standard
|
|
if s3StandardCost > 0 {
|
|
for i := range analysis.Recommendations {
|
|
rec := &analysis.Recommendations[i]
|
|
rec.SavingsVsS3 = s3StandardCost - rec.TotalMonthly
|
|
if s3StandardCost > 0 {
|
|
rec.SavingsPct = (rec.SavingsVsS3 / s3StandardCost) * 100.0
|
|
}
|
|
}
|
|
}
|
|
|
|
return analysis
|
|
}
|
|
|
|
func outputCostTable(analysis *CostAnalysis, stats *catalog.Stats) error {
|
|
fmt.Println()
|
|
fmt.Println("═══════════════════════════════════════════════════════════════════════════")
|
|
fmt.Printf(" Cloud Storage Cost Analysis\n")
|
|
fmt.Println("═══════════════════════════════════════════════════════════════════════════")
|
|
fmt.Println()
|
|
|
|
fmt.Printf("[CURRENT BACKUP INVENTORY]\n")
|
|
fmt.Printf(" Total Backups: %d\n", stats.TotalBackups)
|
|
fmt.Printf(" Total Size: %.2f GB (%s)\n", analysis.TotalSizeGB, stats.TotalSizeHuman)
|
|
if costDatabase != "" {
|
|
fmt.Printf(" Database: %s\n", costDatabase)
|
|
} else {
|
|
fmt.Printf(" Databases: %d\n", len(stats.ByDatabase))
|
|
}
|
|
fmt.Printf(" Region: %s\n", analysis.Region)
|
|
fmt.Printf(" Analysis Period: %d days\n", analysis.Days)
|
|
fmt.Println()
|
|
|
|
fmt.Println("───────────────────────────────────────────────────────────────────────────")
|
|
fmt.Printf("%-20s %-20s %12s %12s %12s\n",
|
|
"PROVIDER", "TIER", "MONTHLY", "ANNUAL", "SAVINGS")
|
|
fmt.Println("───────────────────────────────────────────────────────────────────────────")
|
|
|
|
for _, rec := range analysis.Recommendations {
|
|
savings := ""
|
|
if rec.SavingsVsS3 > 0 {
|
|
savings = fmt.Sprintf("↓ $%.2f (%.0f%%)", rec.SavingsVsS3, rec.SavingsPct)
|
|
} else if rec.SavingsVsS3 < 0 {
|
|
savings = fmt.Sprintf("↑ $%.2f", -rec.SavingsVsS3)
|
|
} else {
|
|
savings = "baseline"
|
|
}
|
|
|
|
fmt.Printf("%-20s %-20s $%10.2f $%10.2f %s\n",
|
|
rec.Provider,
|
|
rec.Tier,
|
|
rec.TotalMonthly,
|
|
rec.TotalAnnual,
|
|
savings,
|
|
)
|
|
}
|
|
|
|
fmt.Println("───────────────────────────────────────────────────────────────────────────")
|
|
fmt.Println()
|
|
|
|
// Top recommendations
|
|
fmt.Println("[COST OPTIMIZATION RECOMMENDATIONS]")
|
|
fmt.Println()
|
|
|
|
// Find cheapest option
|
|
cheapest := analysis.Recommendations[0]
|
|
for _, rec := range analysis.Recommendations {
|
|
if rec.TotalAnnual < cheapest.TotalAnnual {
|
|
cheapest = rec
|
|
}
|
|
}
|
|
|
|
fmt.Printf("💰 CHEAPEST OPTION: %s %s\n", cheapest.Provider, cheapest.Tier)
|
|
fmt.Printf(" Annual Cost: $%.2f (save $%.2f/year vs S3 Standard)\n",
|
|
cheapest.TotalAnnual, cheapest.SavingsVsS3*12)
|
|
fmt.Printf(" Best For: %s\n", cheapest.BestFor)
|
|
fmt.Println()
|
|
|
|
// Find best balance
|
|
fmt.Printf("⚖️ BALANCED OPTION: AWS S3 Standard-IA or GCS Nearline\n")
|
|
fmt.Printf(" Good balance of cost and accessibility\n")
|
|
fmt.Printf(" Suitable for 30-day retention backups\n")
|
|
fmt.Println()
|
|
|
|
// Find hot storage
|
|
fmt.Printf("🔥 HOT STORAGE: Wasabi or Backblaze B2\n")
|
|
fmt.Printf(" No egress fees (Wasabi) or low retrieval costs\n")
|
|
fmt.Printf(" Perfect for frequent restore testing\n")
|
|
fmt.Println()
|
|
|
|
// Strategy recommendation
|
|
fmt.Println("[TIERED STORAGE STRATEGY]")
|
|
fmt.Println()
|
|
fmt.Printf(" Day 0-7: S3 Standard or Wasabi (frequent access)\n")
|
|
fmt.Printf(" Day 8-30: S3 Standard-IA or GCS Nearline (weekly access)\n")
|
|
fmt.Printf(" Day 31-90: S3 Glacier or GCS Coldline (monthly access)\n")
|
|
fmt.Printf(" Day 90+: S3 Deep Archive or GCS Archive (compliance)\n")
|
|
fmt.Println()
|
|
|
|
potentialSaving := 0.0
|
|
for _, rec := range analysis.Recommendations {
|
|
if rec.Provider == "AWS S3" && rec.Tier == "Deep Archive" {
|
|
potentialSaving = rec.SavingsVsS3 * 12
|
|
}
|
|
}
|
|
|
|
if potentialSaving > 0 {
|
|
fmt.Printf("💡 With tiered lifecycle policies, you could save ~$%.2f/year\n", potentialSaving)
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("═══════════════════════════════════════════════════════════════════════════")
|
|
fmt.Println()
|
|
fmt.Println("Note: Costs are estimates based on standard pricing.")
|
|
fmt.Println("Actual costs may vary by region, usage patterns, and current pricing.")
|
|
fmt.Println()
|
|
|
|
return nil
|
|
}
|
|
|
|
func outputCostJSON(analysis *CostAnalysis, stats *catalog.Stats) error {
|
|
output := map[string]interface{}{
|
|
"inventory": map[string]interface{}{
|
|
"total_backups": stats.TotalBackups,
|
|
"total_size_gb": analysis.TotalSizeGB,
|
|
"total_size_human": stats.TotalSizeHuman,
|
|
"region": analysis.Region,
|
|
"analysis_days": analysis.Days,
|
|
},
|
|
"recommendations": analysis.Recommendations,
|
|
}
|
|
|
|
// Find cheapest
|
|
cheapest := analysis.Recommendations[0]
|
|
for _, rec := range analysis.Recommendations {
|
|
if rec.TotalAnnual < cheapest.TotalAnnual {
|
|
cheapest = rec
|
|
}
|
|
}
|
|
|
|
output["cheapest"] = map[string]interface{}{
|
|
"provider": cheapest.Provider,
|
|
"tier": cheapest.Tier,
|
|
"annual_cost": cheapest.TotalAnnual,
|
|
"monthly_cost": cheapest.TotalMonthly,
|
|
}
|
|
|
|
data, err := json.MarshalIndent(output, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(string(data))
|
|
return nil
|
|
}
|