UNPKG

@cloud-carbon-footprint/aws

Version:

The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.

195 lines (176 loc) 7.3 kB
/* * © 2021 Thoughtworks, Inc. */ import { GetRightsizingRecommendationRequest, RightsizingRecommendation as AwsRightsizingRecommendation, RightsizingRecommendationList, } from 'aws-sdk/clients/costexplorer' 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: GetRightsizingRecommendationRequest = { Service: this.rightsizingRecommendationsService, Configuration: { BenefitsConsidered: false, RecommendationTarget: recommendationTarget, }, } try { const results = await this.serviceWrapper.getRightsizingRecommendationsResponses(params) const rightsizingRecommendations: RightsizingRecommendationList = 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 } if (recommendation.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: { [key: string]: string } = { Terminate: defaultDetail, Modify: `${defaultDetail} ${modifyDetail}`, } return recommendationTypes[rightsizingCurrentRecommendation.type] } 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, ] } }