Skip to content

Commit

Permalink
updated distribution with location based cost
Browse files Browse the repository at this point in the history
Signed-off-by: bishal7679 <bishalhnj127@gmail.com>
  • Loading branch information
bishal7679 committed Oct 30, 2023
1 parent fe0a48e commit 4ddcb38
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 121 deletions.
1 change: 0 additions & 1 deletion docs/configuration/cloud-providers/aws.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ sidebar_label: Amazon Web Services
- API Gateway
- Access control lists
- CloudFront distributions
- CloudFront functions
- CloudWatch Dashboards
- CloudWatch alarms
- CloudWatch metrics
Expand Down
1 change: 0 additions & 1 deletion policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"apigateway:GET",
"cloudwatch:GetMetricStatistics",
"cloudfront:ListDistributions",
"cloudfront:Functions",
"cloudfront:ListTagsForResource",
"cloudwatch:DescribeAlarms",
"cloudwatch:ListTagsForResource",
Expand Down
277 changes: 162 additions & 115 deletions providers/aws/cloudfront/distributions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cloudfront

import (
"context"
"encoding/json"
"fmt"
"time"

Expand All @@ -26,15 +27,12 @@ const (
per10kRequest = 10000
)

var EdgeLocation string

func Distributions(ctx context.Context, client ProviderClient) ([]Resource, error) {
resources := make([]Resource, 0)
var config cloudfront.ListDistributionsInput
cloudfrontClient := cloudfront.NewFromConfig(*client.AWSClient)

tempRegion := client.AWSClient.Region
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{
Expand All @@ -44,143 +42,192 @@ func Distributions(ctx context.Context, client ProviderClient) ([]Resource, erro
log.Errorf("ERROR: Couldn't fetch pricing info for AWS CloudFront: %v", err)
}

priceMapForDataTransfer, err := awsUtils.GetPriceMap(pricingOutput, "transferType")
priceMapForDataTransfer, err := GetPriceMapCF(pricingOutput, "fromLocation")
if err != nil {
log.Errorf("ERROR: Failed to calculate cost per month: %v", err)
}

priceMapForRequest, err := awsUtils.GetPriceMap(pricingOutput, "requestType")
priceMapForRequest, err := GetPriceMapCF(pricingOutput, "location")
if err != nil {
log.Errorf("ERROR: Failed to calculate cost per month: %v", err)
}

for {
output, err := cloudfrontClient.ListDistributions(ctx, &config)
if err != nil {
return resources, err
}

for _, distribution := range output.DistributionList.Items {
metricsBytesDownloadedOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
EndTime: aws.Time(time.Now()),
MetricName: aws.String("BytesDownloaded"),
Namespace: aws.String("AWS/CloudFront"),
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 invocations metric for %s", *distribution.Id)
}

bytesDownloaded := 0.0
if metricsBytesDownloadedOutput != nil && len(metricsBytesDownloadedOutput.Datapoints) > 0 {
bytesDownloaded = *metricsBytesDownloadedOutput.Datapoints[0].Sum
}
getRegions := getRegionMapping()
for region, locations := range getRegions {
client.AWSClient.Region = region
for _, edgelocation := range locations {
output, err := cloudfrontClient.ListDistributions(ctx, &config)
if err != nil {
return resources, err
}

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{
{
Name: aws.String("DistributionId"),
Value: distribution.Id,
},
},
Period: aws.Int32(86400),
Statistics: []types.Statistic{
types.StatisticSum,
},
})
cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient)

for _, distribution := range output.DistributionList.Items {
metricsBytesDownloadedOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
EndTime: aws.Time(time.Now()),
MetricName: aws.String("BytesDownloaded"),
Namespace: aws.String("AWS/CloudFront"),
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 invocations metric for %s", *distribution.Id)
}
if err != nil {
log.Warnf("Couldn't fetch invocations metric for %s", *distribution.Id)
}

bytesDownloaded := 0.0
if metricsBytesDownloadedOutput != nil && len(metricsBytesDownloadedOutput.Datapoints) > 0 {
bytesDownloaded = *metricsBytesDownloadedOutput.Datapoints[0].Sum
}

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{
{
Name: aws.String("DistributionId"),
Value: distribution.Id,
},
},
Period: aws.Int32(86400),
Statistics: []types.Statistic{
types.StatisticSum,
},
})

requests := 0.0
if metricsRequestsOutput != nil && len(metricsRequestsOutput.Datapoints) > 0 {
requests = *metricsRequestsOutput.Datapoints[0].Sum
}
if requests > freeTierRequests {
requests -= freeTierRequests
}
if err != nil {
log.Warnf("Couldn't fetch invocations metric for %s", *distribution.Id)
}

dataTransferToOriginCost := awsUtils.GetCost(priceMapForDataTransfer["CloudFront to Origin"], (float64(bytesDownloaded)/1099511627776)*1024)
requests := 0.0
if metricsRequestsOutput != nil && len(metricsRequestsOutput.Datapoints) > 0 {
requests = *metricsRequestsOutput.Datapoints[0].Sum
}
if requests > freeTierRequests {
requests -= freeTierRequests
}

requestsCost := awsUtils.GetCost(priceMapForRequest["CloudFront-Request-HTTP-Proxy"], requests/per10kRequest)
if priceMapForDataTransfer[edgelocation] != nil {
EdgeLocation = edgelocation
break
}
dataTransferToOriginCost := awsUtils.GetCost(priceMapForDataTransfer[EdgeLocation], (float64(bytesDownloaded)/1099511627776)*1024)

monthlyCost := dataTransferToOriginCost + requestsCost
requestsCost := awsUtils.GetCost(priceMapForRequest[EdgeLocation], requests/per10kRequest)

outputTags, err := cloudfrontClient.ListTagsForResource(ctx, &cloudfront.ListTagsForResourceInput{
Resource: distribution.ARN,
})
monthlyCost := dataTransferToOriginCost + requestsCost

tags := make([]Tag, 0)
outputTags, err := cloudfrontClient.ListTagsForResource(ctx, &cloudfront.ListTagsForResourceInput{
Resource: distribution.ARN,
})

if err == nil {
for _, tag := range outputTags.Tags.Items {
tags = append(tags, Tag{
Key: *tag.Key,
Value: *tag.Value,
tags := make([]Tag, 0)

if err == nil {
for _, tag := range outputTags.Tags.Items {
tags = append(tags, Tag{
Key: *tag.Key,
Value: *tag.Value,
})
}
}

resources = append(resources, Resource{
Provider: "AWS",
Account: client.Name,
Service: "CloudFront",
ResourceId: *distribution.ARN,
Region: client.AWSClient.Region,
Name: *distribution.DomainName,
Cost: monthlyCost,
Tags: tags,
FetchedAt: time.Now(),
Link: fmt.Sprintf("https://%s.console.aws.amazon.com/cloudfront/v3/home?region=%s#/distributions/%s", client.AWSClient.Region, client.AWSClient.Region, *distribution.Id),
})
}
}

resources = append(resources, Resource{
Provider: "AWS",
Account: client.Name,
Service: "CloudFront",
ResourceId: *distribution.ARN,
Region: client.AWSClient.Region,
Name: *distribution.DomainName,
Cost: monthlyCost,
Tags: tags,
FetchedAt: time.Now(),
Link: fmt.Sprintf("https://%s.console.aws.amazon.com/cloudfront/v3/home?region=%s#/distributions/%s", client.AWSClient.Region, client.AWSClient.Region, *distribution.Id),
})
if aws.ToString(output.DistributionList.NextMarker) == "" {
break
}
config.Marker = output.DistributionList.Marker
}
log.WithFields(log.Fields{
"provider": "AWS",
"account": client.Name,
"region": client.AWSClient.Region,
"service": "CloudFront",
"resources": len(resources),
}).Info("Fetched resources")
return resources, nil
}

if aws.ToString(output.DistributionList.NextMarker) == "" {
break
}
config.Marker = output.DistributionList.Marker
}
log.WithFields(log.Fields{
"provider": "AWS",
"account": client.Name,
"region": client.AWSClient.Region,
"service": "CloudFront",
"resources": len(resources),
}).Info("Fetched resources")
return resources, nil
}

