cdk-cost-analyzer 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.cdk-cost-analyzer-cache/metadata.json +8 -8
  2. package/dist/action/api/index.d.ts +1 -0
  3. package/dist/action/api/single-template-types.d.ts +4 -0
  4. package/dist/action/api/types.d.ts +3 -0
  5. package/dist/action/config/types.d.ts +2 -0
  6. package/dist/action/optimization/OptimizationEngine.d.ts +11 -0
  7. package/dist/action/optimization/analyzers/GravitonMigrationAnalyzer.d.ts +17 -0
  8. package/dist/action/optimization/analyzers/NATGatewayOptimizationAnalyzer.d.ts +13 -0
  9. package/dist/action/optimization/analyzers/ReservedInstanceAnalyzer.d.ts +13 -0
  10. package/dist/action/optimization/analyzers/RightSizingAnalyzer.d.ts +13 -0
  11. package/dist/action/optimization/analyzers/SavingsPlansAnalyzer.d.ts +11 -0
  12. package/dist/action/optimization/analyzers/SpotInstanceAnalyzer.d.ts +12 -0
  13. package/dist/action/optimization/analyzers/StorageOptimizationAnalyzer.d.ts +14 -0
  14. package/dist/action/optimization/defaults.d.ts +2 -0
  15. package/dist/action/optimization/index.d.ts +10 -0
  16. package/dist/action/optimization/types.d.ts +35 -0
  17. package/dist/action/reporter/RecommendationReporter.d.ts +3 -0
  18. package/dist/analysis/SingleTemplateAnalyzer.js +12 -1
  19. package/dist/api/index.d.ts +1 -0
  20. package/dist/api/index.js +2 -1
  21. package/dist/api/single-template-types.d.ts +4 -0
  22. package/dist/api/single-template-types.js +1 -1
  23. package/dist/api/types.d.ts +3 -0
  24. package/dist/api/types.js +1 -1
  25. package/dist/cli/index.js +13 -2
  26. package/dist/config/types.d.ts +2 -0
  27. package/dist/config/types.js +1 -1
  28. package/dist/optimization/OptimizationEngine.d.ts +11 -0
  29. package/dist/optimization/OptimizationEngine.js +51 -0
  30. package/dist/optimization/analyzers/GravitonMigrationAnalyzer.d.ts +17 -0
  31. package/dist/optimization/analyzers/GravitonMigrationAnalyzer.js +209 -0
  32. package/dist/optimization/analyzers/NATGatewayOptimizationAnalyzer.d.ts +13 -0
  33. package/dist/optimization/analyzers/NATGatewayOptimizationAnalyzer.js +142 -0
  34. package/dist/optimization/analyzers/ReservedInstanceAnalyzer.d.ts +13 -0
  35. package/dist/optimization/analyzers/ReservedInstanceAnalyzer.js +102 -0
  36. package/dist/optimization/analyzers/RightSizingAnalyzer.d.ts +13 -0
  37. package/dist/optimization/analyzers/RightSizingAnalyzer.js +109 -0
  38. package/dist/optimization/analyzers/SavingsPlansAnalyzer.d.ts +11 -0
  39. package/dist/optimization/analyzers/SavingsPlansAnalyzer.js +102 -0
  40. package/dist/optimization/analyzers/SpotInstanceAnalyzer.d.ts +12 -0
  41. package/dist/optimization/analyzers/SpotInstanceAnalyzer.js +113 -0
  42. package/dist/optimization/analyzers/StorageOptimizationAnalyzer.d.ts +14 -0
  43. package/dist/optimization/analyzers/StorageOptimizationAnalyzer.js +142 -0
  44. package/dist/optimization/defaults.d.ts +2 -0
  45. package/dist/optimization/defaults.js +22 -0
  46. package/dist/optimization/index.d.ts +10 -0
  47. package/dist/optimization/index.js +22 -0
  48. package/dist/optimization/types.d.ts +35 -0
  49. package/dist/optimization/types.js +3 -0
  50. package/dist/releasetag.txt +1 -1
  51. package/dist/reporter/RecommendationReporter.d.ts +3 -0
  52. package/dist/reporter/RecommendationReporter.js +133 -0
  53. package/dist/reporter/SingleTemplateReporter.js +11 -1
  54. package/docs/CALCULATORS.md +374 -8
  55. package/docs/README.md +3 -2
  56. package/docs/RECOMMENDATIONS.md +466 -0
  57. package/docs/SINGLE-TEMPLATE-ANALYSIS.md +59 -16
  58. package/docs/index.md +38 -8
  59. package/examples/recommendations-demo.json +74 -0
  60. package/package.json +1 -1
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NATGatewayOptimizationAnalyzer = void 0;
4
+ const NAT_GATEWAY_HOURLY = 0.045;
5
+ const NAT_INSTANCE_T3_NANO_HOURLY = 0.0052;
6
+ const HOURS_PER_MONTH = 730;
7
+ class NATGatewayOptimizationAnalyzer {
8
+ category = 'nat-gateway-optimization';
9
+ name = 'NAT Gateway Optimization';
10
+ isApplicable(resources) {
11
+ return resources.some((r) => r.type === 'AWS::EC2::NatGateway');
12
+ }
13
+ async analyze(resources, resourceCosts, _region) {
14
+ const recommendations = [];
15
+ const natGateways = resources.filter((r) => r.type === 'AWS::EC2::NatGateway');
16
+ for (const natGw of natGateways) {
17
+ recommendations.push(this.suggestNATInstanceForDev(natGw, resourceCosts));
18
+ }
19
+ const vpcEndpointRec = this.suggestVPCEndpoints(resources, natGateways);
20
+ if (vpcEndpointRec) {
21
+ recommendations.push(vpcEndpointRec);
22
+ }
23
+ if (natGateways.length > 1) {
24
+ const consolidationRec = this.suggestConsolidation(natGateways, resourceCosts);
25
+ if (consolidationRec) {
26
+ recommendations.push(consolidationRec);
27
+ }
28
+ }
29
+ return recommendations;
30
+ }
31
+ suggestNATInstanceForDev(natGw, resourceCosts) {
32
+ const natCost = this.findCost(natGw.logicalId, resourceCosts);
33
+ const natInstanceCost = NAT_INSTANCE_T3_NANO_HOURLY * HOURS_PER_MONTH;
34
+ const savings = natCost > 0
35
+ ? natCost - natInstanceCost
36
+ : (NAT_GATEWAY_HOURLY * HOURS_PER_MONTH) - natInstanceCost;
37
+ const savingsPercent = natCost > 0
38
+ ? Math.round((savings / natCost) * 100)
39
+ : 88;
40
+ return {
41
+ id: `nat-instance-${natGw.logicalId}`,
42
+ title: `Replace ${natGw.logicalId} with NAT instance (dev/test)`,
43
+ description: `NAT Gateways cost ~$${(NAT_GATEWAY_HOURLY * HOURS_PER_MONTH).toFixed(2)}/month. A t3.nano NAT instance costs ~$${natInstanceCost.toFixed(2)}/month. For non-production environments, a NAT instance provides significant savings.`,
44
+ category: this.category,
45
+ priority: 'medium',
46
+ estimatedMonthlySavings: Math.round(savings * 100) / 100,
47
+ estimatedSavingsPercent: savingsPercent,
48
+ affectedResources: [natGw.logicalId],
49
+ actionItems: [
50
+ 'Replace NAT Gateway with a t3.nano or t4g.nano EC2 instance configured as NAT',
51
+ 'Enable source/destination check disable on the NAT instance',
52
+ 'Update route tables to point to the NAT instance',
53
+ 'Configure auto-recovery for the NAT instance',
54
+ ],
55
+ caveats: [
56
+ 'Not recommended for production (single point of failure)',
57
+ 'Lower throughput and no built-in redundancy',
58
+ 'Requires manual patching and maintenance',
59
+ 'Only suitable for dev/test/staging environments',
60
+ ],
61
+ };
62
+ }
63
+ suggestVPCEndpoints(resources, natGateways) {
64
+ const hasS3 = resources.some((r) => r.type === 'AWS::S3::Bucket');
65
+ const hasDynamoDB = resources.some((r) => r.type === 'AWS::DynamoDB::Table');
66
+ if (!hasS3 && !hasDynamoDB)
67
+ return null;
68
+ const existingGatewayEndpoints = resources.filter((r) => {
69
+ if (r.type !== 'AWS::EC2::VPCEndpoint')
70
+ return false;
71
+ const vpcEndpointType = r.properties.VpcEndpointType;
72
+ return !vpcEndpointType || vpcEndpointType === 'Gateway';
73
+ });
74
+ const existingServices = existingGatewayEndpoints.map((r) => r.properties.ServiceName);
75
+ const missingEndpoints = [];
76
+ if (hasS3 &&
77
+ !existingServices.some((s) => s?.includes('s3'))) {
78
+ missingEndpoints.push('com.amazonaws.REGION.s3');
79
+ }
80
+ if (hasDynamoDB &&
81
+ !existingServices.some((s) => s?.includes('dynamodb'))) {
82
+ missingEndpoints.push('com.amazonaws.REGION.dynamodb');
83
+ }
84
+ if (missingEndpoints.length === 0)
85
+ return null;
86
+ const natIds = natGateways.map((n) => n.logicalId);
87
+ const services = missingEndpoints
88
+ .map((s) => s.split('.').pop())
89
+ .join(', ');
90
+ return {
91
+ id: 'vpc-gateway-endpoints',
92
+ title: `Add VPC Gateway Endpoints for ${services}`,
93
+ description: `S3 and DynamoDB Gateway Endpoints are free and route traffic directly to the service, bypassing NAT Gateway. This eliminates NAT Gateway data processing charges ($0.045/GB) for these services.`,
94
+ category: this.category,
95
+ priority: 'high',
96
+ estimatedMonthlySavings: 0,
97
+ estimatedSavingsPercent: 0,
98
+ affectedResources: natIds,
99
+ actionItems: missingEndpoints.map((ep) => `Add VPC Gateway Endpoint for ${ep}`),
100
+ caveats: [
101
+ 'Savings depend on data transfer volume to S3/DynamoDB',
102
+ 'Gateway Endpoints are free; savings come from reduced NAT Gateway data processing',
103
+ 'Update route tables to include the Gateway Endpoint',
104
+ ],
105
+ };
106
+ }
107
+ suggestConsolidation(natGateways, resourceCosts) {
108
+ if (natGateways.length <= 1)
109
+ return null;
110
+ const totalCost = natGateways.reduce((sum, ng) => sum + this.findCost(ng.logicalId, resourceCosts), 0);
111
+ const perGatewayCost = totalCost > 0
112
+ ? totalCost / natGateways.length
113
+ : NAT_GATEWAY_HOURLY * HOURS_PER_MONTH;
114
+ const savings = perGatewayCost * (natGateways.length - 1);
115
+ return {
116
+ id: 'nat-consolidation',
117
+ title: `Consolidate ${natGateways.length} NAT Gateways (dev/test)`,
118
+ description: `${natGateways.length} NAT Gateways detected. For non-production environments, a single NAT Gateway may be sufficient, saving ~$${savings.toFixed(2)}/month.`,
119
+ category: this.category,
120
+ priority: savings >= 50 ? 'high' : 'medium',
121
+ estimatedMonthlySavings: Math.round(savings * 100) / 100,
122
+ estimatedSavingsPercent: Math.round(((natGateways.length - 1) / natGateways.length) * 100),
123
+ affectedResources: natGateways.map((n) => n.logicalId),
124
+ actionItems: [
125
+ 'Evaluate if multi-AZ NAT Gateway redundancy is needed for this environment',
126
+ 'Route all private subnets through a single NAT Gateway',
127
+ `Remove ${natGateways.length - 1} redundant NAT Gateways`,
128
+ ],
129
+ caveats: [
130
+ 'Not recommended for production (single AZ failure risk)',
131
+ 'Cross-AZ data transfer charges may apply (~$0.01/GB)',
132
+ 'Only suitable for non-production environments',
133
+ ],
134
+ };
135
+ }
136
+ findCost(logicalId, resourceCosts) {
137
+ const cost = resourceCosts.find((c) => c.logicalId === logicalId);
138
+ return cost?.monthlyCost.amount ?? 0;
139
+ }
140
+ }
141
+ exports.NATGatewayOptimizationAnalyzer = NATGatewayOptimizationAnalyzer;
142
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTkFUR2F0ZXdheU9wdGltaXphdGlvbkFuYWx5emVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL29wdGltaXphdGlvbi9hbmFseXplcnMvTkFUR2F0ZXdheU9wdGltaXphdGlvbkFuYWx5emVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQVFBLE1BQU0sa0JBQWtCLEdBQUcsS0FBSyxDQUFDO0FBQ2pDLE1BQU0sMkJBQTJCLEdBQUcsTUFBTSxDQUFDO0FBQzNDLE1BQU0sZUFBZSxHQUFHLEdBQUcsQ0FBQztBQUU1QixNQUFhLDhCQUE4QjtJQUNoQyxRQUFRLEdBQXlCLDBCQUEwQixDQUFDO0lBQzVELElBQUksR0FBRywwQkFBMEIsQ0FBQztJQUUzQyxZQUFZLENBQUMsU0FBMkI7UUFDdEMsT0FBTyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxLQUFLLHNCQUFzQixDQUFDLENBQUM7SUFDbEUsQ0FBQztJQUVELEtBQUssQ0FBQyxPQUFPLENBQ1gsU0FBMkIsRUFDM0IsYUFBNkIsRUFDN0IsT0FBZTtRQUVmLE1BQU0sZUFBZSxHQUFxQixFQUFFLENBQUM7UUFDN0MsTUFBTSxXQUFXLEdBQUcsU0FBUyxDQUFDLE1BQU0sQ0FDbEMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEtBQUssc0JBQXNCLENBQ3pDLENBQUM7UUFFRixLQUFLLE1BQU0sS0FBSyxJQUFJLFdBQVcsRUFBRSxDQUFDO1lBQ2hDLGVBQWUsQ0FBQyxJQUFJLENBQ2xCLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxLQUFLLEVBQUUsYUFBYSxDQUFDLENBQ3BELENBQUM7UUFDSixDQUFDO1FBRUQsTUFBTSxjQUFjLEdBQUcsSUFBSSxDQUFDLG1CQUFtQixDQUFDLFNBQVMsRUFBRSxXQUFXLENBQUMsQ0FBQztRQUN4RSxJQUFJLGNBQWMsRUFBRSxDQUFDO1lBQ25CLGVB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QixRQUFRLEVBQUUsT0FBTyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxRQUFRO1lBQzNDLHVCQUF1QixFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxHQUFHLEdBQUcsQ0FBQyxHQUFHLEdBQUc7WUFDeEQsdUJBQXVCLEVBQUUsSUFBSSxDQUFDLEtBQUssQ0FDakMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FDdEQ7WUFDRCxpQkFBaUIsRUFBRSxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO1lBQ3RELFdBQVcsRUFBRTtnQkFDWCw0RUFBNEU7Z0JBQzVFLHdEQUF3RDtnQkFDeEQsVUFBVSxXQUFXLENBQUMsTUFBTSxHQUFHLENBQUMseUJBQXlCO2FBQzFEO1lBQ0QsT0FBTyxFQUFFO2dCQUNQLHlEQUF5RDtnQkFDekQsc0RBQXNEO2dCQUN0RCwrQ0FBK0M7YUFDaEQ7U0FDRixDQUFDO0lBQ0osQ0FBQztJQUVPLFFBQVEsQ0FBQyxTQUFpQixFQUFFLGFBQTZCO1FBQy9ELE1BQU0sSUFBSSxHQUFHLGFBQWEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxTQUFTLEtBQUssU0FBUyxDQUFDLENBQUM7UUFDbEUsT0FBTyxJQUFJLEVBQUUsV0FBVyxDQUFDLE1BQU0sSUFBSSxDQUFDLENBQUM7SUFDdkMsQ0FBQztDQUNGO0FBekxELHdFQXlMQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IFJlc291cmNlV2l0aElkIH0gZnJvbSAnLi4vLi4vZGlmZi90eXBlcyc7XG5pbXBvcnQgeyBSZXNvdXJjZUNvc3QgfSBmcm9tICcuLi8uLi9wcmljaW5nL3R5cGVzJztcbmltcG9ydCB7XG4gIE9wdGltaXphdGlvbkFuYWx5emVyLFxuICBPcHRpbWl6YXRpb25DYXRlZ29yeSxcbiAgUmVjb21tZW5kYXRpb24sXG59IGZyb20gJy4uL3R5cGVzJztcblxuY29uc3QgTkFUX0dBVEVXQVlfSE9VUkxZID0gMC4wNDU7XG5jb25zdCBOQVRfSU5TVEFOQ0VfVDNfTkFOT19IT1VSTFkgPSAwLjAwNTI7XG5jb25zdCBIT1VSU19QRVJfTU9OVEggPSA3MzA7XG5cbmV4cG9ydCBjbGFzcyBOQVRHYXRld2F5T3B0aW1pemF0aW9uQW5hbHl6ZXIgaW1wbGVtZW50cyBPcHRpbWl6YXRpb25BbmFseXplciB7XG4gIHJlYWRvbmx5IGNhdGVnb3J5OiBPcHRpbWl6YXRpb25DYXRlZ29yeSA9ICduYXQtZ2F0ZXdheS1vcHRpbWl6YXRpb24nO1xuICByZWFkb25seSBuYW1lID0gJ05BVCBHYXRld2F5IE9wdGltaXphdGlvbic7XG5cbiAgaXNBcHBsaWNhYmxlKHJlc291cmNlczogUmVzb3VyY2VXaXRoSWRbXSk6IGJvb2xlYW4ge1xuICAgIHJldHVybiByZXNvdXJjZXMuc29tZSgocikgPT4gci50eXBlID09PSAnQVdTOjpFQzI6Ok5hdEdhdGV3YXknKTtcbiAgfVxuXG4gIGFzeW5jIGFuYWx5emUoXG4gICAgcmVzb3VyY2VzOiBSZXNvdXJjZVdpdGhJZFtdLFxuICAgIHJlc291cmNlQ29zdHM6IFJlc291cmNlQ29zdFtdLFxuICAgIF9yZWdpb246IHN0cmluZyxcbiAgKTogUHJvbWlzZTxSZWNvbW1lbmRhdGlvbltdPiB7XG4gICAgY29uc3QgcmVjb21tZW5kYXRpb25zOiBSZWNvbW1lbmRhdGlvbltdID0gW107XG4gICAgY29uc3QgbmF0R2F0ZXdheXMgPSByZXNvdXJjZXMuZmlsdGVyKFxuICAgICAgKHIpID0+IHIudHlwZSA9PT0gJ0FXUzo6RUMyOjpOYXRHYXRld2F5JyxcbiAgICApO1xuXG4gICAgZm9yIChjb25zdCBuYXRHdyBvZiBuYXRHYXRld2F5cykge1xuICAgICAgcmVjb21tZW5kYXRpb25zLnB1c2goXG4gICAgICAgIHRoaXMuc3VnZ2VzdE5BVEluc3RhbmNlRm9yRGV2KG5hdEd3LCByZXNvdXJjZUNvc3RzKSxcbiAgICAgICk7XG4gICAgfVxuXG4gICAgY29uc3QgdnBjRW5kcG9pbnRSZWMgPSB0aGlzLnN1Z2dlc3RWUENFbmRwb2ludHMocmVzb3VyY2VzLCBuYXRHYXRld2F5cyk7XG4gICAgaWYgKHZwY0VuZHBvaW50UmVjKSB7XG4gICAgICByZWNvbW1lbmRhdGlvbnMucHVzaCh2cGNFbmRwb2ludFJlYyk7XG4gICAgfVxuXG4gICAgaWYgKG5hdEdhdGV3YXlzLmxlbmd0aCA+IDEpIHtcbiAgICAgIGNvbnN0IGNvbnNvbGlkYXRpb25SZWMgPSB0aGlzLnN1Z2dlc3RDb25zb2xpZGF0aW9uKFxuICAgICAgICBuYXRHYXRld2F5cyxcbiAgICAgICAgcmVzb3VyY2VDb3N0cyxcbiAgICAgICk7XG4gICAgICBpZiAoY29uc29saWRhdGlvblJlYykge1xuICAgICAgICByZWNvbW1lbmRhdGlvbnMucHVzaChjb25zb2xpZGF0aW9uUmVjKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gcmVjb21tZW5kYXRpb25zO1xuICB9XG5cbiAgcHJpdmF0ZSBzdWdnZXN0TkFUSW5zdGFuY2VGb3JEZXYoXG4gICAgbmF0R3c6IFJlc291cmNlV2l0aElkLFxuICAgIHJlc291cmNlQ29zdHM6IFJlc291cmNlQ29zdFtdLFxuICApOiBSZWNvbW1lbmRhdGlvbiB7XG4gICAgY29uc3QgbmF0Q29zdCA9IHRoaXMuZmluZENvc3QobmF0R3cubG9naWNhbElkLCByZXNvdXJjZUNvc3RzKTtcbiAgICBjb25zdCBuYXRJbnN0YW5jZUNvc3QgPSBOQVRfSU5TVEFOQ0VfVDNfTkFOT19IT1VSTFkgKiBIT1VSU19QRVJfTU9OVEg7XG4gICAgY29uc3Qgc2F2aW5ncyA9IG5hdENvc3QgPiAwXG4gICAgICA/IG5hdENvc3QgLSBuYXRJbnN0YW5jZUNvc3RcbiAgICAgIDogKE5BVF9HQVRFV0FZX0hPVVJMWSAqIEhPVVJTX1BFUl9NT05USCkgLSBuYXRJbnN0YW5jZUNvc3Q7XG4gICAgY29uc3Qgc2F2aW5nc1BlcmNlbnQgPSBuYXRDb3N0ID4gMFxuICAgICAgPyBNYXRoLnJvdW5kKChzYXZpbmdzIC8gbmF0Q29zdCkgKiAxMDApXG4gICAgICA6IDg4O1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIGlkOiBgbmF0LWluc3RhbmNlLSR7bmF0R3cubG9naWNhbElkfWAsXG4gICAgICB0aXRsZTogYFJlcGxhY2UgJHtuYXRHdy5sb2dpY2FsSWR9IHdpdGggTkFUIGluc3RhbmNlIChkZXYvdGVzdClgLFxuICAgICAgZGVzY3JpcHRpb246IGBOQVQgR2F0ZXdheXMgY29zdCB+JCR7KE5BVF9HQVRFV0FZX0hPVVJMWSAqIEhPVVJTX1BFUl9NT05USCkudG9GaXhlZCgyKX0vbW9udGguIEEgdDMubmFubyBOQVQgaW5zdGFuY2UgY29zdHMgfiQke25hdEluc3RhbmNlQ29zdC50b0ZpeGVkKDIpfS9tb250aC4gRm9yIG5vbi1wcm9kdWN0aW9uIGVudmlyb25tZW50cywgYSBOQVQgaW5zdGFuY2UgcHJvdmlkZXMgc2lnbmlmaWNhbnQgc2F2aW5ncy5gLFxuICAgICAgY2F0ZWdvcnk6IHRoaXMuY2F0ZWdvcnksXG4gICAgICBwcmlvcml0eTogJ21lZGl1bScsXG4gICAgICBlc3RpbWF0ZWRNb250aGx5U2F2aW5nczogTWF0aC5yb3VuZChzYXZpbmdzICogMTAwKSAvIDEwMCxcbiAgICAgIGVzdGltYXRlZFNhdmluZ3NQZXJjZW50OiBzYXZpbmdzUGVyY2VudCxcbiAgICAgIGFmZmVjdGVkUmVzb3VyY2VzOiBbbmF0R3cubG9naWNhbElkXSxcbiAgICAgIGFjdGlvbkl0ZW1zOiBbXG4gICAgICAgICdSZXBsYWNlIE5BVCBHYXRld2F5IHdpdGggYSB0My5uYW5vIG9yIHQ0Zy5uYW5vIEVDMiBpbnN0YW5jZSBjb25maWd1cmVkIGFzIE5BVCcsXG4gICAgICAgICdFbmFibGUgc291cmNlL2Rlc3RpbmF0aW9uIGNoZWNrIGRpc2FibGUgb24gdGhlIE5BVCBpbnN0YW5jZScsXG4gICAgICAgICdVcGRhdGUgcm91dGUgdGFibGVzIHRvIHBvaW50IHRvIHRoZSBOQVQgaW5zdGFuY2UnLFxuICAgICAgICAnQ29uZmlndXJlIGF1dG8tcmVjb3ZlcnkgZm9yIHRoZSBOQVQgaW5zdGFuY2UnLFxuICAgICAgXSxcbiAgICAgIGNhdmVhdHM6IFtcbiAgICAgICAgJ05vdCByZWNvbW1lbmRlZCBmb3IgcHJvZHVjdGlvbiAoc2luZ2xlIHBvaW50IG9mIGZhaWx1cmUpJyxcbiAgICAgICAgJ0xvd2VyIHRocm91Z2hwdXQgYW5kIG5vIGJ1aWx0LWluIHJlZHVuZGFuY3knLFxuICAgICAgICAnUmVxdWlyZXMgbWFudWFsIHBhdGNoaW5nIGFuZCBtYWludGVuYW5jZScsXG4gICAgICAgICdPbmx5IHN1aXRhYmxlIGZvciBkZXYvdGVzdC9zdGFnaW5nIGVudmlyb25tZW50cycsXG4gICAgICBdLFxuICAgIH07XG4gIH1cblxuICBwcml2YXRlIHN1Z2dlc3RWUENFbmRwb2ludHMoXG4gICAgcmVzb3VyY2VzOiBSZXNvdXJjZVdpdGhJZFtdLFxuICAgIG5hdEdhdGV3YXlzOiBSZXNvdXJjZVdpdGhJZFtdLFxuICApOiBSZWNvbW1lbmRhdGlvbiB8IG51bGwge1xuICAgIGNvbnN0IGhhc1MzID0gcmVzb3VyY2VzLnNvbWUoKHIpID0+IHIudHlwZSA9PT0gJ0FXUzo6UzM6OkJ1Y2tldCcpO1xuICAgIGNvbnN0IGhhc0R5bmFtb0RCID0gcmVzb3VyY2VzLnNvbWUoXG4gICAgICAocikgPT4gci50eXBlID09PSAnQVdTOjpEeW5hbW9EQjo6VGFibGUnLFxuICAgICk7XG5cbiAgICBpZiAoIWhhc1MzICYmICFoYXNEeW5hbW9EQikgcmV0dXJuIG51bGw7XG5cbiAgICBjb25zdCBleGlzdGluZ0dhdGV3YXlFbmRwb2ludHMgPSByZXNvdXJjZXMuZmlsdGVyKChyKSA9PiB7XG4gICAgICBpZiAoci50eXBlICE9PSAnQVdTOjpFQzI6OlZQQ0VuZHBvaW50JykgcmV0dXJuIGZhbHNlO1xuICAgICAgY29uc3QgdnBjRW5kcG9pbnRUeXBlID0gci5wcm9wZXJ0aWVzLlZwY0VuZHBvaW50VHlwZSBhcyBzdHJpbmc7XG4gICAgICByZXR1cm4gIXZwY0VuZHBvaW50VHlwZSB8fCB2cGNFbmRwb2ludFR5cGUgPT09ICdHYXRld2F5JztcbiAgICB9KTtcblxuICAgIGNvbnN0IGV4aXN0aW5nU2VydmljZXMgPSBleGlzdGluZ0dhdGV3YXlFbmRwb2ludHMubWFwKFxuICAgICAgKHIpID0+IHIucHJvcGVydGllcy5TZXJ2aWNlTmFtZSBhcyBzdHJpbmcsXG4gICAgKTtcbiAgICBjb25zdCBtaXNzaW5nRW5kcG9pbnRzOiBzdHJpbmdbXSA9IFtdO1xuXG4gICAgaWYgKFxuICAgICAgaGFzUzMgJiZcbiAgICAgICFleGlzdGluZ1NlcnZpY2VzLnNvbWUoKHMpID0+IHM/LmluY2x1ZGVzKCdzMycpKVxuICAgICkge1xuICAgICAgbWlzc2luZ0VuZHBvaW50cy5wdXNoKCdjb20uYW1hem9uYXdzLlJFR0lPTi5zMycpO1xuICAgIH1cbiAgICBpZiAoXG4gICAgICBoYXNEeW5hbW9EQiAmJlxuICAgICAgIWV4aXN0aW5nU2VydmljZXMuc29tZSgocykgPT4gcz8uaW5jbHVkZXMoJ2R5bmFtb2RiJykpXG4gICAgKSB7XG4gICAgICBtaXNzaW5nRW5kcG9pbnRzLnB1c2goJ2NvbS5hbWF6b25hd3MuUkVHSU9OLmR5bmFtb2RiJyk7XG4gICAgfVxuXG4gICAgaWYgKG1pc3NpbmdFbmRwb2ludHMubGVuZ3RoID09PSAwKSByZXR1cm4gbnVsbDtcblxuICAgIGNvbnN0IG5hdElkcyA9IG5hdEdhdGV3YXlzLm1hcCgobikgPT4gbi5sb2dpY2FsSWQpO1xuICAgIGNvbnN0IHNlcnZpY2VzID0gbWlzc2luZ0VuZHBvaW50c1xuICAgICAgLm1hcCgocykgPT4gcy5zcGxpdCgnLicpLnBvcCgpKVxuICAgICAgLmpvaW4oJywgJyk7XG5cbiAgICByZXR1cm4ge1xuICAgICAgaWQ6ICd2cGMtZ2F0ZXdheS1lbmRwb2ludHMnLFxuICAgICAgdGl0bGU6IGBBZGQgVlBDIEdhdGV3YXkgRW5kcG9pbnRzIGZvciAke3NlcnZpY2VzfWAsXG4gICAgICBkZXNjcmlwdGlvbjogYFMzIGFuZCBEeW5hbW9EQiBHYXRld2F5IEVuZHBvaW50cyBhcmUgZnJlZSBhbmQgcm91dGUgdHJhZmZpYyBkaXJlY3RseSB0byB0aGUgc2VydmljZSwgYnlwYXNzaW5nIE5BVCBHYXRld2F5LiBUaGlzIGVsaW1pbmF0ZXMgTkFUIEdhdGV3YXkgZGF0YSBwcm9jZXNzaW5nIGNoYXJnZXMgKCQwLjA0NS9HQikgZm9yIHRoZXNlIHNlcnZpY2VzLmAsXG4gICAgICBjYXRlZ29yeTogdGhpcy5jYXRlZ29yeSxcbiAgICAgIHByaW9yaXR5OiAnaGlnaCcsXG4gICAgICBlc3RpbWF0ZWRNb250aGx5U2F2aW5nczogMCxcbiAgICAgIGVzdGltYXRlZFNhdmluZ3NQZXJjZW50OiAwLFxuICAgICAgYWZmZWN0ZWRSZXNvdXJjZXM6IG5hdElkcyxcbiAgICAgIGFjdGlvbkl0ZW1zOiBtaXNzaW5nRW5kcG9pbnRzLm1hcChcbiAgICAgICAgKGVwKSA9PiBgQWRkIFZQQyBHYXRld2F5IEVuZHBvaW50IGZvciAke2VwfWAsXG4gICAgICApLFxuICAgICAgY2F2ZWF0czogW1xuICAgICAgICAnU2F2aW5ncyBkZXBlbmQgb24gZGF0YSB0cmFuc2ZlciB2b2x1bWUgdG8gUzMvRHluYW1vREInLFxuICAgICAgICAnR2F0ZXdheSBFbmRwb2ludHMgYXJlIGZyZWU7IHNhdmluZ3MgY29tZSBmcm9tIHJlZHVjZWQgTkFUIEdhdGV3YXkgZGF0YSBwcm9jZXNzaW5nJyxcbiAgICAgICAgJ1VwZGF0ZSByb3V0ZSB0YWJsZXMgdG8gaW5jbHVkZSB0aGUgR2F0ZXdheSBFbmRwb2ludCcsXG4gICAgICBdLFxuICAgIH07XG4gIH1cblxuICBwcml2YXRlIHN1Z2dlc3RDb25zb2xpZGF0aW9uKFxuICAgIG5hdEdhdGV3YXlzOiBSZXNvdXJjZVdpdGhJZFtdLFxuICAgIHJlc291cmNlQ29zdHM6IFJlc291cmNlQ29zdFtdLFxuICApOiBSZWNvbW1lbmRhdGlvbiB8IG51bGwge1xuICAgIGlmIChuYXRHYXRld2F5cy5sZW5ndGggPD0gMSkgcmV0dXJuIG51bGw7XG5cbiAgICBjb25zdCB0b3RhbENvc3QgPSBuYXRHYXRld2F5cy5yZWR1Y2UoXG4gICAgICAoc3VtLCBuZykgPT4gc3VtICsgdGhpcy5maW5kQ29zdChuZy5sb2dpY2FsSWQsIHJlc291cmNlQ29zdHMpLFxuICAgICAgMCxcbiAgICApO1xuICAgIGNvbnN0IHBlckdhdGV3YXlDb3N0ID1cbiAgICAgIHRvdGFsQ29zdCA+IDBcbiAgICAgICAgPyB0b3RhbENvc3QgLyBuYXRHYXRld2F5cy5sZW5ndGhcbiAgICAgICAgOiBOQVRfR0FURVdBWV9IT1VSTFkgKiBIT1VSU19QRVJfTU9OVEg7XG4gICAgY29uc3Qgc2F2aW5ncyA9IHBlckdhdGV3YXlDb3N0ICogKG5hdEdhdGV3YXlzLmxlbmd0aCAtIDEpO1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIGlkOiAnbmF0LWNvbnNvbGlkYXRpb24nLFxuICAgICAgdGl0bGU6IGBDb25zb2xpZGF0ZSAke25hdEdhdGV3YXlzLmxlbmd0aH0gTkFUIEdhdGV3YXlzIChkZXYvdGVzdClgLFxuICAgICAgZGVzY3JpcHRpb246IGAke25hdEdhdGV3YXlzLmxlbmd0aH0gTkFUIEdhdGV3YXlzIGRldGVjdGVkLiBGb3Igbm9uLXByb2R1Y3Rpb24gZW52aXJvbm1lbnRzLCBhIHNpbmdsZSBOQVQgR2F0ZXdheSBtYXkgYmUgc3VmZmljaWVudCwgc2F2aW5nIH4kJHtzYXZpbmdzLnRvRml4ZWQoMil9L21vbnRoLmAsXG4gICAgICBjYXRlZ29yeTogdGhpcy5jYXRlZ29yeSxcbiAgICAgIHByaW9yaXR5OiBzYXZpbmdzID49IDUwID8gJ2hpZ2gnIDogJ21lZGl1bScsXG4gICAgICBlc3RpbWF0ZWRNb250aGx5U2F2aW5nczogTWF0aC5yb3VuZChzYXZpbmdzICogMTAwKSAvIDEwMCxcbiAgICAgIGVzdGltYXRlZFNhdmluZ3NQZXJjZW50OiBNYXRoLnJvdW5kKFxuICAgICAgICAoKG5hdEdhdGV3YXlzLmxlbmd0aCAtIDEpIC8gbmF0R2F0ZXdheXMubGVuZ3RoKSAqIDEwMCxcbiAgICAgICksXG4gICAgICBhZmZlY3RlZFJlc291cmNlczogbmF0R2F0ZXdheXMubWFwKChuKSA9PiBuLmxvZ2ljYWxJZCksXG4gICAgICBhY3Rpb25JdGVtczogW1xuICAgICAgICAnRXZhbHVhdGUgaWYgbXVsdGktQVogTkFUIEdhdGV3YXkgcmVkdW5kYW5jeSBpcyBuZWVkZWQgZm9yIHRoaXMgZW52aXJvbm1lbnQnLFxuICAgICAgICAnUm91dGUgYWxsIHByaXZhdGUgc3VibmV0cyB0aHJvdWdoIGEgc2luZ2xlIE5BVCBHYXRld2F5JyxcbiAgICAgICAgYFJlbW92ZSAke25hdEdhdGV3YXlzLmxlbmd0aCAtIDF9IHJlZHVuZGFudCBOQVQgR2F0ZXdheXNgLFxuICAgICAgXSxcbiAgICAgIGNhdmVhdHM6IFtcbiAgICAgICAgJ05vdCByZWNvbW1lbmRlZCBmb3IgcHJvZHVjdGlvbiAoc2luZ2xlIEFaIGZhaWx1cmUgcmlzayknLFxuICAgICAgICAnQ3Jvc3MtQVogZGF0YSB0cmFuc2ZlciBjaGFyZ2VzIG1heSBhcHBseSAofiQwLjAxL0dCKScsXG4gICAgICAgICdPbmx5IHN1aXRhYmxlIGZvciBub24tcHJvZHVjdGlvbiBlbnZpcm9ubWVudHMnLFxuICAgICAgXSxcbiAgICB9O1xuICB9XG5cbiAgcHJpdmF0ZSBmaW5kQ29zdChsb2dpY2FsSWQ6IHN0cmluZywgcmVzb3VyY2VDb3N0czogUmVzb3VyY2VDb3N0W10pOiBudW1iZXIge1xuICAgIGNvbnN0IGNvc3QgPSByZXNvdXJjZUNvc3RzLmZpbmQoKGMpID0+IGMubG9naWNhbElkID09PSBsb2dpY2FsSWQpO1xuICAgIHJldHVybiBjb3N0Py5tb250aGx5Q29zdC5hbW91bnQgPz8gMDtcbiAgfVxufVxuIl19
@@ -0,0 +1,13 @@
1
+ import { ResourceWithId } from '../../diff/types';
2
+ import { ResourceCost } from '../../pricing/types';
3
+ import { OptimizationAnalyzer, OptimizationCategory, Recommendation } from '../types';
4
+ export declare class ReservedInstanceAnalyzer implements OptimizationAnalyzer {
5
+ readonly category: OptimizationCategory;
6
+ readonly name = "Reserved Instance";
7
+ isApplicable(resources: ResourceWithId[]): boolean;
8
+ analyze(resources: ResourceWithId[], resourceCosts: ResourceCost[], _region: string): Promise<Recommendation[]>;
9
+ private createRecommendation;
10
+ private getServiceLabel;
11
+ private getInstanceInfo;
12
+ private findCost;
13
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReservedInstanceAnalyzer = void 0;
4
+ /**
5
+ * Approximate RI discount percentages by term and payment option.
6
+ * These vary by instance type and region but provide reasonable estimates.
7
+ */
8
+ const RI_DISCOUNTS = {
9
+ '1yr-no-upfront': 30,
10
+ '1yr-partial-upfront': 35,
11
+ '1yr-all-upfront': 40,
12
+ '3yr-no-upfront': 45,
13
+ '3yr-partial-upfront': 50,
14
+ '3yr-all-upfront': 60,
15
+ };
16
+ const DEFAULT_DISCOUNT_KEY = '1yr-no-upfront';
17
+ const RI_ELIGIBLE_TYPES = [
18
+ 'AWS::EC2::Instance',
19
+ 'AWS::RDS::DBInstance',
20
+ 'AWS::RDS::DBCluster',
21
+ 'AWS::ElastiCache::CacheCluster',
22
+ 'AWS::ElastiCache::ReplicationGroup',
23
+ 'AWS::Redshift::Cluster',
24
+ 'AWS::OpenSearchService::Domain',
25
+ 'AWS::Elasticsearch::Domain',
26
+ ];
27
+ const MINIMUM_MONTHLY_COST = 50;
28
+ class ReservedInstanceAnalyzer {
29
+ category = 'reserved-instance';
30
+ name = 'Reserved Instance';
31
+ isApplicable(resources) {
32
+ return resources.some((r) => RI_ELIGIBLE_TYPES.includes(r.type));
33
+ }
34
+ async analyze(resources, resourceCosts, _region) {
35
+ const recommendations = [];
36
+ for (const resource of resources) {
37
+ if (!RI_ELIGIBLE_TYPES.includes(resource.type))
38
+ continue;
39
+ const cost = this.findCost(resource.logicalId, resourceCosts);
40
+ if (cost < MINIMUM_MONTHLY_COST)
41
+ continue;
42
+ recommendations.push(this.createRecommendation(resource, cost));
43
+ }
44
+ return recommendations;
45
+ }
46
+ createRecommendation(resource, currentCost) {
47
+ const discountPercent = RI_DISCOUNTS[DEFAULT_DISCOUNT_KEY];
48
+ const savings = currentCost * (discountPercent / 100);
49
+ const newCost = currentCost - savings;
50
+ const serviceLabel = this.getServiceLabel(resource.type);
51
+ const instanceInfo = this.getInstanceInfo(resource);
52
+ return {
53
+ id: `ri-${resource.logicalId}`,
54
+ title: `Purchase Reserved Instance for ${resource.logicalId}`,
55
+ description: `${serviceLabel} ${resource.logicalId}${instanceInfo} costs $${currentCost.toFixed(2)}/month on-demand. A 1-year No Upfront RI would reduce this to ~$${newCost.toFixed(2)}/month, saving ${discountPercent}%.`,
56
+ category: this.category,
57
+ priority: savings >= 100 ? 'high' : savings >= 50 ? 'medium' : 'low',
58
+ estimatedMonthlySavings: Math.round(savings * 100) / 100,
59
+ estimatedSavingsPercent: discountPercent,
60
+ affectedResources: [resource.logicalId],
61
+ actionItems: [
62
+ `Purchase a 1-year No Upfront Reserved Instance for ${serviceLabel}`,
63
+ 'Evaluate if 3-year term or partial/all upfront payment yields better savings',
64
+ 'Consider Convertible RIs for flexibility to change instance types',
65
+ ],
66
+ caveats: [
67
+ '1-year commitment; early termination may result in charges',
68
+ 'Savings percentages are approximate and vary by region and instance type',
69
+ 'Ensure workload will run for the full term to realize savings',
70
+ `Based on ${DEFAULT_DISCOUNT_KEY.replace(/-/g, ' ')} pricing`,
71
+ ],
72
+ };
73
+ }
74
+ getServiceLabel(resourceType) {
75
+ if (resourceType.startsWith('AWS::EC2::'))
76
+ return 'EC2 instance';
77
+ if (resourceType.startsWith('AWS::RDS::'))
78
+ return 'RDS instance';
79
+ if (resourceType.startsWith('AWS::ElastiCache::'))
80
+ return 'ElastiCache node';
81
+ if (resourceType.startsWith('AWS::Redshift::'))
82
+ return 'Redshift cluster';
83
+ if (resourceType.includes('OpenSearch') || resourceType.includes('Elasticsearch')) {
84
+ return 'OpenSearch domain';
85
+ }
86
+ return 'Resource';
87
+ }
88
+ getInstanceInfo(resource) {
89
+ const props = resource.properties;
90
+ const instanceType = props.InstanceType ||
91
+ props.DBInstanceClass ||
92
+ props.CacheNodeType ||
93
+ props.NodeType;
94
+ return instanceType ? ` (${instanceType})` : '';
95
+ }
96
+ findCost(logicalId, resourceCosts) {
97
+ const cost = resourceCosts.find((c) => c.logicalId === logicalId);
98
+ return cost?.monthlyCost.amount ?? 0;
99
+ }
100
+ }
101
+ exports.ReservedInstanceAnalyzer = ReservedInstanceAnalyzer;
102
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUmVzZXJ2ZWRJbnN0YW5jZUFuYWx5emVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL29wdGltaXphdGlvbi9hbmFseXplcnMvUmVzZXJ2ZWRJbnN0YW5jZUFuYWx5emVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQVFBOzs7R0FHRztBQUNILE1BQU0sWUFBWSxHQUEyQjtJQUMzQyxnQkFBZ0IsRUFBRSxFQUFFO0lBQ3BCLHFCQUFxQixFQUFFLEVBQUU7SUFDekIsaUJBQWlCLEVBQUUsRUFBRTtJQUNyQixnQkFBZ0IsRUFBRSxFQUFFO0lBQ3BCLHFCQUFxQixFQUFFLEVBQUU7SUFDekIsaUJBQWlCLEVBQUUsRUFBRTtDQUN0QixDQUFDO0FBRUYsTUFBTSxvQkFBb0IsR0FBRyxnQkFBZ0IsQ0FBQztBQUU5QyxNQUFNLGlCQUFpQixHQUFHO0lBQ3hCLG9CQUFvQjtJQUNwQixzQkFBc0I7SUFDdEIscUJBQXFCO0lBQ3JCLGdDQUFnQztJQUNoQyxvQ0FBb0M7SUFDcEMsd0JBQXdCO0lBQ3hCLGdDQUFnQztJQUNoQyw0QkFBNEI7Q0FDN0IsQ0FBQztBQUVGLE1BQU0sb0JBQW9CLEdBQUcsRUFBRSxDQUFDO0FBRWhDLE1BQWEsd0JBQXdCO0lBQzFCLFFBQVEsR0FBeUIsbUJBQW1CLENBQUM7SUFDckQsSUFBSSxHQUFHLG1CQUFtQixDQUFDO0lBRXBDLFlBQVksQ0FBQyxTQUEyQjtRQUN0QyxPQUFPLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLGlCQUFpQixDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUNuRSxDQUFDO0lBRUQsS0FBSyxDQUFDLE9BQU8sQ0FDWCxTQUEyQixFQUMzQixhQUE2QixFQUM3QixPQUFlO1FBRWYsTUFBTSxlQUFlLEdBQXFCLEVBQUUsQ0FBQztRQUU3QyxLQUFLLE1BQU0sUUFBUSxJQUFJLFNBQVMsRUFBRSxDQUFDO1lB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TUFBTSxLQUFLLEdBQUcsUUFBUSxDQUFDLFVBQVUsQ0FBQztRQUNsQyxNQUFNLFlBQVksR0FDZixLQUFLLENBQUMsWUFBdUI7WUFDN0IsS0FBSyxDQUFDLGVBQTBCO1lBQ2hDLEtBQUssQ0FBQyxhQUF3QjtZQUM5QixLQUFLLENBQUMsUUFBbUIsQ0FBQztRQUM3QixPQUFPLFlBQVksQ0FBQyxDQUFDLENBQUMsS0FBSyxZQUFZLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ2xELENBQUM7SUFFTyxRQUFRLENBQUMsU0FBaUIsRUFBRSxhQUE2QjtRQUMvRCxNQUFNLElBQUksR0FBRyxhQUFhLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxLQUFLLFNBQVMsQ0FBQyxDQUFDO1FBQ2xFLE9BQU8sSUFBSSxFQUFFLFdBQVcsQ0FBQyxNQUFNLElBQUksQ0FBQyxDQUFDO0lBQ3ZDLENBQUM7Q0FDRjtBQXJGRCw0REFxRkMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBSZXNvdXJjZVdpdGhJZCB9IGZyb20gJy4uLy4uL2RpZmYvdHlwZXMnO1xuaW1wb3J0IHsgUmVzb3VyY2VDb3N0IH0gZnJvbSAnLi4vLi4vcHJpY2luZy90eXBlcyc7XG5pbXBvcnQge1xuICBPcHRpbWl6YXRpb25BbmFseXplcixcbiAgT3B0aW1pemF0aW9uQ2F0ZWdvcnksXG4gIFJlY29tbWVuZGF0aW9uLFxufSBmcm9tICcuLi90eXBlcyc7XG5cbi8qKlxuICogQXBwcm94aW1hdGUgUkkgZGlzY291bnQgcGVyY2VudGFnZXMgYnkgdGVybSBhbmQgcGF5bWVudCBvcHRpb24uXG4gKiBUaGVzZSB2YXJ5IGJ5IGluc3RhbmNlIHR5cGUgYW5kIHJlZ2lvbiBidXQgcHJvdmlkZSByZWFzb25hYmxlIGVzdGltYXRlcy5cbiAqL1xuY29uc3QgUklfRElTQ09VTlRTOiBSZWNvcmQ8c3RyaW5nLCBudW1iZXI+ID0ge1xuICAnMXlyLW5vLXVwZnJvbnQnOiAzMCxcbiAgJzF5ci1wYXJ0aWFsLXVwZnJvbnQnOiAzNSxcbiAgJzF5ci1hbGwtdXBmcm9udCc6IDQwLFxuICAnM3lyLW5vLXVwZnJvbnQnOiA0NSxcbiAgJzN5ci1wYXJ0aWFsLXVwZnJvbnQnOiA1MCxcbiAgJzN5ci1hbGwtdXBmcm9udCc6IDYwLFxufTtcblxuY29uc3QgREVGQVVMVF9ESVNDT1VOVF9LRVkgPSAnMXlyLW5vLXVwZnJvbnQnO1xuXG5jb25zdCBSSV9FTElHSUJMRV9UWVBFUyA9IFtcbiAgJ0FXUzo6RUMyOjpJbnN0YW5jZScsXG4gICdBV1M6OlJEUzo6REJJbnN0YW5jZScsXG4gICdBV1M6OlJEUzo6REJDbHVzdGVyJyxcbiAgJ0FXUzo6RWxhc3RpQ2FjaGU6OkNhY2hlQ2x1c3RlcicsXG4gICdBV1M6OkVsYXN0aUNhY2hlOjpSZXBsaWNhdGlvbkdyb3VwJyxcbiAgJ0FXUzo6UmVkc2hpZnQ6OkNsdXN0ZXInLFxuICAnQVdTOjpPcGVuU2VhcmNoU2VydmljZTo6RG9tYWluJyxcbiAgJ0FXUzo6RWxhc3RpY3NlYXJjaDo6RG9tYWluJyxcbl07XG5cbmNvbnN0IE1JTklNVU1fTU9OVEhMWV9DT1NUID0gNTA7XG5cbmV4cG9ydCBjbGFzcyBSZXNlcnZlZEluc3RhbmNlQW5hbHl6ZXIgaW1wbGVtZW50cyBPcHRpbWl6YXRpb25BbmFseXplciB7XG4gIHJlYWRvbmx5IGNhdGVnb3J5OiBPcHRpbWl6YXRpb25DYXRlZ29yeSA9ICdyZXNlcnZlZC1pbnN0YW5jZSc7XG4gIHJlYWRvbmx5IG5hbWUgPSAnUmVzZXJ2ZWQgSW5zdGFuY2UnO1xuXG4gIGlzQXBwbGljYWJsZShyZXNvdXJjZXM6IFJlc291cmNlV2l0aElkW10pOiBib29sZWFuIHtcbiAgICByZXR1cm4gcmVzb3VyY2VzLnNvbWUoKHIpID0+IFJJX0VMSUdJQkxFX1RZUEVTLmluY2x1ZGVzKHIudHlwZSkpO1xuICB9XG5cbiAgYXN5bmMgYW5hbHl6ZShcbiAgICByZXNvdXJjZXM6IFJlc291cmNlV2l0aElkW10sXG4gICAgcmVzb3VyY2VDb3N0czogUmVzb3VyY2VDb3N0W10sXG4gICAgX3JlZ2lvbjogc3RyaW5nLFxuICApOiBQcm9taXNlPFJlY29tbWVuZGF0aW9uW10+IHtcbiAgICBjb25zdCByZWNvbW1lbmRhdGlvbnM6IFJlY29tbWVuZGF0aW9uW10gPSBbXTtcblxuICAgIGZvciAoY29uc3QgcmVzb3VyY2Ugb2YgcmVzb3VyY2VzKSB7XG4gICAgICBpZiAoIVJJX0VMSUdJQkxFX1RZUEVTLmluY2x1ZGVzKHJlc291cmNlLnR5cGUpKSBjb250aW51ZTtcblxuICAgICAgY29uc3QgY29zdCA9IHRoaXMuZmluZENvc3QocmVzb3VyY2UubG9naWNhbElkLCByZXNvdXJjZUNvc3RzKTtcbiAgICAgIGlmIChjb3N0IDwgTUlOSU1VTV9NT05USExZX0NPU1QpIGNvbnRpbnVlO1xuXG4gICAgICByZWNvbW1lbmRhdGlvbnMucHVzaCh0aGlzLmNyZWF0ZVJlY29tbWVuZGF0aW9uKHJlc291cmNlLCBjb3N0KSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHJlY29tbWVuZGF0aW9ucztcbiAgfVxuXG4gIHByaXZhdGUgY3JlYXRlUmVjb21tZW5kYXRpb24oXG4gICAgcmVzb3VyY2U6IFJlc291cmNlV2l0aElkLFxuICAgIGN1cnJlbnRDb3N0OiBudW1iZXIsXG4gICk6IFJlY29tbWVuZGF0aW9uIHtcbiAgICBjb25zdCBkaXNjb3VudFBlcmNlbnQgPSBSSV9ESVNDT1VOVFNbREVGQVVMVF9ESVNDT1VOVF9LRVldO1xuICAgIGNvbnN0IHNhdmluZ3MgPSBjdXJyZW50Q29zdCAqIChkaXNjb3VudFBlcmNlbnQgLyAxMDApO1xuICAgIGNvbnN0IG5ld0Nvc3QgPSBjdXJyZW50Q29zdCAtIHNhdmluZ3M7XG4gICAgY29uc3Qgc2VydmljZUxhYmVsID0gdGhpcy5nZXRTZXJ2aWNlTGFiZWwocmVzb3VyY2UudHlwZSk7XG4gICAgY29uc3QgaW5zdGFuY2VJbmZvID0gdGhpcy5nZXRJbnN0YW5jZUluZm8ocmVzb3VyY2UpO1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIGlkOiBgcmktJHtyZXNvdXJjZS5sb2dpY2FsSWR9YCxcbiAgICAgIHRpdGxlOiBgUHVyY2hhc2UgUmVzZXJ2ZWQgSW5zdGFuY2UgZm9yICR7cmVzb3VyY2UubG9naWNhbElkfWAsXG4gICAgICBkZXNjcmlwdGlvbjogYCR7c2VydmljZUxhYmVsfSAke3Jlc291cmNlLmxvZ2ljYWxJZH0ke2luc3RhbmNlSW5mb30gY29zdHMgJCR7Y3VycmVudENvc3QudG9GaXhlZCgyKX0vbW9udGggb24tZGVtYW5kLiBBIDEteWVhciBObyBVcGZyb250IFJJIHdvdWxkIHJlZHVjZSB0aGlzIHRvIH4kJHtuZXdDb3N0LnRvRml4ZWQoMil9L21vbnRoLCBzYXZpbmcgJHtkaXNjb3VudFBlcmNlbnR9JS5gLFxuICAgICAgY2F0ZWdvcnk6IHRoaXMuY2F0ZWdvcnksXG4gICAgICBwcmlvcml0eTogc2F2aW5ncyA+PSAxMDAgPyAnaGlnaCcgOiBzYXZpbmdzID49IDUwID8gJ21lZGl1bScgOiAnbG93JyxcbiAgICAgIGVzdGltYXRlZE1vbnRobHlTYXZpbmdzOiBNYXRoLnJvdW5kKHNhdmluZ3MgKiAxMDApIC8gMTAwLFxuICAgICAgZXN0aW1hdGVkU2F2aW5nc1BlcmNlbnQ6IGRpc2NvdW50UGVyY2VudCxcbiAgICAgIGFmZmVjdGVkUmVzb3VyY2VzOiBbcmVzb3VyY2UubG9naWNhbElkXSxcbiAgICAgIGFjdGlvbkl0ZW1zOiBbXG4gICAgICAgIGBQdXJjaGFzZSBhIDEteWVhciBObyBVcGZyb250IFJlc2VydmVkIEluc3RhbmNlIGZvciAke3NlcnZpY2VMYWJlbH1gLFxuICAgICAgICAnRXZhbHVhdGUgaWYgMy15ZWFyIHRlcm0gb3IgcGFydGlhbC9hbGwgdXBmcm9udCBwYXltZW50IHlpZWxkcyBiZXR0ZXIgc2F2aW5ncycsXG4gICAgICAgICdDb25zaWRlciBDb252ZXJ0aWJsZSBSSXMgZm9yIGZsZXhpYmlsaXR5IHRvIGNoYW5nZSBpbnN0YW5jZSB0eXBlcycsXG4gICAgICBdLFxuICAgICAgY2F2ZWF0czogW1xuICAgICAgICAnMS15ZWFyIGNvbW1pdG1lbnQ7IGVhcmx5IHRlcm1pbmF0aW9uIG1heSByZXN1bHQgaW4gY2hhcmdlcycsXG4gICAgICAgICdTYXZpbmdzIHBlcmNlbnRhZ2VzIGFyZSBhcHByb3hpbWF0ZSBhbmQgdmFyeSBieSByZWdpb24gYW5kIGluc3RhbmNlIHR5cGUnLFxuICAgICAgICAnRW5zdXJlIHdvcmtsb2FkIHdpbGwgcnVuIGZvciB0aGUgZnVsbCB0ZXJtIHRvIHJlYWxpemUgc2F2aW5ncycsXG4gICAgICAgIGBCYXNlZCBvbiAke0RFRkFVTFRfRElTQ09VTlRfS0VZLnJlcGxhY2UoLy0vZywgJyAnKX0gcHJpY2luZ2AsXG4gICAgICBdLFxuICAgIH07XG4gIH1cblxuICBwcml2YXRlIGdldFNlcnZpY2VMYWJlbChyZXNvdXJjZVR5cGU6IHN0cmluZyk6IHN0cmluZyB7XG4gICAgaWYgKHJlc291cmNlVHlwZS5zdGFydHNXaXRoKCdBV1M6OkVDMjo6JykpIHJldHVybiAnRUMyIGluc3RhbmNlJztcbiAgICBpZiAocmVzb3VyY2VUeXBlLnN0YXJ0c1dpdGgoJ0FXUzo6UkRTOjonKSkgcmV0dXJuICdSRFMgaW5zdGFuY2UnO1xuICAgIGlmIChyZXNvdXJjZVR5cGUuc3RhcnRzV2l0aCgnQVdTOjpFbGFzdGlDYWNoZTo6JykpIHJldHVybiAnRWxhc3RpQ2FjaGUgbm9kZSc7XG4gICAgaWYgKHJlc291cmNlVHlwZS5zdGFydHNXaXRoKCdBV1M6OlJlZHNoaWZ0OjonKSkgcmV0dXJuICdSZWRzaGlmdCBjbHVzdGVyJztcbiAgICBpZiAocmVzb3VyY2VUeXBlLmluY2x1ZGVzKCdPcGVuU2VhcmNoJykgfHwgcmVzb3VyY2VUeXBlLmluY2x1ZGVzKCdFbGFzdGljc2VhcmNoJykpIHtcbiAgICAgIHJldHVybiAnT3BlblNlYXJjaCBkb21haW4nO1xuICAgIH1cbiAgICByZXR1cm4gJ1Jlc291cmNlJztcbiAgfVxuXG4gIHByaXZhdGUgZ2V0SW5zdGFuY2VJbmZvKHJlc291cmNlOiBSZXNvdXJjZVdpdGhJZCk6IHN0cmluZyB7XG4gICAgY29uc3QgcHJvcHMgPSByZXNvdXJjZS5wcm9wZXJ0aWVzO1xuICAgIGNvbnN0IGluc3RhbmNlVHlwZSA9XG4gICAgICAocHJvcHMuSW5zdGFuY2VUeXBlIGFzIHN0cmluZykgfHxcbiAgICAgIChwcm9wcy5EQkluc3RhbmNlQ2xhc3MgYXMgc3RyaW5nKSB8fFxuICAgICAgKHByb3BzLkNhY2hlTm9kZVR5cGUgYXMgc3RyaW5nKSB8fFxuICAgICAgKHByb3BzLk5vZGVUeXBlIGFzIHN0cmluZyk7XG4gICAgcmV0dXJuIGluc3RhbmNlVHlwZSA/IGAgKCR7aW5zdGFuY2VUeXBlfSlgIDogJyc7XG4gIH1cblxuICBwcml2YXRlIGZpbmRDb3N0KGxvZ2ljYWxJZDogc3RyaW5nLCByZXNvdXJjZUNvc3RzOiBSZXNvdXJjZUNvc3RbXSk6IG51bWJlciB7XG4gICAgY29uc3QgY29zdCA9IHJlc291cmNlQ29zdHMuZmluZCgoYykgPT4gYy5sb2dpY2FsSWQgPT09IGxvZ2ljYWxJZCk7XG4gICAgcmV0dXJuIGNvc3Q/Lm1vbnRobHlDb3N0LmFtb3VudCA/PyAwO1xuICB9XG59XG4iXX0=
@@ -0,0 +1,13 @@
1
+ import { ResourceWithId } from '../../diff/types';
2
+ import { ResourceCost } from '../../pricing/types';
3
+ import { OptimizationAnalyzer, OptimizationCategory, Recommendation } from '../types';
4
+ export declare class RightSizingAnalyzer implements OptimizationAnalyzer {
5
+ readonly category: OptimizationCategory;
6
+ readonly name = "Right-Sizing";
7
+ isApplicable(resources: ResourceWithId[]): boolean;
8
+ analyze(resources: ResourceWithId[], resourceCosts: ResourceCost[], _region: string): Promise<Recommendation[]>;
9
+ private analyzeResource;
10
+ private getInstanceType;
11
+ private extractSize;
12
+ private findCost;
13
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RightSizingAnalyzer = void 0;
4
+ /**
5
+ * Instance sizes ordered from smallest to largest.
6
+ * Used to suggest one size down for right-sizing.
7
+ */
8
+ const EC2_SIZES = [
9
+ 'nano',
10
+ 'micro',
11
+ 'small',
12
+ 'medium',
13
+ 'large',
14
+ 'xlarge',
15
+ '2xlarge',
16
+ '4xlarge',
17
+ '8xlarge',
18
+ '12xlarge',
19
+ '16xlarge',
20
+ '24xlarge',
21
+ '32xlarge',
22
+ '48xlarge',
23
+ 'metal',
24
+ ];
25
+ /**
26
+ * Instance sizes considered "large" enough to warrant right-sizing review.
27
+ * We only suggest downsizing for 2xlarge and above.
28
+ */
29
+ const REVIEW_THRESHOLD_INDEX = EC2_SIZES.indexOf('2xlarge');
30
+ class RightSizingAnalyzer {
31
+ category = 'right-sizing';
32
+ name = 'Right-Sizing';
33
+ isApplicable(resources) {
34
+ return resources.some((r) => r.type === 'AWS::EC2::Instance' ||
35
+ r.type === 'AWS::RDS::DBInstance' ||
36
+ r.type === 'AWS::ElastiCache::CacheCluster');
37
+ }
38
+ async analyze(resources, resourceCosts, _region) {
39
+ const recommendations = [];
40
+ for (const resource of resources) {
41
+ const rec = this.analyzeResource(resource, resourceCosts);
42
+ if (rec)
43
+ recommendations.push(rec);
44
+ }
45
+ return recommendations;
46
+ }
47
+ analyzeResource(resource, resourceCosts) {
48
+ const instanceType = this.getInstanceType(resource);
49
+ if (!instanceType)
50
+ return null;
51
+ const size = this.extractSize(instanceType);
52
+ const sizeIndex = EC2_SIZES.indexOf(size);
53
+ if (sizeIndex < REVIEW_THRESHOLD_INDEX)
54
+ return null;
55
+ const smallerSize = EC2_SIZES[sizeIndex - 1];
56
+ const family = instanceType.substring(0, instanceType.length - size.length);
57
+ const smallerType = `${family}${smallerSize}`;
58
+ const cost = this.findCost(resource.logicalId, resourceCosts);
59
+ // Downsizing by one step typically halves cost for compute instances
60
+ const savingsPercent = 50;
61
+ const savings = cost * (savingsPercent / 100);
62
+ return {
63
+ id: `rightsize-${resource.logicalId}`,
64
+ title: `Review sizing of ${resource.logicalId} (${instanceType})`,
65
+ description: `${resource.logicalId} uses ${instanceType}. Consider downsizing to ${smallerType} if utilization is consistently below 40%. This could save ~$${savings.toFixed(2)}/month.`,
66
+ category: this.category,
67
+ priority: savings >= 100 ? 'high' : savings >= 40 ? 'medium' : 'low',
68
+ estimatedMonthlySavings: Math.round(savings * 100) / 100,
69
+ estimatedSavingsPercent: savingsPercent,
70
+ affectedResources: [resource.logicalId],
71
+ actionItems: [
72
+ `Check CloudWatch metrics for CPU, memory, and network utilization`,
73
+ `If avg utilization < 40%, downsize from ${instanceType} to ${smallerType}`,
74
+ 'Test in staging before applying to production',
75
+ 'Monitor performance for at least 1 week after change',
76
+ ],
77
+ caveats: [
78
+ 'Requires CloudWatch data to validate; this is a template-based estimate',
79
+ 'Memory utilization requires CloudWatch Agent (not available by default)',
80
+ 'Peak usage patterns may differ from average',
81
+ 'Consider using AWS Compute Optimizer for data-driven recommendations',
82
+ ],
83
+ };
84
+ }
85
+ getInstanceType(resource) {
86
+ const props = resource.properties;
87
+ switch (resource.type) {
88
+ case 'AWS::EC2::Instance':
89
+ return props.InstanceType || null;
90
+ case 'AWS::RDS::DBInstance':
91
+ return props.DBInstanceClass || null;
92
+ case 'AWS::ElastiCache::CacheCluster':
93
+ return props.CacheNodeType || null;
94
+ default:
95
+ return null;
96
+ }
97
+ }
98
+ extractSize(instanceType) {
99
+ // "m5.2xlarge" -> "2xlarge", "db.r5.large" -> "large"
100
+ const parts = instanceType.split('.');
101
+ return parts[parts.length - 1] || '';
102
+ }
103
+ findCost(logicalId, resourceCosts) {
104
+ const cost = resourceCosts.find((c) => c.logicalId === logicalId);
105
+ return cost?.monthlyCost.amount ?? 0;
106
+ }
107
+ }
108
+ exports.RightSizingAnalyzer = RightSizingAnalyzer;
109
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUmlnaHRTaXppbmdBbmFseXplci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9vcHRpbWl6YXRpb24vYW5hbHl6ZXJzL1JpZ2h0U2l6aW5nQW5hbHl6ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBUUE7OztHQUdHO0FBQ0gsTUFBTSxTQUFTLEdBQUc7SUFDaEIsTUFBTTtJQUNOLE9BQU87SUFDUCxPQUFPO0lBQ1AsUUFBUTtJQUNSLE9BQU87SUFDUCxRQUFRO0lBQ1IsU0FBUztJQUNULFNBQVM7SUFDVCxTQUFTO0lBQ1QsVUFBVTtJQUNWLFVBQVU7SUFDVixVQUFVO0lBQ1YsVUFBVTtJQUNWLFVBQVU7SUFDVixPQUFPO0NBQ1IsQ0FBQztBQUVGOzs7R0FHRztBQUNILE1BQU0sc0JBQXNCLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUU1RCxNQUFhLG1CQUFtQjtJQUNyQixRQUFRLEdBQXlCLGNBQWMsQ0FBQztJQUNoRCxJQUFJLEdBQUcsY0FBYyxDQUFDO0lBRS9CLFlBQVksQ0FBQyxTQUEyQjtRQUN0QyxPQUFPLFNBQVMsQ0FBQyxJQUFJLENBQ25CLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FDSixDQUFDLENBQUMsSUFBSSxLQUFLLG9CQUFvQjtZQUMvQixDQUFDLENBQUMsSUFBSSxLQUFLLHNCQUFzQjtZQUNqQyxDQUFDLENBQUMsSUFBSSxLQUFLLGdDQUFnQyxDQUM5QyxDQUFDO0lBQ0osQ0FBQztJQUVELEtBQUssQ0FBQyxPQUFPLENBQ1gsU0FBMkIsRUFDM0IsYUFBNkIsRUFDN0IsT0FBZTtRQUVmLE1BQU0sZUFBZSxHQUFxQixFQUFFLENBQUM7UUFFN0MsS0FBSyxNQUFNLFFBQVEsSUFBSSxTQUFTLEVBQUUsQ0FBQztZQUNqQyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsZUFBZSxDQUFDLFFBQVEsRUFBRSxhQUFhLENBQUMsQ0FBQztZQUMxRCxJQUFJLEdBQUc7Z0JBQUUs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QUN0RCxNQUFNLEtBQUssR0FBRyxZQUFZLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3RDLE9BQU8sS0FBSyxDQUFDLEtBQUssQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ3ZDLENBQUM7SUFFTyxRQUFRLENBQUMsU0FBaUIsRUFBRSxhQUE2QjtRQUMvRCxNQUFNLElBQUksR0FBRyxhQUFhLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxLQUFLLFNBQVMsQ0FBQyxDQUFDO1FBQ2xFLE9BQU8sSUFBSSxFQUFFLFdBQVcsQ0FBQyxNQUFNLElBQUksQ0FBQyxDQUFDO0lBQ3ZDLENBQUM7Q0FDRjtBQW5HRCxrREFtR0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBSZXNvdXJjZVdpdGhJZCB9IGZyb20gJy4uLy4uL2RpZmYvdHlwZXMnO1xuaW1wb3J0IHsgUmVzb3VyY2VDb3N0IH0gZnJvbSAnLi4vLi4vcHJpY2luZy90eXBlcyc7XG5pbXBvcnQge1xuICBPcHRpbWl6YXRpb25BbmFseXplcixcbiAgT3B0aW1pemF0aW9uQ2F0ZWdvcnksXG4gIFJlY29tbWVuZGF0aW9uLFxufSBmcm9tICcuLi90eXBlcyc7XG5cbi8qKlxuICogSW5zdGFuY2Ugc2l6ZXMgb3JkZXJlZCBmcm9tIHNtYWxsZXN0IHRvIGxhcmdlc3QuXG4gKiBVc2VkIHRvIHN1Z2dlc3Qgb25lIHNpemUgZG93biBmb3IgcmlnaHQtc2l6aW5nLlxuICovXG5jb25zdCBFQzJfU0laRVMgPSBbXG4gICduYW5vJyxcbiAgJ21pY3JvJyxcbiAgJ3NtYWxsJyxcbiAgJ21lZGl1bScsXG4gICdsYXJnZScsXG4gICd4bGFyZ2UnLFxuICAnMnhsYXJnZScsXG4gICc0eGxhcmdlJyxcbiAgJzh4bGFyZ2UnLFxuICAnMTJ4bGFyZ2UnLFxuICAnMTZ4bGFyZ2UnLFxuICAnMjR4bGFyZ2UnLFxuICAnMzJ4bGFyZ2UnLFxuICAnNDh4bGFyZ2UnLFxuICAnbWV0YWwnLFxuXTtcblxuLyoqXG4gKiBJbnN0YW5jZSBzaXplcyBjb25zaWRlcmVkIFwibGFyZ2VcIiBlbm91Z2ggdG8gd2FycmFudCByaWdodC1zaXppbmcgcmV2aWV3LlxuICogV2Ugb25seSBzdWdnZXN0IGRvd25zaXppbmcgZm9yIDJ4bGFyZ2UgYW5kIGFib3ZlLlxuICovXG5jb25zdCBSRVZJRVdfVEhSRVNIT0xEX0lOREVYID0gRUMyX1NJWkVTLmluZGV4T2YoJzJ4bGFyZ2UnKTtcblxuZXhwb3J0IGNsYXNzIFJpZ2h0U2l6aW5nQW5hbHl6ZXIgaW1wbGVtZW50cyBPcHRpbWl6YXRpb25BbmFseXplciB7XG4gIHJlYWRvbmx5IGNhdGVnb3J5OiBPcHRpbWl6YXRpb25DYXRlZ29yeSA9ICdyaWdodC1zaXppbmcnO1xuICByZWFkb25seSBuYW1lID0gJ1JpZ2h0LVNpemluZyc7XG5cbiAgaXNBcHBsaWNhYmxlKHJlc291cmNlczogUmVzb3VyY2VXaXRoSWRbXSk6IGJvb2xlYW4ge1xuICAgIHJldHVybiByZXNvdXJjZXMuc29tZShcbiAgICAgIChyKSA9PlxuICAgICAgICByLnR5cGUgPT09ICdBV1M6OkVDMjo6SW5zdGFuY2UnIHx8XG4gICAgICAgIHIudHlwZSA9PT0gJ0FXUzo6UkRTOjpEQkluc3RhbmNlJyB8fFxuICAgICAgICByLnR5cGUgPT09ICdBV1M6OkVsYXN0aUNhY2hlOjpDYWNoZUNsdXN0ZXInLFxuICAgICk7XG4gIH1cblxuICBhc3luYyBhbmFseXplKFxuICAgIHJlc291cmNlczogUmVzb3VyY2VXaXRoSWRbXSxcbiAgICByZXNvdXJjZUNvc3RzOiBSZXNvdXJjZUNvc3RbXSxcbiAgICBfcmVnaW9uOiBzdHJpbmcsXG4gICk6IFByb21pc2U8UmVjb21tZW5kYXRpb25bXT4ge1xuICAgIGNvbnN0IHJlY29tbWVuZGF0aW9uczogUmVjb21tZW5kYXRpb25bXSA9IFtdO1xuXG4gICAgZm9yIChjb25zdCByZXNvdXJjZSBvZiByZXNvdXJjZXMpIHtcbiAgICAgIGNvbnN0IHJlYyA9IHRoaXMuYW5hbHl6ZVJlc291cmNlKHJlc291cmNlLCByZXNvdXJjZUNvc3RzKTtcbiAgICAgIGlmIChyZWMpIHJlY29tbWVuZGF0aW9ucy5wdXNoKHJlYyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHJlY29tbWVuZGF0aW9ucztcbiAgfVxuXG4gIHByaXZhdGUgYW5hbHl6ZVJlc291cmNlKFxuICAgIHJlc291cmNlOiBSZXNvdXJjZVdpdGhJZCxcbiAgICByZXNvdXJjZUNvc3RzOiBSZXNvdXJjZUNvc3RbXSxcbiAgKTogUmVjb21tZW5kYXRpb24gfCBudWxsIHtcbiAgICBjb25zdCBpbnN0YW5jZVR5cGUgPSB0aGlzLmdldEluc3RhbmNlVHlwZShyZXNvdXJjZSk7XG4gICAgaWYgKCFpbnN0YW5jZVR5cGUpIHJldHVybiBudWxsO1xuXG4gICAgY29uc3Qgc2l6ZSA9IHRoaXMuZXh0cmFjdFNpemUoaW5zdGFuY2VUeXBlKTtcbiAgICBjb25zdCBzaXplSW5kZXggPSBFQzJfU0laRVMuaW5kZXhPZihzaXplKTtcbiAgICBpZiAoc2l6ZUluZGV4IDwgUkVWSUVXX1RIUkVTSE9MRF9JTkRFWCkgcmV0dXJuIG51bGw7XG5cbiAgICBjb25zdCBzbWFsbGVyU2l6ZSA9IEVDMl9TSVpFU1tzaXplSW5kZXggLSAxXTtcbiAgICBjb25zdCBmYW1pbHkgPSBpbnN0YW5jZVR5cGUuc3Vic3RyaW5nKFxuICAgICAgMCxcbiAgICAgIGluc3RhbmNlVHlwZS5sZW5ndGggLSBzaXplLmxlbmd0aCxcbiAgICApO1xuICAgIGNvbnN0IHNtYWxsZXJUeXBlID0gYCR7ZmFtaWx5fSR7c21hbGxlclNpemV9YDtcblxuICAgIGNvbnN0IGNvc3QgPSB0aGlzLmZpbmRDb3N0KHJlc291cmNlLmxvZ2ljYWxJZCwgcmVzb3VyY2VDb3N0cyk7XG4gICAgLy8gRG93bnNpemluZyBieSBvbmUgc3RlcCB0eXBpY2FsbHkgaGFsdmVzIGNvc3QgZm9yIGNvbXB1dGUgaW5zdGFuY2VzXG4gICAgY29uc3Qgc2F2aW5nc1BlcmNlbnQgPSA1MDtcbiAgICBjb25zdCBzYXZpbmdzID0gY29zdCAqIChzYXZpbmdzUGVyY2VudCAvIDEwMCk7XG5cbiAgICByZXR1cm4ge1xuICAgICAgaWQ6IGByaWdodHNpemUtJHtyZXNvdXJjZS5sb2dpY2FsSWR9YCxcbiAgICAgIHRpdGxlOiBgUmV2aWV3IHNpemluZyBvZiAke3Jlc291cmNlLmxvZ2ljYWxJZH0gKCR7aW5zdGFuY2VUeXBlfSlgLFxuICAgICAgZGVzY3JpcHRpb246IGAke3Jlc291cmNlLmxvZ2ljYWxJZH0gdXNlcyAke2luc3RhbmNlVHlwZX0uIENvbnNpZGVyIGRvd25zaXppbmcgdG8gJHtzbWFsbGVyVHlwZX0gaWYgdXRpbGl6YXRpb24gaXMgY29uc2lzdGVudGx5IGJlbG93IDQwJS4gVGhpcyBjb3VsZCBzYXZlIH4kJHtzYXZpbmdzLnRvRml4ZWQoMil9L21vbnRoLmAsXG4gICAgICBjYXRlZ29yeTogdGhpcy5jYXRlZ29yeSxcbiAgICAgIHByaW9yaXR5OiBzYXZpbmdzID49IDEwMCA/ICdoaWdoJyA6IHNhdmluZ3MgPj0gNDAgPyAnbWVkaXVtJyA6ICdsb3cnLFxuICAgICAgZXN0aW1hdGVkTW9udGhseVNhdmluZ3M6IE1hdGgucm91bmQoc2F2aW5ncyAqIDEwMCkgLyAxMDAsXG4gICAgICBlc3RpbWF0ZWRTYXZpbmdzUGVyY2VudDogc2F2aW5nc1BlcmNlbnQsXG4gICAgICBhZmZlY3RlZFJlc291cmNlczogW3Jlc291cmNlLmxvZ2ljYWxJZF0sXG4gICAgICBhY3Rpb25JdGVtczogW1xuICAgICAgICBgQ2hlY2sgQ2xvdWRXYXRjaCBtZXRyaWNzIGZvciBDUFUsIG1lbW9yeSwgYW5kIG5ldHdvcmsgdXRpbGl6YXRpb25gLFxuICAgICAgICBgSWYgYXZnIHV0aWxpemF0aW9uIDwgNDAlLCBkb3duc2l6ZSBmcm9tICR7aW5zdGFuY2VUeXBlfSB0byAke3NtYWxsZXJUeXBlfWAsXG4gICAgICAgICdUZXN0IGluIHN0YWdpbmcgYmVmb3JlIGFwcGx5aW5nIHRvIHByb2R1Y3Rpb24nLFxuICAgICAgICAnTW9uaXRvciBwZXJmb3JtYW5jZSBmb3IgYXQgbGVhc3QgMSB3ZWVrIGFmdGVyIGNoYW5nZScsXG4gICAgICBdLFxuICAgICAgY2F2ZWF0czogW1xuICAgICAgICAnUmVxdWlyZXMgQ2xvdWRXYXRjaCBkYXRhIHRvIHZhbGlkYXRlOyB0aGlzIGlzIGEgdGVtcGxhdGUtYmFzZWQgZXN0aW1hdGUnLFxuICAgICAgICAnTWVtb3J5IHV0aWxpemF0aW9uIHJlcXVpcmVzIENsb3VkV2F0Y2ggQWdlbnQgKG5vdCBhdmFpbGFibGUgYnkgZGVmYXVsdCknLFxuICAgICAgICAnUGVhayB1c2FnZSBwYXR0ZXJucyBtYXkgZGlmZmVyIGZyb20gYXZlcmFnZScsXG4gICAgICAgICdDb25zaWRlciB1c2luZyBBV1MgQ29tcHV0ZSBPcHRpbWl6ZXIgZm9yIGRhdGEtZHJpdmVuIHJlY29tbWVuZGF0aW9ucycsXG4gICAgICBdLFxuICAgIH07XG4gIH1cblxuICBwcml2YXRlIGdldEluc3RhbmNlVHlwZShyZXNvdXJjZTogUmVzb3VyY2VXaXRoSWQpOiBzdHJpbmcgfCBudWxsIHtcbiAgICBjb25zdCBwcm9wcyA9IHJlc291cmNlLnByb3BlcnRpZXM7XG4gICAgc3dpdGNoIChyZXNvdXJjZS50eXBlKSB7XG4gICAgICBjYXNlICdBV1M6OkVDMjo6SW5zdGFuY2UnOlxuICAgICAgICByZXR1cm4gKHByb3BzLkluc3RhbmNlVHlwZSBhcyBzdHJpbmcpIHx8IG51bGw7XG4gICAgICBjYXNlICdBV1M6OlJEUzo6REJJbnN0YW5jZSc6XG4gICAgICAgIHJldHVybiAocHJvcHMuREJJbnN0YW5jZUNsYXNzIGFzIHN0cmluZykgfHwgbnVsbDtcbiAgICAgIGNhc2UgJ0FXUzo6RWxhc3RpQ2FjaGU6OkNhY2hlQ2x1c3Rlcic6XG4gICAgICAgIHJldHVybiAocHJvcHMuQ2FjaGVOb2RlVHlwZSBhcyBzdHJpbmcpIHx8IG51bGw7XG4gICAgICBkZWZhdWx0OlxuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICB9XG4gIH1cblxuICBwcml2YXRlIGV4dHJhY3RTaXplKGluc3RhbmNlVHlwZTogc3RyaW5nKTogc3RyaW5nIHtcbiAgICAvLyBcIm01LjJ4bGFyZ2VcIiAtPiBcIjJ4bGFyZ2VcIiwgXCJkYi5yNS5sYXJnZVwiIC0+IFwibGFyZ2VcIlxuICAgIGNvbnN0IHBhcnRzID0gaW5zdGFuY2VUeXBlLnNwbGl0KCcuJyk7XG4gICAgcmV0dXJuIHBhcnRzW3BhcnRzLmxlbmd0aCAtIDFdIHx8ICcnO1xuICB9XG5cbiAgcHJpdmF0ZSBmaW5kQ29zdChsb2dpY2FsSWQ6IHN0cmluZywgcmVzb3VyY2VDb3N0czogUmVzb3VyY2VDb3N0W10pOiBudW1iZXIge1xuICAgIGNvbnN0IGNvc3QgPSByZXNvdXJjZUNvc3RzLmZpbmQoKGMpID0+IGMubG9naWNhbElkID09PSBsb2dpY2FsSWQpO1xuICAgIHJldHVybiBjb3N0Py5tb250aGx5Q29zdC5hbW91bnQgPz8gMDtcbiAgfVxufVxuIl19
@@ -0,0 +1,11 @@
1
+ import { ResourceWithId } from '../../diff/types';
2
+ import { ResourceCost } from '../../pricing/types';
3
+ import { OptimizationAnalyzer, OptimizationCategory, Recommendation } from '../types';
4
+ export declare class SavingsPlansAnalyzer implements OptimizationAnalyzer {
5
+ readonly category: OptimizationCategory;
6
+ readonly name = "Savings Plans";
7
+ isApplicable(resources: ResourceWithId[]): boolean;
8
+ analyze(resources: ResourceWithId[], resourceCosts: ResourceCost[], _region: string): Promise<Recommendation[]>;
9
+ private hasMultipleComputeTypes;
10
+ private findCost;
11
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SavingsPlansAnalyzer = void 0;
4
+ const COMPUTE_SP_DISCOUNT = 30;
5
+ const EC2_SP_DISCOUNT = 35;
6
+ const COMPUTE_TYPES = [
7
+ 'AWS::EC2::Instance',
8
+ 'AWS::EC2::LaunchTemplate',
9
+ 'AWS::AutoScaling::AutoScalingGroup',
10
+ 'AWS::ECS::Service',
11
+ 'AWS::ECS::TaskDefinition',
12
+ 'AWS::Lambda::Function',
13
+ ];
14
+ const EC2_ONLY_TYPES = [
15
+ 'AWS::EC2::Instance',
16
+ 'AWS::EC2::LaunchTemplate',
17
+ 'AWS::AutoScaling::AutoScalingGroup',
18
+ ];
19
+ const MINIMUM_AGGREGATE_COST = 100;
20
+ class SavingsPlansAnalyzer {
21
+ category = 'savings-plan';
22
+ name = 'Savings Plans';
23
+ isApplicable(resources) {
24
+ return resources.some((r) => COMPUTE_TYPES.includes(r.type));
25
+ }
26
+ async analyze(resources, resourceCosts, _region) {
27
+ const computeResources = resources.filter((r) => COMPUTE_TYPES.includes(r.type));
28
+ if (computeResources.length === 0)
29
+ return [];
30
+ const totalComputeCost = computeResources.reduce((sum, r) => {
31
+ return sum + this.findCost(r.logicalId, resourceCosts);
32
+ }, 0);
33
+ if (totalComputeCost < MINIMUM_AGGREGATE_COST)
34
+ return [];
35
+ const recommendations = [];
36
+ const hasMultipleComputeTypes = this.hasMultipleComputeTypes(computeResources);
37
+ const ec2OnlyResources = computeResources.filter((r) => EC2_ONLY_TYPES.includes(r.type));
38
+ // Compute Savings Plan (works across EC2, Fargate, Lambda)
39
+ const computeSavings = totalComputeCost * (COMPUTE_SP_DISCOUNT / 100);
40
+ recommendations.push({
41
+ id: 'compute-savings-plan',
42
+ title: 'Purchase Compute Savings Plan',
43
+ description: `Total compute spend across ${computeResources.length} resources is $${totalComputeCost.toFixed(2)}/month. A 1-year Compute Savings Plan could save ~${COMPUTE_SP_DISCOUNT}%.${hasMultipleComputeTypes ? ' Compute Savings Plans apply across EC2, Fargate, and Lambda.' : ''}`,
44
+ category: this.category,
45
+ priority: computeSavings >= 200 ? 'high' : computeSavings >= 100 ? 'medium' : 'low',
46
+ estimatedMonthlySavings: Math.round(computeSavings * 100) / 100,
47
+ estimatedSavingsPercent: COMPUTE_SP_DISCOUNT,
48
+ affectedResources: computeResources.map((r) => r.logicalId),
49
+ actionItems: [
50
+ `Commit to $${Math.round(totalComputeCost * (1 - COMPUTE_SP_DISCOUNT / 100))}/month in compute usage`,
51
+ 'Evaluate 1-year vs 3-year term based on workload stability',
52
+ 'Use AWS Cost Explorer Savings Plans recommendations for exact pricing',
53
+ ],
54
+ caveats: [
55
+ '1-year minimum commitment',
56
+ `${COMPUTE_SP_DISCOUNT}% discount is approximate; actual savings vary by instance type and region`,
57
+ 'Applies to EC2, Fargate, and Lambda compute usage',
58
+ 'Cannot be cancelled or modified after purchase',
59
+ ],
60
+ });
61
+ // EC2 Instance Savings Plan (higher discount, less flexibility)
62
+ if (ec2OnlyResources.length > 0) {
63
+ const ec2Cost = ec2OnlyResources.reduce((sum, r) => sum + this.findCost(r.logicalId, resourceCosts), 0);
64
+ if (ec2Cost >= MINIMUM_AGGREGATE_COST) {
65
+ const ec2Savings = ec2Cost * (EC2_SP_DISCOUNT / 100);
66
+ recommendations.push({
67
+ id: 'ec2-savings-plan',
68
+ title: 'Purchase EC2 Instance Savings Plan',
69
+ description: `EC2 spend is $${ec2Cost.toFixed(2)}/month across ${ec2OnlyResources.length} resources. An EC2 Instance Savings Plan offers ~${EC2_SP_DISCOUNT}% discount (higher than Compute SP) but only applies to EC2.`,
70
+ category: this.category,
71
+ priority: ec2Savings >= 200 ? 'high' : 'medium',
72
+ estimatedMonthlySavings: Math.round(ec2Savings * 100) / 100,
73
+ estimatedSavingsPercent: EC2_SP_DISCOUNT,
74
+ affectedResources: ec2OnlyResources.map((r) => r.logicalId),
75
+ actionItems: [
76
+ 'Compare Compute SP vs EC2 Instance SP savings in AWS Cost Explorer',
77
+ 'Choose EC2 Instance SP only if EC2 is the dominant compute spend',
78
+ 'Consider combining with Compute SP for non-EC2 workloads',
79
+ ],
80
+ caveats: [
81
+ 'Only applies to EC2 instance usage (not Fargate or Lambda)',
82
+ 'Locked to a specific instance family and region',
83
+ `${EC2_SP_DISCOUNT}% discount is approximate`,
84
+ ],
85
+ });
86
+ }
87
+ }
88
+ return recommendations;
89
+ }
90
+ hasMultipleComputeTypes(resources) {
91
+ const types = new Set(resources.map((r) => r.type));
92
+ const hasEC2 = [...types].some((t) => EC2_ONLY_TYPES.includes(t));
93
+ const hasNonEC2 = [...types].some((t) => COMPUTE_TYPES.includes(t) && !EC2_ONLY_TYPES.includes(t));
94
+ return hasEC2 && hasNonEC2;
95
+ }
96
+ findCost(logicalId, resourceCosts) {
97
+ const cost = resourceCosts.find((c) => c.logicalId === logicalId);
98
+ return cost?.monthlyCost.amount ?? 0;
99
+ }
100
+ }
101
+ exports.SavingsPlansAnalyzer = SavingsPlansAnalyzer;
102
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU2F2aW5nc1BsYW5zQW5hbHl6ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvb3B0aW1pemF0aW9uL2FuYWx5emVycy9TYXZpbmdzUGxhbnNBbmFseXplci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFRQSxNQUFNLG1CQUFtQixHQUFHLEVBQUUsQ0FBQztBQUMvQixNQUFNLGVBQWUsR0FBRyxFQUFFLENBQUM7QUFFM0IsTUFBTSxhQUFhLEdBQUc7SUFDcEIsb0JBQW9CO0lBQ3BCLDBCQUEwQjtJQUMxQixvQ0FBb0M7SUFDcEMsbUJBQW1CO0lBQ25CLDBCQUEwQjtJQUMxQix1QkFBdUI7Q0FDeEIsQ0FBQztBQUVGLE1BQU0sY0FBYyxHQUFHO0lBQ3JCLG9CQUFvQjtJQUNwQiwwQkFBMEI7SUFDMUIsb0NBQW9DO0NBQ3JDLENBQUM7QUFFRixNQUFNLHNCQUFzQixHQUFHLEdBQUcsQ0FBQztBQUVuQyxNQUFhLG9CQUFvQjtJQUN0QixRQUFRLEdBQXlCLGNBQWMsQ0FBQztJQUNoRCxJQUFJLEdBQUcsZUFBZSxDQUFDO0lBRWhDLFlBQVksQ0FBQyxTQUEyQjtRQUN0QyxPQUFPLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDL0QsQ0FBQztJQUVELEtBQUssQ0FBQyxPQUFPLENBQ1gsU0FBMkIsRUFDM0IsYUFBNkIsRUFDN0IsT0FBZTtRQUVmLE1BQU0sZ0JBQWdCLEdBQUcsU0FBUyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQzlDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUMvQixDQUFDO1FBQ0YsSUFBSSxnQkFBZ0IsQ0FBQyxNQUFNLEtBQUssQ0FBQztZQUFFLE9BQU8sRUFBRSxDQUFDO1FBRTdDLE1BQU0sZ0JBQWdCLEdBQUcs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Q0YsT0FBTyxNQUFNLElBQUksU0FBUyxDQUFDO0lBQzdCLENBQUM7SUFFTyxRQUFRLENBQUMsU0FBaUIsRUFBRSxhQUE2QjtRQUMvRCxNQUFNLElBQUksR0FBRyxhQUFhLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxLQUFLLFNBQVMsQ0FBQyxDQUFDO1FBQ2xFLE9BQU8sSUFBSSxFQUFFLFdBQVcsQ0FBQyxNQUFNLElBQUksQ0FBQyxDQUFDO0lBQ3ZDLENBQUM7Q0FDRjtBQXRHRCxvREFzR0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBSZXNvdXJjZVdpdGhJZCB9IGZyb20gJy4uLy4uL2RpZmYvdHlwZXMnO1xuaW1wb3J0IHsgUmVzb3VyY2VDb3N0IH0gZnJvbSAnLi4vLi4vcHJpY2luZy90eXBlcyc7XG5pbXBvcnQge1xuICBPcHRpbWl6YXRpb25BbmFseXplcixcbiAgT3B0aW1pemF0aW9uQ2F0ZWdvcnksXG4gIFJlY29tbWVuZGF0aW9uLFxufSBmcm9tICcuLi90eXBlcyc7XG5cbmNvbnN0IENPTVBVVEVfU1BfRElTQ09VTlQgPSAzMDtcbmNvbnN0IEVDMl9TUF9ESVNDT1VOVCA9IDM1O1xuXG5jb25zdCBDT01QVVRFX1RZUEVTID0gW1xuICAnQVdTOjpFQzI6Okluc3RhbmNlJyxcbiAgJ0FXUzo6RUMyOjpMYXVuY2hUZW1wbGF0ZScsXG4gICdBV1M6OkF1dG9TY2FsaW5nOjpBdXRvU2NhbGluZ0dyb3VwJyxcbiAgJ0FXUzo6RUNTOjpTZXJ2aWNlJyxcbiAgJ0FXUzo6RUNTOjpUYXNrRGVmaW5pdGlvbicsXG4gICdBV1M6OkxhbWJkYTo6RnVuY3Rpb24nLFxuXTtcblxuY29uc3QgRUMyX09OTFlfVFlQRVMgPSBbXG4gICdBV1M6OkVDMjo6SW5zdGFuY2UnLFxuICAnQVdTOjpFQzI6OkxhdW5jaFRlbXBsYXRlJyxcbiAgJ0FXUzo6QXV0b1NjYWxpbmc6OkF1dG9TY2FsaW5nR3JvdXAnLFxuXTtcblxuY29uc3QgTUlOSU1VTV9BR0dSRUdBVEVfQ09TVCA9IDEwMDtcblxuZXhwb3J0IGNsYXNzIFNhdmluZ3NQbGFuc0FuYWx5emVyIGltcGxlbWVudHMgT3B0aW1pemF0aW9uQW5hbHl6ZXIge1xuICByZWFkb25seSBjYXRlZ29yeTogT3B0aW1pemF0aW9uQ2F0ZWdvcnkgPSAnc2F2aW5ncy1wbGFuJztcbiAgcmVhZG9ubHkgbmFtZSA9ICdTYXZpbmdzIFBsYW5zJztcblxuICBpc0FwcGxpY2FibGUocmVzb3VyY2VzOiBSZXNvdXJjZVdpdGhJZFtdKTogYm9vbGVhbiB7XG4gICAgcmV0dXJuIHJlc291cmNlcy5zb21lKChyKSA9PiBDT01QVVRFX1RZUEVTLmluY2x1ZGVzKHIudHlwZSkpO1xuICB9XG5cbiAgYXN5bmMgYW5hbHl6ZShcbiAgICByZXNvdXJjZXM6IFJlc291cmNlV2l0aElkW10sXG4gICAgcmVzb3VyY2VDb3N0czogUmVzb3VyY2VDb3N0W10sXG4gICAgX3JlZ2lvbjogc3RyaW5nLFxuICApOiBQcm9taXNlPFJlY29tbWVuZGF0aW9uW10+IHtcbiAgICBjb25zdCBjb21wdXRlUmVzb3VyY2VzID0gcmVzb3VyY2VzLmZpbHRlcigocikgPT5cbiAgICAgIENPTVBVVEVfVFlQRVMuaW5jbHVkZXMoci50eXBlKSxcbiAgICApO1xuICAgIGlmIChjb21wdXRlUmVzb3VyY2VzLmxlbmd0aCA9PT0gMCkgcmV0dXJuIFtdO1xuXG4gICAgY29uc3QgdG90YWxDb21wdXRlQ29zdCA9IGNvbXB1dGVSZXNvdXJjZXMucmVkdWNlKChzdW0sIHIpID0+IHtcbiAgICAgIHJldHVybiBzdW0gKyB0aGlzLmZpbmRDb3N0KHIubG9naWNhbElkLCByZXNvdXJjZUNvc3RzKTtcbiAgICB9LCAwKTtcblxuICAgIGlmICh0b3RhbENvbXB1dGVDb3N0IDwgTUlOSU1VTV9BR0dSRUdBVEVfQ09TVCkgcmV0dXJuIFtdO1xuXG4gICAgY29uc3QgcmVjb21tZW5kYXRpb25zOiBSZWNvbW1lbmRhdGlvbltdID0gW107XG4gICAgY29uc3QgaGFzTXVsdGlwbGVDb21wdXRlVHlwZXMgPSB0aGlzLmhhc011bHRpcGxlQ29tcHV0ZVR5cGVzKGNvbXB1dGVSZXNvdXJjZXMpO1xuICAgIGNvbnN0IGVjMk9ubHlSZXNvdXJjZXMgPSBjb21wdXRlUmVzb3VyY2VzLmZpbHRlcigocikgPT5cbiAgICAgIEVDMl9PTkxZX1RZUEVTLmluY2x1ZGVzKHIudHlwZSksXG4gICAgKTtcblxuICAgIC8vIENvbXB1dGUgU2F2aW5ncyBQbGFuICh3b3JrcyBhY3Jvc3MgRUMyLCBGYXJnYXRlLCBMYW1iZGEpXG4gICAgY29uc3QgY29tcHV0ZVNhdmluZ3MgPSB0b3RhbENvbXB1dGVDb3N0ICogKENPTVBVVEVfU1BfRElTQ09VTlQgLyAxMDApO1xuICAgIHJlY29tbWVuZGF0aW9ucy5wdXNoKHtcbiAgICAgIGlkOiAnY29tcHV0ZS1zYXZpbmdzLXBsYW4nLFxuICAgICAgdGl0bGU6ICdQdXJjaGFzZSBDb21wdXRlIFNhdmluZ3MgUGxhbicsXG4gICAgICBkZXNjcmlwdGlvbjogYFRvdGFsIGNvbXB1dGUgc3BlbmQgYWNyb3NzICR7Y29tcHV0ZVJlc291cmNlcy5sZW5ndGh9IHJlc291cmNlcyBpcyAkJHt0b3RhbENvbXB1dGVDb3N0LnRvRml4ZWQoMil9L21vbnRoLiBBIDEteWVhciBDb21wdXRlIFNhdmluZ3MgUGxhbiBjb3VsZCBzYXZlIH4ke0NPTVBVVEVfU1BfRElTQ09VTlR9JS4ke2hhc011bHRpcGxlQ29tcHV0ZVR5cGVzID8gJyBDb21wdXRlIFNhdmluZ3MgUGxhbnMgYXBwbHkgYWNyb3NzIEVDMiwgRmFyZ2F0ZSwgYW5kIExhbWJkYS4nIDogJyd9YCxcbiAgICAgIGNhdGVnb3J5OiB0aGlzLmNhdGVnb3J5LFxuICAgICAgcHJpb3JpdHk6IGNvbXB1dGVTYXZpbmdzID49IDIwMCA/ICdoaWdoJyA6IGNvbXB1dGVTYXZpbmdzID49IDEwMCA/ICdtZWRpdW0nIDogJ2xvdycsXG4gICAgICBlc3RpbWF0ZWRNb250aGx5U2F2aW5nczogTWF0aC5yb3VuZChjb21wdXRlU2F2aW5ncyAqIDEwMCkgLyAxMDAsXG4gICAgICBlc3RpbWF0ZWRTYXZpbmdzUGVyY2VudDogQ09NUFVURV9TUF9ESVNDT1VOVCxcbiAgICAgIGFmZmVjdGVkUmVzb3VyY2VzOiBjb21wdXRlUmVzb3VyY2VzLm1hcCgocikgPT4gci5sb2dpY2FsSWQpLFxuICAgICAgYWN0aW9uSXRlbXM6IFtcbiAgICAgICAgYENvbW1pdCB0byAkJHtNYXRoLnJvdW5kKHRvdGFsQ29tcHV0ZUNvc3QgKiAoMSAtIENPTVBVVEVfU1BfRElTQ09VTlQgLyAxMDApKX0vbW9udGggaW4gY29tcHV0ZSB1c2FnZWAsXG4gICAgICAgICdFdmFsdWF0ZSAxLXllYXIgdnMgMy15ZWFyIHRlcm0gYmFzZWQgb24gd29ya2xvYWQgc3RhYmlsaXR5JyxcbiAgICAgICAgJ1VzZSBBV1MgQ29zdCBFeHBsb3JlciBTYXZpbmdzIFBsYW5zIHJlY29tbWVuZGF0aW9ucyBmb3IgZXhhY3QgcHJpY2luZycsXG4gICAgICBdLFxuICAgICAgY2F2ZWF0czogW1xuICAgICAgICAnMS15ZWFyIG1pbmltdW0gY29tbWl0bWVudCcsXG4gICAgICAgIGAke0NPTVBVVEVfU1BfRElTQ09VTlR9JSBkaXNjb3VudCBpcyBhcHByb3hpbWF0ZTsgYWN0dWFsIHNhdmluZ3MgdmFyeSBieSBpbnN0YW5jZSB0eXBlIGFuZCByZWdpb25gLFxuICAgICAgICAnQXBwbGllcyB0byBFQzIsIEZhcmdhdGUsIGFuZCBMYW1iZGEgY29tcHV0ZSB1c2FnZScsXG4gICAgICAgICdDYW5ub3QgYmUgY2FuY2VsbGVkIG9yIG1vZGlmaWVkIGFmdGVyIHB1cmNoYXNlJyxcbiAgICAgIF0sXG4gICAgfSk7XG5cbiAgICAvLyBFQzIgSW5zdGFuY2UgU2F2aW5ncyBQbGFuIChoaWdoZXIgZGlzY291bnQsIGxlc3MgZmxleGliaWxpdHkpXG4gICAgaWYgKGVjMk9ubHlSZXNvdXJjZXMubGVuZ3RoID4gMCkge1xuICAgICAgY29uc3QgZWMyQ29zdCA9IGVjMk9ubHlSZXNvdXJjZXMucmVkdWNlKFxuICAgICAgICAoc3VtLCByKSA9PiBzdW0gKyB0aGlzLmZpbmRDb3N0KHIubG9naWNhbElkLCByZXNvdXJjZUNvc3RzKSxcbiAgICAgICAgMCxcbiAgICAgICk7XG5cbiAgICAgIGlmIChlYzJDb3N0ID49IE1JTklNVU1fQUdHUkVHQVRFX0NPU1QpIHtcbiAgICAgICAgY29uc3QgZWMyU2F2aW5ncyA9IGVjMkNvc3QgKiAoRUMyX1NQX0RJU0NPVU5UIC8gMTAwKTtcbiAgICAgICAgcmVjb21tZW5kYXRpb25zLnB1c2goe1xuICAgICAgICAgIGlkOiAnZWMyLXNhdmluZ3MtcGxhbicsXG4gICAgICAgICAgdGl0bGU6ICdQdXJjaGFzZSBFQzIgSW5zdGFuY2UgU2F2aW5ncyBQbGFuJyxcbiAgICAgICAgICBkZXNjcmlwdGlvbjogYEVDMiBzcGVuZCBpcyAkJHtlYzJDb3N0LnRvRml4ZWQoMil9L21vbnRoIGFjcm9zcyAke2VjMk9ubHlSZXNvdXJjZXMubGVuZ3RofSByZXNvdXJjZXMuIEFuIEVDMiBJbnN0YW5jZSBTYXZpbmdzIFBsYW4gb2ZmZXJzIH4ke0VDMl9TUF9ESVNDT1VOVH0lIGRpc2NvdW50IChoaWdoZXIgdGhhbiBDb21wdXRlIFNQKSBidXQgb25seSBhcHBsaWVzIHRvIEVDMi5gLFxuICAgICAgICAgIGNhdGVnb3J5OiB0aGlzLmNhdGVnb3J5LFxuICAgICAgICAgIHByaW9yaXR5OiBlYzJTYXZpbmdzID49IDIwMCA/ICdoaWdoJyA6ICdtZWRpdW0nLFxuICAgICAgICAgIGVzdGltYXRlZE1vbnRobHlTYXZpbmdzOiBNYXRoLnJvdW5kKGVjMlNhdmluZ3MgKiAxMDApIC8gMTAwLFxuICAgICAgICAgIGVzdGltYXRlZFNhdmluZ3NQZXJjZW50OiBFQzJfU1BfRElTQ09VTlQsXG4gICAgICAgICAgYWZmZWN0ZWRSZXNvdXJjZXM6IGVjMk9ubHlSZXNvdXJjZXMubWFwKChyKSA9PiByLmxvZ2ljYWxJZCksXG4gICAgICAgICAgYWN0aW9uSXRlbXM6IFtcbiAgICAgICAgICAgICdDb21wYXJlIENvbXB1dGUgU1AgdnMgRUMyIEluc3RhbmNlIFNQIHNhdmluZ3MgaW4gQVdTIENvc3QgRXhwbG9yZXInLFxuICAgICAgICAgICAgJ0Nob29zZSBFQzIgSW5zdGFuY2UgU1Agb25seSBpZiBFQzIgaXMgdGhlIGRvbWluYW50IGNvbXB1dGUgc3BlbmQnLFxuICAgICAgICAgICAgJ0NvbnNpZGVyIGNvbWJpbmluZyB3aXRoIENvbXB1dGUgU1AgZm9yIG5vbi1FQzIgd29ya2xvYWRzJyxcbiAgICAgICAgICBdLFxuICAgICAgICAgIGNhdmVhdHM6IFtcbiAgICAgICAgICAgICdPbmx5IGFwcGxpZXMgdG8gRUMyIGluc3RhbmNlIHVzYWdlIChub3QgRmFyZ2F0ZSBvciBMYW1iZGEpJyxcbiAgICAgICAgICAgICdMb2NrZWQgdG8gYSBzcGVjaWZpYyBpbnN0YW5jZSBmYW1pbHkgYW5kIHJlZ2lvbicsXG4gICAgICAgICAgICBgJHtFQzJfU1BfRElTQ09VTlR9JSBkaXNjb3VudCBpcyBhcHByb3hpbWF0ZWAsXG4gICAgICAgICAgXSxcbiAgICAgICAgfSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIHJlY29tbWVuZGF0aW9ucztcbiAgfVxuXG4gIHByaXZhdGUgaGFzTXVsdGlwbGVDb21wdXRlVHlwZXMocmVzb3VyY2VzOiBSZXNvdXJjZVdpdGhJZFtdKTogYm9vbGVhbiB7XG4gICAgY29uc3QgdHlwZXMgPSBuZXcgU2V0KHJlc291cmNlcy5tYXAoKHIpID0+IHIudHlwZSkpO1xuICAgIGNvbnN0IGhhc0VDMiA9IFsuLi50eXBlc10uc29tZSgodCkgPT4gRUMyX09OTFlfVFlQRVMuaW5jbHVkZXModCkpO1xuICAgIGNvbnN0IGhhc05vbkVDMiA9IFsuLi50eXBlc10uc29tZShcbiAgICAgICh0KSA9PiBDT01QVVRFX1RZUEVTLmluY2x1ZGVzKHQpICYmICFFQzJfT05MWV9UWVBFUy5pbmNsdWRlcyh0KSxcbiAgICApO1xuICAgIHJldHVybiBoYXNFQzIgJiYgaGFzTm9uRUMyO1xuICB9XG5cbiAgcHJpdmF0ZSBmaW5kQ29zdChsb2dpY2FsSWQ6IHN0cmluZywgcmVzb3VyY2VDb3N0czogUmVzb3VyY2VDb3N0W10pOiBudW1iZXIge1xuICAgIGNvbnN0IGNvc3QgPSByZXNvdXJjZUNvc3RzLmZpbmQoKGMpID0+IGMubG9naWNhbElkID09PSBsb2dpY2FsSWQpO1xuICAgIHJldHVybiBjb3N0Py5tb250aGx5Q29zdC5hbW91bnQgPz8gMDtcbiAgfVxufVxuIl19
@@ -0,0 +1,12 @@
1
+ import { ResourceWithId } from '../../diff/types';
2
+ import { ResourceCost } from '../../pricing/types';
3
+ import { OptimizationAnalyzer, OptimizationCategory, Recommendation } from '../types';
4
+ export declare class SpotInstanceAnalyzer implements OptimizationAnalyzer {
5
+ readonly category: OptimizationCategory;
6
+ readonly name = "Spot Instances";
7
+ isApplicable(resources: ResourceWithId[]): boolean;
8
+ analyze(resources: ResourceWithId[], resourceCosts: ResourceCost[], _region: string): Promise<Recommendation[]>;
9
+ private analyzeASG;
10
+ private analyzeECSService;
11
+ private findCost;
12
+ }