UNPKG

@cloud-carbon-footprint/aws

Version:

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

266 lines (238 loc) 9.76 kB
/* * © 2021 Thoughtworks, Inc. */ import moment from 'moment' import { CloudConstantsEmissionsFactors, ComputeEstimator, ICloudRecommendationsService, MemoryEstimator, StorageEstimator, } from '@cloud-carbon-footprint/core' import { getEmissionsFactors, Logger, RecommendationResult, } from '@cloud-carbon-footprint/common' import { ServiceWrapper } from '../ServiceWrapper' import { EBSCurrentComputeOptimizerRecommendation, EBSTargetComputeOptimizerRecommendation, EC2CurrentComputeOptimizerRecommendation, EC2TargetComputeOptimizerRecommendation, LambdaCurrentComputeOptimizerRecommendation, LambdaTargetComputeOptimizerRecommendation, } from './ComputeOptimizer' import AWSComputeEstimatesBuilder from '../AWSComputeEstimatesBuilder' import AWSStorageEstimatesBuilder from '../AWSStorageEstimatesBuilder' import AWSMemoryEstimatesBuilder from '../AWSMemoryEstimatesBuilder' import { SSD_USAGE_TYPES } from '../CostAndUsageTypes' import ComputeOptimizerRecommendation from './ComputeOptimizer/ComputeOptimizerRecommendation' import { AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH } from '../../domain' import { AWS_MAPPED_REGIONS_TO_ELECTRICITY_MAPS_ZONES } from '../AWSRegions' export default class ComputeOptimizerRecommendations implements ICloudRecommendationsService { private readonly recommendationsLogger: Logger constructor( private readonly computeEstimator: ComputeEstimator, private readonly memoryEstimator: MemoryEstimator, private readonly ssdStorageEstimator: StorageEstimator, private readonly hddStorageEstimator: StorageEstimator, private readonly serviceWrapper: ServiceWrapper, ) { this.recommendationsLogger = new Logger('AWSRecommendations') } async getRecommendations(centralComputeOptimizerBucket: string) { const params = { Bucket: centralComputeOptimizerBucket, } try { const bucketObjectsList = await this.serviceWrapper.listBucketObjects( params, ) const recommendationsResult: RecommendationResult[] = [] const includedRecommendationTypes = ['OVER_PROVISIONED', 'NOTOPTIMIZED'] for (const recommendationsData of bucketObjectsList.Contents) { const recommendationDate = new Date( recommendationsData.Key.match( /[0-9]{4}-[0-9]{2}-[0-9]{2}/g, )[0].toString(), ) const isFromPreviousDay = moment.utc(recommendationDate) >= moment.utc().subtract(1, 'days') const isIncludedService = !recommendationsData.Key.includes('asg') const service: string = recommendationsData.Key.split('/')[1] if (isFromPreviousDay && isIncludedService) { const params = { Bucket: bucketObjectsList.Name, Key: recommendationsData.Key, } const parsedRecommendations = await this.serviceWrapper.getComputeOptimizerRecommendationsResponse( params, ) for (let i = 0; i < parsedRecommendations.length; i++) { const recommendation = parsedRecommendations[i] const recommendationType = recommendation.finding if ( includedRecommendationTypes.includes( recommendationType.toUpperCase(), ) ) { const computeOptimizerRecommendationServices: { [service: string]: any } = { ec2: [ EC2CurrentComputeOptimizerRecommendation, EC2TargetComputeOptimizerRecommendation, ], ebs: [ EBSCurrentComputeOptimizerRecommendation, EBSTargetComputeOptimizerRecommendation, ], lambda: [ LambdaCurrentComputeOptimizerRecommendation, LambdaTargetComputeOptimizerRecommendation, ], } const currentComputeOptimizerRecommendation = new computeOptimizerRecommendationServices[service][0]( recommendation, ) const targetComputeOptimizerRecommendation = new computeOptimizerRecommendationServices[service][1]( recommendation, ) if (targetComputeOptimizerRecommendation.costSavings) { let kilowattHourSavings = 0 let co2eSavings = 0 if (service === 'ebs') { const currentStorageFootprint = this.getStorageFootprintEstimate( currentComputeOptimizerRecommendation, ) const targetStorageFootprint = this.getStorageFootprintEstimate( targetComputeOptimizerRecommendation, ) kilowattHourSavings = currentStorageFootprint.kilowattHours - targetStorageFootprint.kilowattHours co2eSavings = currentStorageFootprint.co2e - targetStorageFootprint.co2e } else { const [currentComputeFootprint, currentMemoryFootprint] = await this.getComputeAndMemoryFootprintEstimates( currentComputeOptimizerRecommendation, this.recommendationsLogger, ) const [targetComputeFootprint, targetMemoryFootprint] = await this.getComputeAndMemoryFootprintEstimates( targetComputeOptimizerRecommendation, this.recommendationsLogger, ) kilowattHourSavings = currentComputeFootprint.kilowattHours - targetComputeFootprint.kilowattHours co2eSavings = currentComputeFootprint.co2e - targetComputeFootprint.co2e if ( currentMemoryFootprint.co2e || targetMemoryFootprint.co2e ) { kilowattHourSavings += currentMemoryFootprint.kilowattHours - targetMemoryFootprint.kilowattHours co2eSavings += currentMemoryFootprint.co2e - targetMemoryFootprint.co2e } } recommendationsResult.push({ cloudProvider: 'AWS', accountId: currentComputeOptimizerRecommendation.accountId, accountName: currentComputeOptimizerRecommendation.accountId, instanceName: currentComputeOptimizerRecommendation?.instanceName, region: currentComputeOptimizerRecommendation.region, recommendationType: currentComputeOptimizerRecommendation.type, resourceId: currentComputeOptimizerRecommendation.resourceId, recommendationDetail: this.getRecommendationDetail( service, currentComputeOptimizerRecommendation, targetComputeOptimizerRecommendation, ), costSavings: parseFloat( targetComputeOptimizerRecommendation.costSavings, ) || 0, co2eSavings, kilowattHourSavings, }) } } } } } return recommendationsResult } catch (e) { throw new Error( `Failed to grab AWS Compute Optimizer recommendations. Reason: ${e.message}`, ) } } private getRecommendationDetail( service: string, currentRecommendation: ComputeOptimizerRecommendation, targetRecommendation: ComputeOptimizerRecommendation, ): string { type ServiceDetail = { [service: string]: string } const modifyType: ServiceDetail = { ec2: 'instance type', ebs: 'volume type', lambda: 'configuration memory size', } return `Save cost by changing ${modifyType[service]} from ${currentRecommendation.description} to ${targetRecommendation.description}.` } private async getComputeAndMemoryFootprintEstimates( computeOptimizerRecommendation: EC2CurrentComputeOptimizerRecommendation, logger: Logger, ) { const dateTime = new Date().toISOString() const emissionsFactors: CloudConstantsEmissionsFactors = await getEmissionsFactors( computeOptimizerRecommendation.region, dateTime, AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH, AWS_MAPPED_REGIONS_TO_ELECTRICITY_MAPS_ZONES, logger, ) const computeFootprint = new AWSComputeEstimatesBuilder( computeOptimizerRecommendation, this.computeEstimator, emissionsFactors, ).computeFootprint const memoryFootprint = new AWSMemoryEstimatesBuilder( computeOptimizerRecommendation, this.memoryEstimator, emissionsFactors, ).memoryFootprint return [computeFootprint, memoryFootprint] } private getStorageFootprintEstimate( computeOptimizerRecommendation: EBSCurrentComputeOptimizerRecommendation, ) { let storageEstimator = this.hddStorageEstimator if (this.volumeTypeIsSSD(computeOptimizerRecommendation.volumeType)) { storageEstimator = this.ssdStorageEstimator } return new AWSStorageEstimatesBuilder( computeOptimizerRecommendation, storageEstimator, ).storageFootprint } private volumeTypeIsSSD(volumeType: string) { return SSD_USAGE_TYPES.some((type) => type.includes(volumeType)) } }