func getCloudFrontDistributionLocation(distributionID string,cloudfrontClient *cloudfront.Client) (string, error) {

func getRegionMapping() map[string][]string {
return map[string][]string{
"us-east-1": {"United States", "Mexico", "Canada"},
"eu-west-1": {"Europe", "Israel"},
"ap-northeast-1": {"Australia", "New Zealand", "Taiwan"},
"ap-northeast-2": {"South Korea"},
"ap-southeast-1": {"Philippines", "Singapore", "Thailand", "Malaysia"},
"ap-southeast-3": {"Indonesia"},
"ap-south-1": {"India"},
"sa-east-1": {"Japan", "South America"},
"me-south-1": {"South Africa", "Kenya", "Middle East"},
"ap-east-1": {"Hong Kong", "Vietnam"},
"cn-north-1": {"China"},
}
}

// GetPriceMapCF is modified functions from awsUtils.GetPriceMap to get CF distribution unit price based on location
func GetPriceMapCF(pricingOutput *pricing.GetProductsOutput, field string) (map[string][]awsUtils.PriceDimensions, error) {
priceMap := make(map[string][]awsUtils.PriceDimensions)

if pricingOutput != nil && len(pricingOutput.PriceList) > 0 {
for _, item := range pricingOutput.PriceList {
price := awsUtils.ProductEntry{}
err := json.Unmarshal([]byte(item), &price)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}

var key string
switch field {
case "fromLocation":
if price.Product.Attributes.TransferType == "CloudFront to Origin" {

// Specify the CloudFront distribution ID for which you want to fetch the regional location
input := &cloudfront.GetDistributionConfigInput{
Id: &distributionID,
}
key = price.Product.Attributes.FromLocation
}
case "location":
if price.Product.Attributes.RequestType == "CloudFront-Request-HTTP-Proxy" {
key = price.Product.Attributes.RequestLocation
}
}

// Fetch the distribution configuration
resp, err := cloudfrontClient.GetDistributionConfig(context.TODO(), input)
if err != nil {
return "", err
}
unitPrices := []awsUtils.PriceDimensions{}
for _, pd := range price.Terms.OnDemand {
for _, p := range pd.PriceDimensions {
unitPrices = append(unitPrices, p)
}
}

priceMap[key] = unitPrices
}
}

// Extract the regional location from the distribution configuration
location := resp.DistributionConfig.Aliases.Items[0]
return location, nil
return priceMap, nil
}
12 changes: 8 additions & 4 deletions providers/aws/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ type ProductEntry struct {
Operation string `json:"operation"`
GroupDescription string `json:"groupDescription"`
RequestDescription string `json:"requestDescription"`
RequestType string `json:"requestType"`
InstanceType string `json:"instanceType"`
InstanceTypeFamily string `json:"instanceTypeFamily"`
TransferType string `json:"transferType"`
FromLocation string `json:"fromLocation"`
RequestLocation string `json:"location"`
} `json:"attributes"`
} `json:"product"`
Terms struct {
Expand Down Expand Up @@ -93,8 +97,8 @@ func GetPriceMap(pricingOutput *pricing.GetProductsOutput, field string) (map[st
}

func Int64PtrToFloat64(i *int64) float64 {
if i == nil {
return 0.0 // or any default value you prefer
}
return float64(*i)
if i == nil {
return 0.0 // or any default value you prefer
}
return float64(*i)
}

0 comments on commit 4ddcb38

Please sign in to comment.