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

aws-ce-grafana-backend: cluster name config, total costs, total costs per hub #4846

Merged
merged 6 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions helm-charts/aws-ce-grafana-backend/ce-test-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ fullnameOverride: ce-test
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::783616723547:role/aws_ce_grafana_backend_iam_role

envBasedConfig:
clusterName: openscapeshub
1 change: 1 addition & 0 deletions helm-charts/aws-ce-grafana-backend/mounted-files/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ First authenticate yourself against the AWS openscapes account.

```bash
cd helm-charts/aws-ce-grafana-backend/mounted-files
export AWS_CE_GRAFANA_BACKEND__CLUSTER_NAME=openscapeshub
python -m flask --app=webserver run --port=8080

# visit http://localhost:8080/aws
Expand Down
122 changes: 0 additions & 122 deletions helm-charts/aws-ce-grafana-backend/mounted-files/aws.py

This file was deleted.

87 changes: 87 additions & 0 deletions helm-charts/aws-ce-grafana-backend/mounted-files/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Constants used to compose queries against AWS Cost Explorer API.
"""

import os

# Environment variables based config isn't great, see fixme comment in
# values.yaml under the software configuration heading
CLUSTER_NAME = os.environ["AWS_CE_GRAFANA_BACKEND__CLUSTER_NAME"]

# Metrics:
#
# UnblendedCost represents costs for an individual AWS account. It is
# the default metric in the AWS web console. BlendedCosts represents
# the potentially reduced costs stemming from having multiple AWS
# accounts in an organization the collectively could enter better
# pricing tiers.
#
METRICS_UNBLENDED_COST = "UnblendedCost"

# Granularity:
#
# HOURLY granularity is only available for the last two days, while
# DAILY is available for the last 13 months.
#
GRANULARITY_DAILY = "DAILY"

# Filter:
#
# The various filter objects are meant to be combined based on the needs for
# different kinds of queries.
#
FILTER_USAGE_COSTS = {
"Dimensions": {
# RECORD_TYPE is also called Charge type. By filtering on this
# we avoid results related to credits, tax, etc.
"Key": "RECORD_TYPE",
"Values": ["Usage"],
},
}
FILTER_ATTRIBUTABLE_COSTS = {
# ref: https://github.com/2i2c-org/infrastructure/issues/4787#issue-2519110356
"Or": [
{
"Tags": {
"Key": "alpha.eksctl.io/cluster-name",
"Values": [CLUSTER_NAME],
"MatchOptions": ["EQUALS"],
},
},
{
"Tags": {
"Key": f"kubernetes.io/cluster/{CLUSTER_NAME}",
"Values": ["owned"],
"MatchOptions": ["EQUALS"],
},
},
{
"Tags": {
"Key": "2i2c.org/cluster-name",
"Values": [CLUSTER_NAME],
"MatchOptions": ["EQUALS"],
},
},
{
"Not": {
"Tags": {
"Key": "2i2c:hub-name",
"MatchOptions": ["ABSENT"],
},
},
},
{
"Not": {
"Tags": {
"Key": "2i2c:node-purpose",
"MatchOptions": ["ABSENT"],
},
},
},
]
}

GROUP_BY_HUB_TAG = {
"Type": "TAG",
"Key": "2i2c:hub-name",
}
173 changes: 173 additions & 0 deletions helm-charts/aws-ce-grafana-backend/mounted-files/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""
Queries to AWS Cost Explorer to get different kinds of cost data.
"""

import boto3

from .const import (
FILTER_ATTRIBUTABLE_COSTS,
FILTER_USAGE_COSTS,
GRANULARITY_DAILY,
GROUP_BY_HUB_TAG,
METRICS_UNBLENDED_COST,
)

aws_ce_client = boto3.client("ce")


def query_aws_cost_explorer(metrics, granularity, from_date, to_date, filter, group_by):
"""
Function meant to be responsible for making the API call and handling
pagination etc. Currently pagination isn't handled.
"""
# ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ce/client/get_cost_and_usage.html#get-cost-and-usage
response = aws_ce_client.get_cost_and_usage(
Metrics=metrics,
Granularity=granularity,
TimePeriod={"Start": from_date, "End": to_date},
Filter=filter,
GroupBy=group_by,
)
return response


def query_total_costs(from_date, to_date):
"""
A query with processing of the response tailored query to report hub
independent total costs.
"""
response = query_aws_cost_explorer(
metrics=[METRICS_UNBLENDED_COST],
granularity=GRANULARITY_DAILY,
from_date=from_date,
to_date=to_date,
filter={
"And": [
FILTER_USAGE_COSTS,
FILTER_ATTRIBUTABLE_COSTS,
]
},
group_by=[],
)

# response["ResultsByTime"] is a list with entries looking like this...
#
# [
# {
# "Estimated": false,
# "Groups": [],
# "TimePeriod": {
# "End": "2024-07-28",
# "Start": "2024-07-27",
# },
# "Total": {
# "UnblendedCost": {
# "Amount": "23.3110299724",
# "Unit": "USD",
# },
# },
# },
# # ...
# ]
#
# processed_response is a list with entries looking like this...
#
# [
# {
# "date":"2024-08-30",
# "cost":"12.19",
# },
# ]
#
processed_response = [
{
"date": e["TimePeriod"]["Start"],
"cost": f'{float(e["Total"]["UnblendedCost"]["Amount"]):.2f}',
}
for e in response["ResultsByTime"]
]
return processed_response


def query_total_costs_per_hub(from_date, to_date):
"""
A query with processing of the response tailored query to report total costs
per hub, where costs not attributed to a specific hub is listed under
'shared'.
"""
response = query_aws_cost_explorer(
metrics=[METRICS_UNBLENDED_COST],
granularity=GRANULARITY_DAILY,
from_date=from_date,
to_date=to_date,
filter={
"And": [
FILTER_USAGE_COSTS,
FILTER_ATTRIBUTABLE_COSTS,
]
},
group_by=[
GROUP_BY_HUB_TAG,
],
)

# response["ResultsByTime"] is a list with entries looking like this...
#
# [
# {
# "TimePeriod": {"Start": "2024-08-30", "End": "2024-08-31"},
# "Total": {},
# "Groups": [
# {
# "Keys": ["2i2c:hub-name$"],
# "Metrics": {
# "UnblendedCost": {"Amount": "12.1930361882", "Unit": "USD"}
# },
# },
# {
# "Keys": ["2i2c:hub-name$prod"],
# "Metrics": {
# "UnblendedCost": {"Amount": "18.662514854", "Unit": "USD"}
# },
# },
# {
# "Keys": ["2i2c:hub-name$staging"],
# "Metrics": {
# "UnblendedCost": {"Amount": "0.000760628", "Unit": "USD"}
# },
# },
# {
# "Keys": ["2i2c:hub-name$workshop"],
# "Metrics": {
# "UnblendedCost": {"Amount": "0.1969903219", "Unit": "USD"}
# },
# },
# ],
# "Estimated": False,
# },
# ]
#
# processed_response is a list with entries looking like this...
#
# [
# {
# "date":"2024-08-30",
# "cost":"12.19",
# "name":"shared",
# },
# ]
#
processed_response = []
for e in response["ResultsByTime"]:
processed_response.extend(
[
{
"date": e["TimePeriod"]["Start"],
"cost": f'{float(g["Metrics"]["UnblendedCost"]["Amount"]):.2f}',
"name": g["Keys"][0].split("$", maxsplit=1)[1] or "shared",
}
for g in e["Groups"]
]
)

return processed_response
Loading