Skip to content
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 & Functions #1131

Conversation

bishal7679
Copy link
Contributor

@bishal7679 bishal7679 commented Oct 23, 2023

Problem #877

Currently, cost for AWS CloudFront is full static and showing wrong.

Solution

This PR is fixing that issue by adding more metrics for lambda-edge-duration, lambda-edge-requests, origin-shield-request with pricemap fetching.

Changes Made

  • Fetching price info using AWS SDK price module using AmazonCloudFront service code and appropriate filters.
  • getting price map using util function GetPriceMap from utils.go
  • fetching distribution objects & function objects using CloudFront client
  • also fetching lambda-edge-duration, lambda-edge-requests, origin-shield-request metrics using CloudWatch
  • Finally added dynamic calculation as per all the metrics

Checklist

  • Code follows the contributing guidelines
  • Changes have been thoroughly tested
  • Documentation has been updated, if necessary
  • Any dependencies have been added to the project, if necessary

Pricingoutput :-
pricing.json

REF :- #1083

Signed-off-by: bishal7679 <bishalhnj127@gmail.com>
@bishal7679
Copy link
Contributor Author

@AvineshTripathi @Azanul
PHAL!
Now all checks passed 👍


// calculate region data transfer out to origin
dataTransferToOrigin := (bytesDownloaded / 1000000000) * 0.02
dataTransferToOriginCost := awsUtils.GetCost(priceMap["AWS-CloudFront-DataTransfer-Out-Bytes"], (float64(bytesDownloaded) / 1099511627776)*1024)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought it would be better to declare a var for static values for better readability for others who might take ref of your work

Copy link
Collaborator

@Azanul Azanul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The priceMap keys are incorrect, please fix those. Log the pricing output to find the correct keys.

Cloudfront distribution cost is Data transfer out cost + request cost
Check out the pricing examples in https://aws.amazon.com/cloudfront/pricing/

providers/aws/cloudfront/functions.go Outdated Show resolved Hide resolved
@bishal7679
Copy link
Contributor Author

bishal7679 commented Oct 24, 2023

The priceMap keys are incorrect, please fix those. Log the pricing output to find the correct keys.

Cloudfront distribution cost is Data transfer out cost + request cost Check out the pricing examples in https://aws.amazon.com/cloudfront/pricing/

But I didn't change anything on this. It was included cost = upload + download + request
Also help me to get the correct keys. I am unable to find bcoz pricing output doesn't fetch those


lambdaEdgeDurationCost := awsUtils.GetCost(priceMap["AWS-Lambda-Edge-Duration"], lambdaEdgeDuration)

