@cloud-carbon-footprint/aws
Version:
The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.
204 lines (183 loc) • 7.71 kB
text/typescript
/*
* © 2021 Thoughtworks, Inc.
*/
import {
GetRightsizingRecommendationCommandInput,
RightsizingRecommendation as AwsRightsizingRecommendation,
RightsizingType,
} from '@aws-sdk/client-cost-explorer'
import {
CloudConstantsEmissionsFactors,
ComputeEstimator,
ICloudRecommendationsService,
MemoryEstimator,
} from '@cloud-carbon-footprint/core'
import {
AWS_RECOMMENDATIONS_TARGETS,
getEmissionsFactors,
Logger,
RecommendationResult,
} from '@cloud-carbon-footprint/common'
import { ServiceWrapper } from '../ServiceWrapper'
import AWSComputeEstimatesBuilder from '../AWSComputeEstimatesBuilder'
import AWSMemoryEstimatesBuilder from '../AWSMemoryEstimatesBuilder'
import {
RightsizingCurrentRecommendation,
RightsizingRecommendation,
RightsizingTargetRecommendation,
} from './Rightsizing'
import { AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH } from '../../domain'
import { AWS_MAPPED_REGIONS_TO_ELECTRICITY_MAPS_ZONES } from '../AWSRegions'
export default class RightsizingRecommendations implements ICloudRecommendationsService {
private readonly rightsizingRecommendationsService: string
private readonly recommendationsLogger: Logger
constructor(
private readonly computeEstimator: ComputeEstimator,
private readonly memoryEstimator: MemoryEstimator,
private readonly serviceWrapper: ServiceWrapper,
) {
this.rightsizingRecommendationsService = 'AmazonEC2'
this.recommendationsLogger = new Logger('AWSRecommendations')
}
async getRecommendations(
recommendationTarget: AWS_RECOMMENDATIONS_TARGETS,
): Promise<RecommendationResult[]> {
const params: GetRightsizingRecommendationCommandInput = {
Service: this.rightsizingRecommendationsService,
Configuration: {
BenefitsConsidered: false,
RecommendationTarget: recommendationTarget,
},
}
try {
const results =
await this.serviceWrapper.getRightsizingRecommendationsResponses(params)
const rightsizingRecommendations: AwsRightsizingRecommendation[] =
results.flatMap(
({ RightsizingRecommendations }) => RightsizingRecommendations,
)
const recommendationsResult: RecommendationResult[] = []
for (let i = 0; i < rightsizingRecommendations.length; i++) {
const recommendation: AwsRightsizingRecommendation =
rightsizingRecommendations[i]
const rightsizingCurrentRecommendation =
new RightsizingCurrentRecommendation(recommendation)
const [currentComputeFootprint, currentMemoryFootprint] =
await this.getFootprintEstimates(rightsizingCurrentRecommendation)
let kilowattHourSavings = currentComputeFootprint.kilowattHours
let co2eSavings = currentComputeFootprint.co2e
let costSavings = rightsizingCurrentRecommendation.costSavings
let recommendationDetail = this.getRecommendationDetail(
rightsizingCurrentRecommendation,
)
if (currentMemoryFootprint.co2e) {
kilowattHourSavings += currentMemoryFootprint.kilowattHours
co2eSavings += currentMemoryFootprint.co2e
}
const normalizedRecommendationType = this.getNormalizedRightsizingType(
recommendation.RightsizingType,
)
if (normalizedRecommendationType === RightsizingType.MODIFY) {
this.getTargetInstance(recommendation)
const rightsizingTargetRecommendation =
new RightsizingTargetRecommendation(recommendation)
const [targetComputeFootprint, targetMemoryFootprint] =
await this.getFootprintEstimates(rightsizingTargetRecommendation)
kilowattHourSavings -= targetComputeFootprint.kilowattHours
co2eSavings -= targetComputeFootprint.co2e
costSavings = rightsizingTargetRecommendation.costSavings
recommendationDetail = this.getRecommendationDetail(
rightsizingCurrentRecommendation,
rightsizingTargetRecommendation,
)
if (targetMemoryFootprint.co2e) {
kilowattHourSavings -= targetMemoryFootprint.kilowattHours
co2eSavings -= targetMemoryFootprint.co2e
}
}
// if there are no potential savings, do not include recommendation object
recommendationsResult.push({
cloudProvider: 'AWS',
accountId: rightsizingCurrentRecommendation.accountId,
accountName: rightsizingCurrentRecommendation.accountId,
region: rightsizingCurrentRecommendation.region,
recommendationType: rightsizingCurrentRecommendation.type,
recommendationDetail,
kilowattHourSavings,
resourceId: rightsizingCurrentRecommendation.resourceId,
instanceName: rightsizingCurrentRecommendation.instanceName,
co2eSavings,
costSavings,
})
}
return recommendationsResult
} catch (e) {
throw new Error(
`Failed to grab AWS Rightsizing recommendations. Reason: ${e.message}`,
)
}
}
private getRecommendationDetail(
rightsizingCurrentRecommendation: RightsizingCurrentRecommendation,
rightsizingTargetRecommendation?: RightsizingTargetRecommendation,
): string {
const modifyDetail = `Update instance type ${rightsizingCurrentRecommendation.instanceType} to ${rightsizingTargetRecommendation?.instanceType}`
let defaultDetail = `${rightsizingCurrentRecommendation.type} instance: ${rightsizingCurrentRecommendation.instanceName}.`
if (!rightsizingCurrentRecommendation.instanceName) {
defaultDetail = `${rightsizingCurrentRecommendation.type} instance with Resource ID: ${rightsizingCurrentRecommendation.resourceId}.`
}
const recommendationTypes: Record<string, string> = {
[RightsizingType.TERMINATE]: defaultDetail,
[RightsizingType.MODIFY]: `${defaultDetail} ${modifyDetail}`,
}
const rightsizingType = this.getNormalizedRightsizingType(
rightsizingCurrentRecommendation.type,
)
return recommendationTypes[rightsizingType] ?? defaultDetail
}
private getNormalizedRightsizingType(rightsizingType?: string): string {
return (rightsizingType ?? '').toUpperCase()
}
private async getFootprintEstimates(
rightsizingRecommendation: RightsizingRecommendation,
) {
const dateTime = new Date().toISOString()
const emissionsFactors: CloudConstantsEmissionsFactors =
await getEmissionsFactors(
rightsizingRecommendation.region,
dateTime,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
AWS_MAPPED_REGIONS_TO_ELECTRICITY_MAPS_ZONES,
this.recommendationsLogger,
)
const computeFootprint = new AWSComputeEstimatesBuilder(
rightsizingRecommendation,
this.computeEstimator,
emissionsFactors,
).computeFootprint
const memoryFootprint = new AWSMemoryEstimatesBuilder(
rightsizingRecommendation,
this.memoryEstimator,
emissionsFactors,
).memoryFootprint
return [computeFootprint, memoryFootprint]
}
private getTargetInstance(
rightsizingRecommendationData: AwsRightsizingRecommendation,
): void {
let targetInstance =
rightsizingRecommendationData.ModifyRecommendationDetail.TargetInstances.pop()
let savings = 0
rightsizingRecommendationData.ModifyRecommendationDetail.TargetInstances.map(
(instance) => {
if (parseFloat(instance.EstimatedMonthlySavings) >= savings) {
savings = parseFloat(instance.EstimatedMonthlySavings)
targetInstance = instance
}
},
)
rightsizingRecommendationData.ModifyRecommendationDetail.TargetInstances = [
targetInstance,
]
}
}