-
Notifications
You must be signed in to change notification settings - Fork 433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Fix Cost Calculation for AWS CloudFront Distributions #1083
Changes from all commits
e2c34bd
a96976f
bc4cc8a
258f4f3
8768cc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,10 @@ package cloudfront | |
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
log "github.com/sirupsen/logrus" | ||
|
@@ -11,11 +14,63 @@ import ( | |
"github.com/aws/aws-sdk-go-v2/service/cloudfront" | ||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch" | ||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" | ||
"github.com/aws/aws-sdk-go-v2/service/pricing" | ||
pricingTypes "github.com/aws/aws-sdk-go-v2/service/pricing/types" | ||
. "github.com/tailwarden/komiser/models" | ||
. "github.com/tailwarden/komiser/providers" | ||
awsUtils "github.com/tailwarden/komiser/providers/aws/utils" | ||
"github.com/tailwarden/komiser/utils" | ||
) | ||
|
||
const ( | ||
freeTierRequests = 10000000 | ||
freeTierFunctionInvocation = 2000000 | ||
freeTierUpload = 1099511627776 | ||
) | ||
|
||
func ConvertBytesToTerabytes(bytes int64) float64 { | ||
return float64(bytes) / 1099511627776 | ||
} | ||
|
||
func getRate(pricingOutput *pricing.GetProductsOutput, groupDescriptionNameOne string, groupDescriptionNameTwo string) (float64, error) { | ||
costPerMonth := 0.0 | ||
|
||
if pricingOutput != nil && len(pricingOutput.PriceList) > 0 { | ||
var priceList interface{} | ||
err := json.Unmarshal([]byte(pricingOutput.PriceList[0]), &priceList) | ||
if err != nil { | ||
return 0, fmt.Errorf("failed to unmarshal JSON: %w", err) | ||
} | ||
|
||
priceListMap := priceList.(map[string]interface{}) | ||
if attribute, ok := priceListMap["product"].(map[string]interface{})["attributes"]; ok { | ||
for _, attb := range attribute.(map[string]interface{}) { | ||
if description, ok := attb.(map[string]string)["groupDescription"]; ok { | ||
if strings.Contains(description, groupDescriptionNameOne) || strings.Contains(description, groupDescriptionNameTwo) { | ||
if onDemand, ok := priceListMap["terms"].(map[string]interface{})["OnDemand"]; ok { | ||
for _, details := range onDemand.(map[string]interface{}) { | ||
if priceDetails, ok := details.(map[string]interface{})["priceDimensions"].(map[string]interface{}); ok { | ||
for _, price := range priceDetails { | ||
usdPrice := price.(map[string]interface{})["pricePerUnit"].(map[string]interface{})["USD"].(string) | ||
costPerMonth, err = strconv.ParseFloat(usdPrice, 64) | ||
if err != nil { | ||
return 0, fmt.Errorf("failed to parse cost per month: %w", err) | ||
} | ||
break | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
} | ||
|
||
return costPerMonth, nil | ||
} | ||
|
||
func Distributions(ctx context.Context, client ProviderClient) ([]Resource, error) { | ||
resources := make([]Resource, 0) | ||
var config cloudfront.ListDistributionsInput | ||
|
@@ -25,6 +80,28 @@ func Distributions(ctx context.Context, client ProviderClient) ([]Resource, erro | |
client.AWSClient.Region = "us-east-1" | ||
cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient) | ||
client.AWSClient.Region = tempRegion | ||
pricingClient := pricing.NewFromConfig(*client.AWSClient) | ||
|
||
pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{ | ||
ServiceCode: aws.String("AmazonCloudFront"), | ||
Filters: []pricingTypes.Filter{ | ||
{ | ||
Field: aws.String("regionCode"), | ||
Value: aws.String(client.AWSClient.Region), | ||
Type: pricingTypes.FilterTypeTermMatch, | ||
}, | ||
}, | ||
}) | ||
if err != nil { | ||
log.Errorf("ERROR: Couldn't fetch pricing info for AWS CloudFront: %v", err) | ||
return resources, err | ||
} | ||
|
||
priceMap, err := awsUtils.GetPriceMap(pricingOutput) | ||
if err != nil { | ||
log.Errorf("ERROR: Failed to calculate cost per month: %v", err) | ||
return resources, err | ||
} | ||
|
||
for { | ||
output, err := cloudfrontClient.ListDistributions(ctx, &config) | ||
|
@@ -39,7 +116,7 @@ func Distributions(ctx context.Context, client ProviderClient) ([]Resource, erro | |
MetricName: aws.String("BytesDownloaded"), | ||
Namespace: aws.String("AWS/CloudFront"), | ||
Dimensions: []types.Dimension{ | ||
types.Dimension{ | ||
{ | ||
Name: aws.String("DistributionId"), | ||
Value: distribution.Id, | ||
}, | ||
|
@@ -59,13 +136,15 @@ func Distributions(ctx context.Context, client ProviderClient) ([]Resource, erro | |
bytesDownloaded = *metricsBytesDownloadedOutput.Datapoints[0].Sum | ||
} | ||
|
||
sizeInTBDownload := ConvertBytesToTerabytes(int64(bytesDownloaded)) | ||
|
||
metricsBytesUploadedOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{ | ||
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())), | ||
EndTime: aws.Time(time.Now()), | ||
MetricName: aws.String("BytesUploaded"), | ||
Namespace: aws.String("AWS/CloudFront"), | ||
Dimensions: []types.Dimension{ | ||
types.Dimension{ | ||
{ | ||
Name: aws.String("DistributionId"), | ||
Value: distribution.Id, | ||
}, | ||
|
@@ -84,14 +163,19 @@ func Distributions(ctx context.Context, client ProviderClient) ([]Resource, erro | |
if metricsBytesUploadedOutput != nil && len(metricsBytesUploadedOutput.Datapoints) > 0 { | ||
bytesUploaded = *metricsBytesUploadedOutput.Datapoints[0].Sum | ||
} | ||
if bytesUploaded > freeTierUpload { | ||
bytesUploaded -= freeTierUpload | ||
} | ||
|
||
sizeInTBUpload := ConvertBytesToTerabytes(int64(bytesUploaded)) | ||
|
||
metricsRequestsOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{ | ||
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())), | ||
EndTime: aws.Time(time.Now()), | ||
MetricName: aws.String("Requests"), | ||
Namespace: aws.String("AWS/CloudFront"), | ||
Dimensions: []types.Dimension{ | ||
types.Dimension{ | ||
{ | ||
Name: aws.String("DistributionId"), | ||
Value: distribution.Id, | ||
}, | ||
|
@@ -110,17 +194,105 @@ func Distributions(ctx context.Context, client ProviderClient) ([]Resource, erro | |
if metricsRequestsOutput != nil && len(metricsRequestsOutput.Datapoints) > 0 { | ||
requests = *metricsRequestsOutput.Datapoints[0].Sum | ||
} | ||
if requests > freeTierRequests { | ||
requests -= freeTierRequests | ||
} | ||
|
||
metricsLambdaEdgeDurationOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{ | ||
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())), | ||
EndTime: aws.Time(time.Now()), | ||
MetricName: aws.String("Duration"), | ||
Namespace: aws.String("AWS/LambdaEdge"), | ||
Dimensions: []types.Dimension{ | ||
{ | ||
Name: aws.String("DistributionId"), | ||
Value: distribution.Id, | ||
}, | ||
}, | ||
Period: aws.Int32(86400), | ||
Statistics: []types.Statistic{ | ||
types.StatisticAverage, | ||
}, | ||
}) | ||
|
||
if err != nil { | ||
log.Warnf("Couldn't fetch Lambda@Edge Duration metric for %s", *distribution.Id) | ||
} | ||
|
||
lambdaEdgeDuration := 0.0 | ||
if metricsLambdaEdgeDurationOutput != nil && len(metricsLambdaEdgeDurationOutput.Datapoints) > 0 { | ||
lambdaEdgeDuration = *metricsLambdaEdgeDurationOutput.Datapoints[0].Average | ||
} | ||
|
||
metricsLambdaEdgeRequestsOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{ | ||
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())), | ||
EndTime: aws.Time(time.Now()), | ||
MetricName: aws.String("Requests"), | ||
Namespace: aws.String("AWS/LambdaEdge"), | ||
Dimensions: []types.Dimension{ | ||
{ | ||
Name: aws.String("DistributionId"), | ||
Value: distribution.Id, | ||
}, | ||
}, | ||
Period: aws.Int32(86400), | ||
Statistics: []types.Statistic{ | ||
types.StatisticSum, | ||
}, | ||
}) | ||
|
||
if err != nil { | ||
log.Warnf("Couldn't fetch Lambda@Edge Requests metric for %s", *distribution.Id) | ||
} | ||
|
||
lambdaEdgeRequests := 0.0 | ||
if metricsLambdaEdgeRequestsOutput != nil && len(metricsLambdaEdgeRequestsOutput.Datapoints) > 0 { | ||
lambdaEdgeRequests = *metricsLambdaEdgeRequestsOutput.Datapoints[0].Sum | ||
} | ||
|
||
metricsFunctionInvocationsOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{ | ||
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())), | ||
EndTime: aws.Time(time.Now()), | ||
MetricName: aws.String("FunctionInvocations"), | ||
Namespace: aws.String("AWS/CloudFront"), | ||
Dimensions: []types.Dimension{ | ||
{ | ||
Name: aws.String("DistributionId"), | ||
Value: distribution.Id, | ||
}, | ||
}, | ||
Period: aws.Int32(3600), | ||
Statistics: []types.Statistic{ | ||
types.StatisticSum, | ||
}, | ||
}) | ||
if err != nil { | ||
log.Warnf("Couldn't fetch Function Invocations metric for %s", *distribution.Id) | ||
} | ||
|
||
functionInvocation := 0.0 | ||
if metricsFunctionInvocationsOutput != nil && len(metricsFunctionInvocationsOutput.Datapoints) > 0 { | ||
functionInvocation = *metricsFunctionInvocationsOutput.Datapoints[0].Sum | ||
} | ||
if functionInvocation > freeTierFunctionInvocation { | ||
functionInvocation -= freeTierFunctionInvocation | ||
} | ||
|
||
dataTransferToInternetCost := awsUtils.GetCost(priceMap["CloudFront-DataTransfer-In-Bytes"], sizeInTBUpload*1024) | ||
|
||
dataTransferToOriginCost := awsUtils.GetCost(priceMap["CloudFront-DataTransfer-Out-Bytes"], sizeInTBDownload*1024) | ||
|
||
requestsCost := awsUtils.GetCost(priceMap["CloudFront-Requests"], requests/10000) | ||
Comment on lines
+281
to
+285
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where did you get the keys for priceMap? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we keep it static like previous??? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me see if I can find the correct keys |
||
|
||
lambdaEdgeDurationCost := awsUtils.GetCost(priceMap["AWS-Lambda-Edge-Duration"], lambdaEdgeDuration) | ||
|
||
// calculate region data transfer out to internet | ||
dataTransferToInternet := (bytesUploaded / 1000000000) * 0.085 | ||
lambdaEdgeRequestsCost := awsUtils.GetCost(priceMap["AWS-Lambda-Edge-Requests"], lambdaEdgeRequests/10000000) | ||
|
||
// calculate region data transfer out to origin | ||
dataTransferToOrigin := (bytesDownloaded / 1000000000) * 0.02 | ||
functionInvocationsRate, _ := getRate(pricingOutput, "CloudFront Function Invocation", "function invocation") | ||
|
||
// calculate requests cost | ||
requestsCost := requests * 0.000001 | ||
functionInvocationsCost := functionInvocationsRate * functionInvocation | ||
|
||
monthlyCost := dataTransferToInternet + dataTransferToOrigin + requestsCost | ||
monthlyCost := dataTransferToInternetCost + dataTransferToOriginCost + requestsCost + lambdaEdgeDurationCost + lambdaEdgeRequestsCost + functionInvocationsCost | ||
|
||
outputTags, err := cloudfrontClient.ListTagsForResource(ctx, &cloudfront.ListTagsForResourceInput{ | ||
Resource: distribution.ARN, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we convert to GB instead? We do the same by multiplying by 1024 to the result
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I thought it would be more efficient not to multiply inline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok i got it