UNPKG

@cloud-carbon-footprint/aws

Version:

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

443 lines (402 loc) 11.2 kB
/* * © 2021 Thoughtworks, Inc. */ import AWS, { CostExplorer, CloudWatchLogs, CloudWatch, S3 } from 'aws-sdk' import AWSMock from 'aws-sdk-mock' import { Logger } from '@cloud-carbon-footprint/common' import { StorageEstimator } from '@cloud-carbon-footprint/core' import RDSStorage from '../lib/RDSStorage' import { buildCostExplorerGetCostResponse, buildCostExplorerGetUsageResponse, } from './fixtures/builders' import { ServiceWrapper } from '../lib/ServiceWrapper' import { AWS_CLOUD_CONSTANTS, AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH, } from '../domain' beforeAll(() => { AWSMock.setSDKInstance(AWS) }) describe('RDSStorage', () => { afterEach(() => { AWSMock.restore() }) const startDate = '2020-07-24' const dayTwo = '2020-07-25' const endDate = '2020-07-26' const region = 'us-east-1' const emissionsFactors = AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH const constants = { powerUsageEffectiveness: AWS_CLOUD_CONSTANTS.getPUE(), } const getServiceWrapper = () => new ServiceWrapper( new CloudWatch(), new CloudWatchLogs(), new CostExplorer(), new S3(), ) it('calculates terabyteHours usage', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetUsageResponse([ { start: startDate, amount: 1, keys: ['USW1-RDS:GP2-Storage'] }, { start: dayTwo, amount: 2, keys: ['USW1-RDS:GP2-Storage'] }, ]), ) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) const result = await rdsStorage.getUsage( new Date(startDate), new Date(endDate), region, ) expect(result).toEqual([ { diskType: 'SSD', terabyteHours: 0.744, timestamp: new Date(startDate), }, { diskType: 'SSD', terabyteHours: 1.488, timestamp: new Date(dayTwo), }, ]) }) it('should call cost explorer with the expected request', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { expect(params).toEqual({ TimePeriod: { Start: startDate, End: endDate, }, Filter: { And: [ { Dimensions: { Key: 'REGION', Values: [region] } }, { Dimensions: { Key: 'USAGE_TYPE_GROUP', Values: ['RDS: Storage'], }, }, ], }, Granularity: 'DAILY', Metrics: ['UsageQuantity'], GroupBy: [ { Key: 'USAGE_TYPE', Type: 'DIMENSION', }, ], }) callback(null, buildCostExplorerGetUsageResponse([])) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) await rdsStorage.getUsage(new Date(startDate), new Date(endDate), region) }) it('calculates terabyteHours for shorter months', async () => { const juneStartDate = '2020-06-24' const juneEndDate = '2020-06-26' AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetUsageResponse([ { start: juneStartDate, amount: 1.0, keys: ['USW1-RDS:GP2-Storage'], }, ]), ) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) const result = await rdsStorage.getUsage( new Date(juneStartDate), new Date(juneEndDate), region, ) expect(result).toEqual([ { diskType: 'SSD', terabyteHours: 0.72, timestamp: new Date(juneStartDate), }, ]) }) it('filters 0 terabyteHours of usage', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetUsageResponse([ { start: startDate, amount: 0, keys: ['USW1-RDS:GP2-Storage'] }, ]), ) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) const result = await rdsStorage.getUsage( new Date(startDate), new Date(endDate), region, ) expect(result).toEqual([]) }) it('should query for the specified region', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { expect(params.Filter.And).toContainEqual({ Dimensions: { Key: 'REGION', Values: [region], }, }) callback( null, buildCostExplorerGetUsageResponse([ { start: startDate, amount: 0, keys: ['USW1-RDS:GP2-Storage'] }, ]), ) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) await rdsStorage.getUsage(new Date(startDate), new Date(endDate), region) }) it('should return empty array if no usage', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback(null, { ResultsByTime: [ { TimePeriod: { Start: startDate, }, Groups: [], }, ], }) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) const result = await rdsStorage.getUsage( new Date(startDate), new Date(endDate), region, ) expect(result).toEqual([]) }) it('should get estimates for RDS IOPS SSD storage', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetUsageResponse([ { start: startDate, amount: 1, keys: ['USW1-RDS:PIOPS-Storage'] }, ]), ) }, ) const rdsService = new RDSStorage(getServiceWrapper()) const ssdStorageEstimator = new StorageEstimator( AWS_CLOUD_CONSTANTS.SSDCOEFFICIENT, ) const result = await rdsService.getEstimates( new Date(startDate), new Date(endDate), region, emissionsFactors, constants, ) expect(result).toEqual( ssdStorageEstimator.estimate( [{ terabyteHours: 0.744, timestamp: new Date(startDate) }], region, emissionsFactors, constants, ), ) }) it('should get estimates for RDS standard HDD storage', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetUsageResponse([ { start: startDate, amount: 1, keys: ['USW1-RDS:StorageUsage'] }, ]), ) }, ) const rdsService = new RDSStorage(getServiceWrapper()) const hddStorageEstimator = new StorageEstimator( AWS_CLOUD_CONSTANTS.HDDCOEFFICIENT, ) const result = await rdsService.getEstimates( new Date(startDate), new Date(endDate), region, emissionsFactors, constants, ) expect(result).toEqual( hddStorageEstimator.estimate( [{ terabyteHours: 0.744, timestamp: new Date(startDate) }], region, emissionsFactors, constants, ), ) }) it('should get estimates for RDS ChargedBackup HDD storage', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetUsageResponse([ { start: startDate, amount: 1, keys: ['USE1-RDS:ChargedBackupUsage'], }, ]), ) }, ) const rdsService = new RDSStorage(getServiceWrapper()) const hddStorageEstimator = new StorageEstimator( AWS_CLOUD_CONSTANTS.HDDCOEFFICIENT, ) const result = await rdsService.getEstimates( new Date(startDate), new Date(endDate), region, emissionsFactors, constants, ) expect(result).toEqual( hddStorageEstimator.estimate( [{ terabyteHours: 0.744, timestamp: new Date(startDate) }], region, emissionsFactors, constants, ), ) }) it('should get costs for RDS', async () => { AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetCostResponse([ { start: startDate, amount: 0.2, keys: ['USW1-RDS:GP2-Storage'] }, { start: dayTwo, amount: 1.8, keys: ['USW1-RDS:GP2-Storage'] }, ]), ) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) const result = await rdsStorage.getCosts( new Date(startDate), new Date(endDate), region, ) expect(result).toEqual([ { amount: 0.2, currency: 'USD', timestamp: new Date(startDate), }, { amount: 1.8, currency: 'USD', timestamp: new Date(dayTwo), }, ]) }) it('Check if warning is called based on valid Disk Type', async () => { const loggerwarnSpy = jest .spyOn(Logger.prototype, 'warn') .mockImplementation() AWSMock.mock( 'CostExplorer', 'getCostAndUsage', ( params: CostExplorer.GetCostAndUsageRequest, callback: (a: Error, response: any) => any, ) => { callback( null, buildCostExplorerGetUsageResponse([ { start: startDate, amount: 1, keys: ['ThrowError'] }, ]), ) }, ) const rdsStorage = new RDSStorage(getServiceWrapper()) //const rdsStorage = new RDSStorage(new ServiceWrapper(new CloudWatch(), new CostExplorer())) await rdsStorage.getUsage(new Date(startDate), new Date(endDate), region) expect(loggerwarnSpy).toHaveBeenCalledWith( 'Unexpected Cost explorer Dimension Name: ThrowError', ) }) })