Skip to content

Commit

Permalink
Merge pull request #1009 from Azanul/aws-cost-calculation
Browse files Browse the repository at this point in the history
Aws Lambda cost calculation improvements
  • Loading branch information
mlabouardy authored Sep 29, 2023
2 parents 7a3e8e0 + b5f6a32 commit fea6f9f
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 13 deletions.
68 changes: 55 additions & 13 deletions providers/aws/lambda/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,60 @@ import (
cloudwatchTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/lambda"
"github.com/aws/aws-sdk-go-v2/service/lambda/types"
lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/aws/aws-sdk-go-v2/service/pricing"
"github.com/aws/aws-sdk-go-v2/service/pricing/types"
"github.com/tailwarden/komiser/models"
. "github.com/tailwarden/komiser/models"
. "github.com/tailwarden/komiser/providers"
"github.com/tailwarden/komiser/providers"
awsUtils "github.com/tailwarden/komiser/providers/aws/utils"
"github.com/tailwarden/komiser/utils"
)

func Functions(ctx context.Context, client ProviderClient) ([]Resource, error) {
const (
freeTierInvocations = 1000000
freeTierDuration = 400000
)

func Functions(ctx context.Context, client providers.ProviderClient) ([]models.Resource, error) {
var config lambda.ListFunctionsInput
resources := make([]Resource, 0)
resources := make([]models.Resource, 0)
cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient)
lambdaClient := lambda.NewFromConfig(*client.AWSClient)
pricingClient := pricing.NewFromConfig(*client.AWSClient)

pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
ServiceCode: aws.String("AWSLambda"),
Filters: []types.Filter{
{
Field: aws.String("regionCode"),
Value: aws.String(client.AWSClient.Region),
Type: types.FilterTypeTermMatch,
},
},
})
if err != nil {
log.Errorf("ERROR: Couldn't fetch pricing info for AWS Lambda: %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 := lambdaClient.ListFunctions(context.Background(), &config)
if err != nil {
return resources, err
}

for _, o := range output.Functions {
archSuffix := ""
if o.Architectures[0] == lambdaTypes.ArchitectureArm64 {
archSuffix = "-ARM"
}

metricsInvocationsOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
EndTime: aws.Time(time.Now()),
Expand All @@ -49,13 +84,16 @@ func Functions(ctx context.Context, client ProviderClient) ([]Resource, error) {
})

if err != nil {
log.Warnf("Couldn't fetch invocations metric for %s", *o.FunctionName)
log.Warnf("Couldn't fetch invocations metric for %s: %v", *o.FunctionName, err)
}

invocations := 0.0
if metricsInvocationsOutput != nil && len(metricsInvocationsOutput.Datapoints) > 0 {
invocations = *metricsInvocationsOutput.Datapoints[0].Sum
}
if invocations > freeTierInvocations {
invocations -= freeTierInvocations
}

metricsDurationOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
Expand All @@ -74,26 +112,30 @@ func Functions(ctx context.Context, client ProviderClient) ([]Resource, error) {
},
})
if err != nil {
log.Warnf("Couldn't fetch duration metric for %s", *o.FunctionName)
log.Warnf("Couldn't fetch duration metric for %s: %v", *o.FunctionName, err)
}

duration := 0.0
if metricsDurationOutput != nil && len(metricsDurationOutput.Datapoints) > 0 {
duration = *metricsDurationOutput.Datapoints[0].Average
}
totalDuration := ((invocations * duration) * (float64(*o.MemorySize))) / (1024 * 1024)
if totalDuration < freeTierDuration {
totalDuration -= freeTierDuration
}

computeCharges := (((invocations * duration) * (float64(*o.MemorySize))) / 1024) * 0.0000000083
requestCharges := invocations * 0.2
computeCharges := awsUtils.GetCost(priceMap["AWS-Lambda-Duration"+archSuffix], totalDuration)
requestCharges := awsUtils.GetCost(priceMap["AWS-Lambda-Requests"+archSuffix], invocations)
monthlyCost := computeCharges + requestCharges

tags := make([]Tag, 0)
tags := make([]models.Tag, 0)
tagsResp, err := lambdaClient.ListTags(context.Background(), &lambda.ListTagsInput{
Resource: o.FunctionArn,
})

if err == nil {
for key, value := range tagsResp.Tags {
tags = append(tags, Tag{
tags = append(tags, models.Tag{
Key: key,
Value: value,
})
Expand All @@ -102,7 +144,7 @@ func Functions(ctx context.Context, client ProviderClient) ([]Resource, error) {

relations := getLambdaRelations(*client.AWSClient, o)

resources = append(resources, Resource{
resources = append(resources, models.Resource{
Provider: "AWS",
Account: client.Name,
Service: "Lambda",
Expand Down Expand Up @@ -136,7 +178,7 @@ func Functions(ctx context.Context, client ProviderClient) ([]Resource, error) {
return resources, nil
}

func getLambdaRelations(config aws.Config, lambda types.FunctionConfiguration) (rel []models.Link) {
func getLambdaRelations(config aws.Config, lambda lambdaTypes.FunctionConfiguration) (rel []models.Link) {
// Get associated IAM roles
if lambda.Role != nil {
iamClient := iam.NewFromConfig(config)
Expand Down
74 changes: 74 additions & 0 deletions providers/aws/utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package utils

import (
"encoding/json"
"fmt"
"strconv"

"github.com/aws/aws-sdk-go-v2/service/pricing"
)

type ProductEntry struct {
Product struct {
Attributes struct {
Group string `json:"group"`
Operation string `json:"operation"`
} `json:"attributes"`
} `json:"product"`
Terms struct {
OnDemand map[string]struct {
PriceDimensions map[string]PriceDimensions `json:"priceDimensions"`
} `json:"OnDemand"`
} `json:"terms"`
}

type PriceDimensions struct {
EndRange string `json:"endRange"`
BeginRange float64 `json:"beginRange,string"`
PricePerUnit struct {
USD float64 `json:"USD,string"`
} `json:"pricePerUnit"`
}

func GetCost(pds []PriceDimensions, v float64) float64 {
total := 0.0
for _, pd := range pds {
applicableRange := v
if pd.BeginRange < v {
if pd.EndRange != "Inf" {
endRange, _ := strconv.ParseFloat(pd.EndRange, 64)
if v > endRange {
applicableRange = endRange
}
}
total += (applicableRange - pd.BeginRange) * pd.PricePerUnit.USD
}
}
return total
}

func GetPriceMap(pricingOutput *pricing.GetProductsOutput) (map[string][]PriceDimensions, error) {
priceMap := make(map[string][]PriceDimensions)

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

group := price.Product.Attributes.Group
unitPrices := []PriceDimensions{}
for _, pd := range price.Terms.OnDemand {
for _, p := range pd.PriceDimensions {
unitPrices = append(unitPrices, p)
}
}

priceMap[group] = unitPrices
}
}

return priceMap, nil
}
Loading

0 comments on commit fea6f9f

Please sign in to comment.