Setting the file. One moment. Price Calculator · Amazon Elasticache · aws/agent-toolkit-for-aws · Skills Docs10
Setup DevOps Agent
33
AWS Deployment
61.5
Recipe Gallery
Script Migration Preflight
scripts/price_calculator.py
Python·336 lines·14 KB
# Node-based with replicas
18 python3 price_calculator.py --engine valkey --mode node --node-type cache.r7g.large --nodes 3
19
20 # Node-based with specific RI term
21 python3 price_calculator.py --mode node --node-type cache.r7g.large --nodes 2 --ri-term 1yr_no_upfront
22
23 # Show all reserved options for a node type
24 python3 price_calculator.py --mode node --node-type cache.r7g.large --nodes 2 --show-ri-options
25
26 # Specific region
27 python3 price_calculator.py --engine valkey --region eu-west-1 --mode serverless --data-gb 10
28
29 # Extended Support surcharge for EOL Redis versions
30 python3 price_calculator.py --engine redis --extended-support --node-type cache.r7g.large --nodes 6
31
32 # Interactive mode
33 python3 price_calculator.py --interactive
34"""
35
36import argparse
37import json
38import os
39import sys
40
41sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
42from pricing import PricingLoader
43
44# Detect region from environment with us-east-1 as fallback
45REGION = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1"
46
47HOURS_PER_MONTH = 730
48
49PRICING_DISCLAIMER = (
50 " Estimates only. Verify current pricing at https://aws.amazon.com/elasticache/pricing/"
51)
52
53
54def estimate_serverless(loader, engine, region, data_gb, ecpu_millions_per_month, avg_payload_kb=1.0):
55 storage_rate = loader.get_serverless_storage_rate(region, engine)
56 ecpu_rate = loader.get_serverless_ecpu_rate(region, engine)
57 min_gb = 0.1 if engine == "valkey" else 1.0
58 billing_gb = max(data_gb, min_gb)
59 storage = billing_gb * storage_rate * HOURS_PER_MONTH
60 scaled_ecpus = ecpu_millions_per_month * max(1.0, avg_payload_kb)
61 compute = scaled_ecpus * 1_000_000 * ecpu_rate
62 result = {
63 "model": "serverless",
64 "engine": engine,
65 "region": region,
66 "data_gb": data_gb,
67 "ecpu_millions": ecpu_millions_per_month,
68 "storage_monthly": round(storage, 2),
69 "compute_monthly": round(compute, 2),
70 "total_monthly": round(storage + compute, 2),
71 }
72 if avg_payload_kb != 1.0:
73 result["avg_payload_kb"] = avg_payload_kb
74 result["ecpu_millions_scaled"] = round(scaled_ecpus, 1)
75 return result
76
77
78def estimate_node_based(loader, engine, region, node_type, node_count, ri_term=None):
79 try:
80 hourly = loader.get_node_hourly_rate(region, node_type, engine)
81 except ValueError as e:
82 return {"error": str(e)}
83
84 monthly_per_node = hourly * HOURS_PER_MONTH
85 total = monthly_per_node * node_count
86 mem = loader.get_node_memory_gb(node_type)
87
88 result = {
89 "model": "node-based (on-demand)",
90 "engine": engine,
91 "region": region,
92 "node_type": node_type,
93 "node_count": node_count,
94 "memory_per_node_gb": mem if mem > 0 else "unknown",
95 "total_memory_gb": round(mem * node_count, 1) if mem > 0 else "unknown",
96 "per_node_monthly": round(monthly_per_node, 2),
97 "total_monthly": round(total, 2),
98 }
99
100 # Try real reserved pricing first, fall back to hardcoded discount
101 if ri_term:
102 ri_map = {
103 "1yr_no_upfront": ("1yr", "No Upfront"),
104 "1yr_partial_upfront": ("1yr", "Partial Upfront"),
105 "1yr_all_upfront": ("1yr", "All Upfront"),
106 "3yr_no_upfront": ("3yr", "No Upfront"),
107 "3yr_partial_upfront": ("3yr", "Partial Upfront"),
108 "3yr_all_upfront": ("3yr", "All Upfront"),
109 }
110 if ri_term in ri_map:
111 lease, purchase = ri_map[ri_term]
112 try:
113 eff_monthly = loader.get_reserved_effective_monthly(
114 region, node_type, engine, lease, purchase
115 )
116 ri_total = eff_monthly * node_count
117 discount_pct = round((1 - ri_total / total) * 100)
118 result["reserved"] = {
119 "term": ri_term,
120 "discount_pct": discount_pct,
121 "per_node_monthly": eff_monthly,
122 "total_monthly": round(ri_total, 2),
123 "annual_savings": round((total - ri_total) * 12, 2),
124 "source": "live",
125 }
126 except ValueError:
127 pass # no reserved pricing available for this combo
128
129 return result
130
131
132def format_ri_options(loader, engine, region, node_type, node_count):
133 """Show all reserved pricing options for a specific node configuration."""
134 options = loader.list_reserved_options(region, node_type, engine)
135 hourly = loader.get_node_hourly_rate(region, node_type, engine)
136 od_monthly = hourly * HOURS_PER_MONTH * node_count
137
138 lines = []
139 lines.append("=" * 68)
140 lines.append("Reserved Pricing Options (live pricing)")
141 lines.append("=" * 68)
142 lines.append("")
143 lines.append(" Node type: {} x {} ({} engine)".format(node_type, node_count, engine))
144 lines.append(" On-demand: ${:,.2f}/month".format(od_monthly))
145 lines.append("")
146
147 if not options:
148 lines.append(" No reserved pricing options found for this configuration.")
149 lines.append("")
150 lines.append(PRICING_DISCLAIMER)
151 lines.append("")
152 return "\n".join(lines)
153
154 lines.append(" {:<10} {:<18} {:>12} {:>10} {:>10}".format(
155 "Term", "Purchase Option", "Monthly", "Upfront", "Savings"))
156 lines.append(" {} {} {} {} {}".format("-" * 10, "-" * 18, "-" * 12, "-" * 10, "-" * 10))
157
158 for opt in options:
159 eff_total = opt["effective_monthly"] * node_count
160 savings_pct = round((1 - eff_total / od_monthly) * 100)
161 upfront_total = opt["upfront"] * node_count
162 lines.append(" {:<10} {:<18} ${:>9,.2f} ${:>8,.0f} {:>8}%".format(
163 opt["lease_length"], opt["purchase_option"],
164 eff_total, upfront_total, savings_pct))
165
166 lines.append("")
167 lines.append(PRICING_DISCLAIMER)
168 lines.append("")
169 return "\n".join(lines)
170
171
172def estimate_extended_support(loader, engine, region, node_type, node_count):
173 """Estimate Extended Support surcharge for EOL Redis versions."""
174 results = {"engine": engine, "region": region, "node_type": node_type, "node_count": node_count, "years": []}
175
176 if not loader.has_extended_support_pricing():
177 results["note"] = "Extended Support pricing not available in bulk pricing API. Check the ElastiCache pricing page."
178 return results
179
180 for year in ["1", "2", "3"]:
181 try:
182 hourly = loader.get_extended_support_rate(region, node_type, engine, year)
183 monthly = hourly * HOURS_PER_MONTH * node_count
184 results["years"].append({
185 "year": int(year),
186 "per_node_hourly": hourly,
187 "total_monthly": round(monthly, 2),
188 "total_annual": round(monthly * 12, 2),
189 })
190 except ValueError:
191 pass
192
193 if results["years"]:
194 try:
195 base_hourly = loader.get_node_hourly_rate(region, node_type, engine)
196 base_monthly = base_hourly * HOURS_PER_MONTH * node_count
197 results["base_monthly"] = round(base_monthly, 2)
198 total_yr1 = base_monthly + results["years"][0]["total_monthly"]
199 results["total_with_surcharge_monthly"] = round(total_yr1, 2)
200 except ValueError:
201 pass
202 results["recommendation"] = (
203 "Migrating to Valkey eliminates Extended Support charges. "
204 "Savings vs other engines: 20% lower on node-based, 33% lower on serverless."
205 )
206
207 return results
208
209
210def format_extended_support_report(results):
211 """Generate a human-readable Extended Support cost report."""
212 lines = []
213 lines.append("=" * 60)
214 lines.append("Extended Support Cost Estimate (live pricing)")
215 lines.append("=" * 60)
216 lines.append("")
217 lines.append(" Engine: {}".format(results["engine"]))
218 lines.append(" Region: {}".format(results["region"]))
219 lines.append(" Node type: {}".format(results.get("node_type", "unknown")))
220 lines.append(" Nodes: {}".format(results["node_count"]))
221 lines.append("")
222
223 if not results["years"]:
224 lines.append(" {}".format(results.get("note", "No pricing data available.")))
225 lines.append("")
226 return "\n".join(lines)
227
228 lines.append(" {:<18} {:>16} {:>16}".format("Year After EOL", "Monthly Cost", "Annual Cost"))
229 lines.append(" {} {} {}".format("-" * 18, "-" * 16, "-" * 16))
230
231 for y in results["years"]:
232 lines.append(" Year {:<13} ${:>13,.2f} ${:>13,.2f}".format(
233 y["year"], y["total_monthly"], y["total_annual"]
234 ))
235
236 if results.get("base_monthly"):
237 lines.append("")
238 lines.append(" Base node cost: ${:>13,.2f}/month".format(results["base_monthly"]))
239 lines.append(" Total (base + Yr1): ${:>13,.2f}/month".format(results["total_with_surcharge_monthly"]))
240
241 lines.append("")
242 if "recommendation" in results:
243 lines.append(" {}".format(results["recommendation"]))
244 lines.append("")
245 lines.append(PRICING_DISCLAIMER)
246 lines.append("")
247 return "\n".join(lines)
248
249
250def estimate_ecpu_from_ops(ops_per_sec, avg_ecpu_per_op=1.0):
251 """Convert operations per second to monthly ECPUs (millions).
252
253 Default: 1 ECPU per operation (assumes simple GET/SET with payload under 1 KB).
254 For larger payloads, use --avg-payload-kb which scales ECPUs linearly.
255 For complex commands (SORT, ZADD, etc.), pass a higher avg_ecpu_per_op.
256 """
257 ecpu_per_month = ops_per_sec * avg_ecpu_per_op * 3600 * HOURS_PER_MONTH
258 return round(ecpu_per_month / 1_000_000, 1)
259
260
261def interactive(loader, region):
262 """Interactive interview-style cost estimation."""
263 print("=== ElastiCache Price Calculator (live pricing) ===\n")
264
265 engine = input("Engine [valkey/redis/memcached] (default: valkey): ").strip().lower() or "valkey"
266 data_gb = float(input("Estimated data size in GB (default: 1): ").strip() or "1")
267 mode = input("Deployment [serverless/node] (default: serverless): ").strip().lower() or "serverless"
268
269 if mode == "serverless":
270 ops = input("Estimated operations per second (default: 100): ").strip() or "100"
271 ops_per_sec = float(ops)
272 ecpu_millions = estimate_ecpu_from_ops(ops_per_sec)
273 print(" -> Estimated {}M ECPUs/month".format(ecpu_millions))
274 result = estimate_serverless(loader, engine, region, data_gb, ecpu_millions)
275 print("\n{}".format(json.dumps(result, indent=2)))
276
277 elif mode == "node":
278 node_type = input("Node type (default: cache.r7g.large): ").strip() or "cache.r7g.large"
279 nodes = int(input("Number of nodes (default: 2): ").strip() or "2")
280 result = estimate_node_based(loader, engine, region, node_type, nodes, ri_term="1yr_no_upfront")
281 print("\n{}".format(json.dumps(result, indent=2)))
282 show_ri = input("\nShow all reserved options? [y/n] (default: n): ").strip().lower()
283 if show_ri == "y":
284 print("\n{}".format(format_ri_options(loader, engine, region, node_type, nodes)))
285
286
287if __name__ == "__main__":
288 parser = argparse.ArgumentParser(description="ElastiCache Price Calculator (live pricing)")
289 parser.add_argument("--interactive", action="store_true", help="Interactive mode")
290 parser.add_argument("--engine", choices=["valkey", "redis", "memcached"], default="valkey")
291 parser.add_argument("--mode", choices=["serverless", "node"], default="serverless")
292 parser.add_argument("--region", default=REGION, help="AWS region (default: from env or us-east-1)")
293 parser.add_argument("--data-gb", type=float, default=1.0, help="Data stored in GB")
294 parser.add_argument("--ecpu-millions", type=float, default=None, help="ECPUs per month (millions)")
295 parser.add_argument("--ops-per-sec", type=float, default=None, help="Operations per second (auto-converts to ECPUs)")
296 parser.add_argument("--node-type", default="cache.r7g.large", help="Node type for node-based")
297 parser.add_argument("--nodes", type=int, default=2, help="Node count for node-based")
298 parser.add_argument("--ri-term", default=None,
299 choices=["1yr_no_upfront", "1yr_partial_upfront", "1yr_all_upfront",
300 "3yr_no_upfront", "3yr_partial_upfront", "3yr_all_upfront"],
301 help="Reserved instance term")
302 parser.add_argument("--show-ri-options", action="store_true",
303 help="Show all reserved pricing options for the node type")
304 parser.add_argument("--extended-support", action="store_true",
305 help="Show Extended Support surcharge for EOL Redis versions")
306 parser.add_argument("--avg-payload-kb", type=float, default=1.0,
307 help="Average payload size in KB (default: 1.0). ECPUs scale linearly with payload.")
308 parser.add_argument("--pricing-csv", default=None, help="Optional local pricing CSV (overrides live fetch)")
309 args = parser.parse_args()
310
311 loader = PricingLoader(args.pricing_csv)
312
313 if args.interactive:
314 interactive(loader, args.region)
315 sys.exit(0)
316
317 # Auto-convert ops/sec to ECPUs if provided
318 ecpu_millions = args.ecpu_millions
319 if ecpu_millions is None and args.ops_per_sec:
320 ecpu_millions = estimate_ecpu_from_ops(args.ops_per_sec)
321 elif ecpu_millions is None:
322 ecpu_millions = 100.0
323
324 if args.extended_support:
325 result = estimate_extended_support(loader, args.engine, args.region, args.node_type, args.nodes)
326 print(format_extended_support_report(result))
327 elif args.show_ri_options:
328 print(format_ri_options(loader, args.engine, args.region, args.node_type, args.nodes))
329 elif args.mode == "serverless":
330 result = estimate_serverless(loader, args.engine, args.region, args.data_gb, ecpu_millions, args.avg_payload_kb)
331 result["disclaimer"] = "Estimates only. Verify at https://aws.amazon.com/elasticache/pricing/"
332 print(json.dumps(result, indent=2))
333 elif args.mode == "node":
334 result = estimate_node_based(loader, args.engine, args.region, args.node_type, args.nodes, ri_term=args.ri_term)
335 result["disclaimer"] = "Estimates only. Verify at https://aws.amazon.com/elasticache/pricing/"
336 print(json.dumps(result, indent=2))