lambdaEdgeRequestsCost := awsUtils.GetCost(priceMap["AWS-Lambda-Edge-Requests"], lambdaEdgeRequests/10000000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should declare a constant for 10000000 this should be descriptive so other developers would have an idea of what this calculation achieves.

@Azanul
Copy link
Collaborator

Azanul commented Oct 24, 2023

But I didn't change anything on this. It was included cost = upload + download + request Also help me to get the correct keys. I am unable to find bcoz pricing output doesn't fetch those

Try without the region code filter since for cloudfront from which region the data is fetched is also considered. Here's the full pricing output w/o region code filter.
op.json

Signed-off-by: bishal7679 <bishalhnj127@gmail.com>
@bishal7679 bishal7679 force-pushed the feature/correct-cost-for-cloudfront branch from 4b1ba27 to ad9362d Compare October 24, 2023 14:05
@bishal7679
Copy link
Contributor Author

@Azanul @AvineshTripathi PTAL now!

Copy link
Collaborator

@Azanul Azanul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloufroint functions looks good to go from me.

Calculation for distributions requires much more calculation as the implementation here would simply take the last value that parser gets in the list of prices. In short, the location needs to be taken into consideration.

@bishal7679
Copy link
Contributor Author

Cloufroint functions looks good to go from me.

Calculation for distributions requires much more calculation as the implementation here would simply take the last value that parser gets in the list of prices. In short, the location needs to be taken into consideration.

Could you pls elaborate more
Location what do you mean and do I have to write another metric??

@Azanul
Copy link
Collaborator

Azanul commented Oct 24, 2023

Calculation for distributions requires much more calculation as the implementation here would simply take the last value that parser gets in the list of prices. In short, the location needs to be taken into consideration.

Could you pls elaborate more Location what do you mean and do I have to write another metric??

If you see the pricing output or the cloudfront pricing page, you'll notice that price is location wise. The location is (probably) from location, the location/region of the origin from the data is fetched. To get this I think we'll probably have to map the aws regions to the listed cloudfront regions and calculate accordingly. Someone with more AWS expertise might be able to confirm.
cc: @mlabouardy @jakepage91

StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
EndTime: aws.Time(time.Now()),
MetricName: aws.String("Duration"),
Namespace: aws.String("AWS/LambdaEdge"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that not aws.String("AWS/Cloudfront")?

Copy link
Contributor Author

@bishal7679 bishal7679 Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Azanul WDYT??
AWS/Cloudfront or AWS/Lambda would be correct??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capital F

StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
EndTime: aws.Time(time.Now()),
MetricName: aws.String("Requests"),
Namespace: aws.String("AWS/LambdaEdge"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it AWS/Cloudfront?

metricsLambdaEdgeDurationOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
EndTime: aws.Time(time.Now()),
MetricName: aws.String("Duration"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that work? If yes, that would be great 👍

resources = append(resources, Resource{
Provider: "AWS",
Account: client.Name,
Service: "CloudFront",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be Cloudfront Functions?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloudfront functions isn't a service, it's a resource/sub-resource of Cloudfront service

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right.

providers/aws/cloudfront/functions.go Outdated Show resolved Hide resolved
@Azanul
Copy link
Collaborator

Azanul commented Oct 26, 2023

@bishal7679 I recently uncovered an oversight here. Cloudfront functions and Lambda edge functions are different, the current PR mixes them both. I'm looking for the Lambda edge metrics API, when found, we could use those metrics instead of cloudfront function invocations, change the file name and it'll work out.
Cloudfront functions won't need a pricing client either since the price is static globally.

@bishal7679
Copy link
Contributor Author

@bishal7679 I recently uncovered an oversight here. Cloudfront functions and Lambda edge functions are different, the current PR mixes them both. I'm looking for the Lambda edge metrics API, when found, we could use those metrics instead of cloudfront function invocations, change the file name and it'll work out. Cloudfront functions won't need a pricing client either since the price is static globally.

Found Lambda@Edge is similar to cloudfront means the functions running on cloudfront edge regionally. Thats might be the reason pricing output is giving Lambda edge results for CloudFront

@Azanul
Copy link
Collaborator

Azanul commented Oct 26, 2023

Found Lambda@Edge is similar to cloudfront means the functions running on cloudfront edge regionally. Thats might be the reason pricing output is giving Lambda edge results for CloudFront

Lambda edge is a feature of cloudfront only but is different from cloudfront functions

@mlabouardy
Copy link
Collaborator

Found Lambda@Edge is similar to cloudfront means the functions running on cloudfront edge regionally. Thats might be the reason pricing output is giving Lambda edge results for CloudFront

Lambda edge is a feature of cloudfront only but is different from cloudfront functions

I agree, Lambda@Edge is just a Lambda function that's triggered on a CloudFront edge

@mlabouardy mlabouardy added this to the v3.1.3 milestone Oct 27, 2023
@jolo-dev
Copy link
Contributor

Found Lambda@Edge is similar to cloudfront means the functions running on cloudfront edge regionally. Thats might be the reason pricing output is giving Lambda edge results for CloudFront

Lambda edge is a feature of cloudfront only but is different from cloudfront functions

I agree, Lambda@Edge is just a Lambda function that's triggered on a CloudFront edge

So meaning my PR #1127 is not completely useless? :D

@bishal7679
Copy link
Contributor Author

Found Lambda@Edge is similar to cloudfront means the functions running on cloudfront edge regionally. Thats might be the reason pricing output is giving Lambda edge results for CloudFront

Lambda edge is a feature of cloudfront only but is different from cloudfront functions

I agree, Lambda@Edge is just a Lambda function that's triggered on a CloudFront edge

So meaning my PR #1127 is not completely useless? :D

How it'd be useless??
@Azanul @AvineshTripathi What to do with #1131 as its getting long convo

@Azanul
Copy link
Collaborator

Azanul commented Oct 30, 2023

I don't think we need to iterate through all the regions every time, we'd need to map only the current region from aws client to the region CF classification.
Let's kill the clutter and take one step at a time. Let's drop the changes in CF distributions till someone has more bandwidth to test it out and check whether our assumptions are correct or not. Instead, let's focus on lambdaedge, it has less room for error.

If you see carefully the region, location loop is not iterating every time Its iterating one region and under that region if only one edge location is matched then it will break the loop and fetch the unit price and again go to another region And you suggested this above comment Since we fetch all resources one region at a time, we can map them to the pricing regions

The location loop is the anti-pattern here. Regions are not handled in every service's file, they're handled in provider's file i.e., aws in this case.

@bishal7679
Copy link
Contributor Author

I don't think we need to iterate through all the regions every time, we'd need to map only the current region from aws client to the region CF classification.
Let's kill the clutter and take one step at a time. Let's drop the changes in CF distributions till someone has more bandwidth to test it out and check whether our assumptions are correct or not. Instead, let's focus on lambdaedge, it has less room for error.

If you see carefully the region, location loop is not iterating every time Its iterating one region and under that region if only one edge location is matched then it will break the loop and fetch the unit price and again go to another region And you suggested this above comment Since we fetch all resources one region at a time, we can map them to the pricing regions

The location loop is the anti-pattern here. Regions are not handled in every service's file, they're handled in provider's file i.e., aws in this case.

since all corresponding edge location of every region of aws.go getRegion func is not present in pricing output how to map all as map[string]string???
Then it'd not be possible to take the region from aws.go thats why it is taken in the service file itself

@Azanul
Copy link
Collaborator

Azanul commented Oct 31, 2023

If you see carefully the region, location loop is not iterating every time Its iterating one region and under that region if only one edge location is matched then it will break the loop and fetch the unit price and again go to another region And you suggested this above comment Since we fetch all resources one region at a time, we can map them to the pricing regions

The location loop is the anti-pattern here. Regions are not handled in every service's file, they're handled in provider's file i.e., aws in this case.

since all corresponding edge location of every region of aws.go getRegion func is not present in pricing output how to map all as map[string]string??? Then it'd not be possible to take the region from aws.go thats why it is taken in the service file itself

try to increase max results for the pricing output

@bishal7679
Copy link
Contributor Author

If you see carefully the region, location loop is not iterating every time Its iterating one region and under that region if only one edge location is matched then it will break the loop and fetch the unit price and again go to another region And you suggested this above comment Since we fetch all resources one region at a time, we can map them to the pricing regions

The location loop is the anti-pattern here. Regions are not handled in every service's file, they're handled in provider's file i.e., aws in this case.

since all corresponding edge location of every region of aws.go getRegion func is not present in pricing output how to map all as map[string]string??? Then it'd not be possible to take the region from aws.go thats why it is taken in the service file itself

try to increase max results for the pricing output

Could you try once either? I've removed all the attribute only using serviceCode.

@Azanul
Copy link
Collaborator

Azanul commented Oct 31, 2023

since all corresponding edge location of every region of aws.go getRegion func is not present in pricing output how to map all as map[string]string??? Then it'd not be possible to take the region from aws.go thats why it is taken in the service file itself

try to increase max results for the pricing output

Could you try once either? I've removed all the attribute only using serviceCode.

When setting MaxResults to 100
op.json

@bishal7679
Copy link
Contributor Author

since all corresponding edge location of every region of aws.go getRegion func is not present in pricing output how to map all as map[string]string??? Then it'd not be possible to take the region from aws.go thats why it is taken in the service file itself

try to increase max results for the pricing output

Could you try once either? I've removed all the attribute only using serviceCode.

When setting MaxResults to 100 op.json

I've taken all the results from your given op.json previously 😃

@Azanul
Copy link
Collaborator

Azanul commented Oct 31, 2023

Could you try once either? I've removed all the attribute only using serviceCode.

When setting MaxResults to 100 op.json

I've taken all the results from your given op.json previously 😃

This is what you get when printing the full paginated result
op.json

@bishal7679
Copy link
Contributor Author

Could you try once either? I've removed all the attribute only using serviceCode.

When setting MaxResults to 100 op.json

I've taken all the results from your given op.json previously 😃

This is what you get when printing the full paginated result op.json

OK! It is giving 2 extra region. suggest modification in my full distribution code and in the location code as well. Would be happy to apply commit suggestion and get this PR ready to merge!

@Azanul
Copy link
Collaborator

Azanul commented Oct 31, 2023

I've taken all the results from your given op.json previously 😃

This is what you get when printing the full paginated result op.json

OK! It is giving 2 extra region. suggest modification in my full distribution code and in the location code as well. Would be happy to apply commit suggestion and get this PR ready to merge!

#1131 (comment)
It is certainly possible

@bishal7679
Copy link
Contributor Author

I've taken all the results from your given op.json previously 😃

This is what you get when printing the full paginated result op.json

OK! It is giving 2 extra region. suggest modification in my full distribution code and in the location code as well. Would be happy to apply commit suggestion and get this PR ready to merge!

#1131 (comment) It is certainly possible

Well moving out the entire code out of the two loop (region, location) and Will I be able to get for which region currently fetching distributions by using client.AWSClient.Region
and compare that to the map which I created?
Does it sounds good?

Signed-off-by: bishal7679 <bishalhnj127@gmail.com>
@bishal7679 bishal7679 force-pushed the feature/correct-cost-for-cloudfront branch from 8f151f1 to 8fcfcc7 Compare October 31, 2023 18:16
@bishal7679
Copy link
Contributor Author

bishal7679 commented Oct 31, 2023

@Azanul Now everything is working nicely!

  • All region is being taken from provider aws.go and being matched with the map I created and fetching the unit price
  • Roughly tested it
    Now only let me know how you got this op.json max result
pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
		ServiceCode: aws.String("AmazonCloudFront"),
	})

will only by ServiceCode work or need to add another attributes?

@bishal7679
Copy link
Contributor Author


// calculate region data transfer out to origin
dataTransferToOrigin := (bytesDownloaded / 1000000000) * 0.02
dataTransferToOriginCost := awsUtils.GetCost(priceMapForDataTransfer[EdgeLocation], (float64(bytesDownloaded)/1099511627776)*1024)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use the freeTierUpload variable so anyone can tell by the name what this calculation should do You might also create a variable for 1024 as well.


}

func getRegionMapping() map[string]string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be better to have this region mapping in an AWS util file so it can be reused by any AWS service needing to map regions.

Also, we should cross-verify if this does not exist (or in a different format) in the codebase as of now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap you are right but this mapping is cloudfront specific
I'm not sure same edge location will be needed for any other service and same edge locations might not be present in their pricingoutput
WDYT??

@Kolawole99
Copy link
Contributor

Kolawole99 commented Nov 1, 2023

@bishal7679 good work you have done with the CloudFront cost calculation

@Azanul
Copy link
Collaborator

Azanul commented Nov 2, 2023

Now only let me know how you got this op.json max result

pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
		ServiceCode: aws.String("AmazonCloudFront"),
	})

will only by ServiceCode work or need to add another attributes?

The result is paginated anyway, so I looped through all the pages (used NextToken) and merged the results into one.

@bishal7679
Copy link
Contributor Author

Now only let me know how you got this op.json max result

pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
		ServiceCode: aws.String("AmazonCloudFront"),
	})

