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.
- package/.cdk-cost-analyzer-cache/metadata.json +8 -8
- package/dist/action/api/index.d.ts +1 -0
- package/dist/action/api/single-template-types.d.ts +4 -0
- package/dist/action/api/types.d.ts +3 -0
- package/dist/action/config/types.d.ts +2 -0
- package/dist/action/optimization/OptimizationEngine.d.ts +11 -0
- package/dist/action/optimization/analyzers/GravitonMigrationAnalyzer.d.ts +17 -0
- package/dist/action/optimization/analyzers/NATGatewayOptimizationAnalyzer.d.ts +13 -0
- package/dist/action/optimization/analyzers/ReservedInstanceAnalyzer.d.ts +13 -0
- package/dist/action/optimization/analyzers/RightSizingAnalyzer.d.ts +13 -0
- package/dist/action/optimization/analyzers/SavingsPlansAnalyzer.d.ts +11 -0
- package/dist/action/optimization/analyzers/SpotInstanceAnalyzer.d.ts +12 -0
- package/dist/action/optimization/analyzers/StorageOptimizationAnalyzer.d.ts +14 -0
- package/dist/action/optimization/defaults.d.ts +2 -0
- package/dist/action/optimization/index.d.ts +10 -0
- package/dist/action/optimization/types.d.ts +35 -0
- package/dist/action/reporter/RecommendationReporter.d.ts +3 -0
- package/dist/analysis/SingleTemplateAnalyzer.js +12 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +2 -1
- package/dist/api/single-template-types.d.ts +4 -0
- package/dist/api/single-template-types.js +1 -1
- package/dist/api/types.d.ts +3 -0
- package/dist/api/types.js +1 -1
- package/dist/cli/index.js +13 -2
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.js +1 -1
- package/dist/optimization/OptimizationEngine.d.ts +11 -0
- package/dist/optimization/OptimizationEngine.js +51 -0
- package/dist/optimization/analyzers/GravitonMigrationAnalyzer.d.ts +17 -0
- package/dist/optimization/analyzers/GravitonMigrationAnalyzer.js +209 -0
- package/dist/optimization/analyzers/NATGatewayOptimizationAnalyzer.d.ts +13 -0
- package/dist/optimization/analyzers/NATGatewayOptimizationAnalyzer.js +142 -0
- package/dist/optimization/analyzers/ReservedInstanceAnalyzer.d.ts +13 -0
- package/dist/optimization/analyzers/ReservedInstanceAnalyzer.js +102 -0
- package/dist/optimization/analyzers/RightSizingAnalyzer.d.ts +13 -0
- package/dist/optimization/analyzers/RightSizingAnalyzer.js +109 -0
- package/dist/optimization/analyzers/SavingsPlansAnalyzer.d.ts +11 -0
- package/dist/optimization/analyzers/SavingsPlansAnalyzer.js +102 -0
- package/dist/optimization/analyzers/SpotInstanceAnalyzer.d.ts +12 -0
- package/dist/optimization/analyzers/SpotInstanceAnalyzer.js +113 -0
- package/dist/optimization/analyzers/StorageOptimizationAnalyzer.d.ts +14 -0
- package/dist/optimization/analyzers/StorageOptimizationAnalyzer.js +142 -0
- package/dist/optimization/defaults.d.ts +2 -0
- package/dist/optimization/defaults.js +22 -0
- package/dist/optimization/index.d.ts +10 -0
- package/dist/optimization/index.js +22 -0
- package/dist/optimization/types.d.ts +35 -0
- package/dist/optimization/types.js +3 -0
- package/dist/releasetag.txt +1 -1
- package/dist/reporter/RecommendationReporter.d.ts +3 -0
- package/dist/reporter/RecommendationReporter.js +133 -0
- package/dist/reporter/SingleTemplateReporter.js +11 -1
- package/docs/CALCULATORS.md +374 -8
- package/docs/README.md +3 -2
- package/docs/RECOMMENDATIONS.md +466 -0
- package/docs/SINGLE-TEMPLATE-ANALYSIS.md +59 -16
- package/docs/index.md +38 -8
- package/examples/recommendations-demo.json +74 -0
- 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
|
+
}
|