will only by ServiceCode work or need to add another attributes?

The result is paginated anyway, so I looped through all the pages (used NextToken) and merged the results into one.

Yeah! I see
What to do then??

@Azanul
Copy link
Collaborator

Azanul commented Nov 2, 2023

Yeah! I see What to do then??

Let's use upcoming Komiser weekly to have an exhaustive discussion on AWS Cloudfront and our approach for calculating it's cost.

cc: @mlabouardy @jakepage91

@bishal7679
Copy link
Contributor Author

@Azanul what's the update on it

@Azanul
Copy link
Collaborator

Azanul commented Nov 8, 2023

@Azanul what's the update on it

Will soon give an update on this, we might pivot from our current cost approach for AWS support

@Azanul
Copy link
Collaborator

Azanul commented Nov 16, 2023

@bishal7679 We're going to go with cost explorer API integration. It's safe to close this PR

@bishal7679
Copy link
Contributor Author

@bishal7679 We're going to go with cost explorer API integration. It's safe to close this PR

Its been three times closing same PR after a lot of consuming time
But no issue 😐
get it done by cost explorer 👍

@bishal7679 bishal7679 closed this Nov 16, 2023
@Azanul
Copy link
Collaborator

Azanul commented Nov 16, 2023

@bishal7679 We're going to go with cost explorer API integration. It's safe to close this PR

Its been three times closing same PR after a lot of consuming time But no issue 😐 get it done by cost explorer 👍

Another day in a software developer's life

@bishal7679 bishal7679 deleted the feature/correct-cost-for-cloudfront branch January 4, 2024 03:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants