Skip to content

Commit

Permalink
Ogsys 6635 kubecost v2 allocation api (deliveryhero#488)
Browse files Browse the repository at this point in the history
  • Loading branch information
nyambati authored Jun 20, 2023
1 parent d2b99b2 commit 501af1f
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 93 deletions.
9 changes: 6 additions & 3 deletions stable/kubecost-reports-exporter/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
apiVersion: v2
name: kubecost-reports-exporter
description: Helm chart for exporting kubernetes cost reports to S3
description: |
Helm chart for exporting kubernetes cost reports to AWS s3 bucket.
N/B We have updated chart to use V2 scripts using allocations and assets api.
if you are using old installation please use v1 chart
home: https://www.kubecost.com
type: application
version: 1.1.0
appVersion: "1.0.5"
version: 2.0.0
appVersion: "2.0.0"
maintainers:
- name: nyambati
email: [email protected]
Expand Down
12 changes: 7 additions & 5 deletions stable/kubecost-reports-exporter/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# kubecost-reports-exporter

![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.5](https://img.shields.io/badge/AppVersion-1.0.5-informational?style=flat-square)
![Version: 2.0.0](https://img.shields.io/badge/Version-2.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.0.0](https://img.shields.io/badge/AppVersion-2.0.0-informational?style=flat-square)

Helm chart for exporting kubernetes cost reports to S3
Helm chart for exporting kubernetes cost reports to AWS s3 bucket.
N/B We have updated chart to use V2 scripts using allocations and assets api.
if you are using old installation please use v1 chart

**Homepage:** <https://www.kubecost.com>

Expand Down Expand Up @@ -53,13 +55,13 @@ helm install my-release deliveryhero/kubecost-reports-exporter -f values.yaml
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"thomasnyambati/kubecost-reports-exporter"` | |
| imagePullSecrets | list | `[]` | |
| kubecost.aggregatedCostUrl | string | `"/model/aggregatedCostModel?window=1d&aggregation=namespace"` | Url for aggregated cost report |
| kubecost.allocationCostUrl | string | `"/model/allocation?window=15m&aggregate=pod"` | Url for allocation api cost reports |
| kubecost.assetsCostUrl | string | `"/model/assets?window=15m&filterCategories=Compute&filterTypes=Node&filterServices=Kubernetes"` | Url for assets api cost reports |
| kubecost.bucketName | string | `"kubecost-reports-exporter"` | S3 Bucket name for reports export |
| kubecost.clusterName | string | `"change_me"` | Name of the cluster |
| kubecost.clusters[0].endpoint | string | `"http://kubecost-cost-analyzer:9090"` | |
| kubecost.clusters[0].name | string | `"default"` | |
| kubecost.diagnosticsUrl | string | `"/diagnostics/prometheusMetrics"` | Url for prometheus diagnostics |
| kubecost.logLevel | string | `"info"` | exporter log level. |
| kubecost.nonAggregatedCostUrl | string | `"/model/costDataModel?timeWindow=1d&offset=1d"` | Url for non-aggregated cost report |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| podAnnotations | object | `{}` | |
Expand Down
8 changes: 5 additions & 3 deletions stable/kubecost-reports-exporter/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ RUN apt update && apt install -y vim curl net-tools iproute2

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY requirements.txt /tmp
RUN pip3 install --upgrade pip && pip3 install -r /tmp/requirements.txt

COPY cost-exporter.py /usr/local/bin/cost-exporter
COPY cost-exporter-v2.py /usr/local/bin/cost-exporter-v2

RUN chmod +x /usr/local/bin/cost-exporter-v2
155 changes: 155 additions & 0 deletions stable/kubecost-reports-exporter/docker/cost-exporter-v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env python3

import boto3
import json
import logging
import os
import sys
import requests
import sentry_sdk

from sentry_sdk.integrations.logging import LoggingIntegration
from botocore.exceptions import ClientError
from datetime import datetime

PREFIX = "allocation_assets"

class KubecostReportsExporter:
def __init__(self) -> None:

self.log_level = os.environ.get('LOG_LEVEL', 'debug')
self.logger = self.set_log_level(self.log_level)
self.parse_env_vars()
self.s3 = boto3.client('s3')

if self.sentry_dsn:
self.enable_sentry_logging()

def enable_sentry_logging(self):
sentry_logging = LoggingIntegration(
level=logging.INFO, # Capture info and above as breadcrumbs
event_level=logging.ERROR # Send errors as events
)

sentry_sdk.init(dsn=self.sentry_dsn, integrations=[sentry_logging])


def set_log_level(self, log_level):
levels = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL
}

logger = logging.getLogger("kubecost-reports-exporter")
logger.setLevel(levels[log_level])
return logger

def parse_env_vars(self):

self.region = os.environ.get('AWS_REGION', 'eu-west-1')
self.account_canonical_id = os.environ.get('ACCOUNT_CANONICAL_ID')
self.tags = os.environ.get('TAGS', '')
self.sentry_dsn = os.environ.get('SENTRY_DSN')
self.request_timeout = int(os.environ.get('REQUEST_TIMEOUT', '3000'))

try:
self.cluster_name = os.environ['CLUSTER_NAME']
self.bucket_name = os.environ['BUCKET_NAME']
self.kubecost_endpoint = os.environ['KUBECOST_ENDPOINT']
self.kubecost_allocation_url = os.environ['KUBECOST_ALLOCATION_URL']
self.kubecost_assets_url = os.environ['KUBECOST_ASSETS_URL']
self.kubecost_diagnostics_url = os.environ['KUBECOST_DIAGNOSTICS_URL']

except KeyError as ke:
self.logger.error(f"KeyError: environment variable {str(ke)} is not set ")
sys.exit(1)

def build_filename(self):
day_prefix = datetime.utcnow().strftime("%Y_%m_%d")
time_prefix = datetime.utcnow().strftime("%Y%m%d_%H_%M_%S")
return f"{self.cluster_name}/{PREFIX}/{day_prefix}/kubecost_export_{PREFIX}_{time_prefix}.json"

# function calling the kubecost svc to get JSON response with the cost data

def get_cost_report(self, url):
try:
resp = requests.get(url)
if resp.status_code != 200:
self.logger.error(f"Error: Failed to reports from {self.cluster_name}: {resp.reason}")
sys.exit(1)
return resp.json()
except Exception as e:
self.logger.error(f"Could not get cost report: '{str(e)}'")
# Uploading the JSON string to S3 bucket

def upload_report_to_aws(self, bucket, s3_file_name, content, account_id):
self.logger.info(f'Uploading reports for cluster {self.cluster_name}')
try:
# provide object access to bucket owner
self.s3.put_object(
Bucket=bucket,
Body=content,
Key=s3_file_name,
ServerSideEncryption='AES256',
GrantFullControl=f'id="{account_id}"'
)
self.logger.info("Reports uploaded Successful")
except ClientError as e:
self.logger.error(str(e))
sys.exit(1)

def parse_tags_from_env(self, tags):
parsed_tags = {}
if not tags:
return parsed_tags
tags_array = tags.split(",")
for tag in tags_array:
tag = tag.split("=")
parsed_tags[tag[0]] = tag[1]
return parsed_tags

def diagnostics_assets(self):
prometheus_metrics_diag = self.get_cost_report(
self.kubecost_endpoint + self.kubecost_diagnostics_url)
diag_result = prometheus_metrics_diag['data']
for v in diag_result.get('prometheus'):
if v['id'] == 'kubecostMetric':
if not v['passed']:
self.logger.info("If you use custom Prometheus(not kubecost bundle version), please confirm the Kube cost scrape job is configured: https://docs.kubecost.com/install-and-configure/install/custom-prom#steps-to-disable-kubecosts-prometheus-deployment-not-recommended ")

def start(self):

# check that the json has data
report = {}
allocation_report = self.get_cost_report(
self.kubecost_endpoint + self.kubecost_allocation_url)
assets_report = self.get_cost_report(
self.kubecost_endpoint + self.kubecost_assets_url)
if allocation_report["data"] is None:
self.logger.info("Error: Skipping upload, allocation has no data")
return
report.update({'allocation': allocation_report["data"]})

if assets_report["data"] is None or assets_report["data"] == [{}]:
self.logger.info("Error: Skipping upload, assets has no data")
self.diagnostics_assets()
return
# data structure differ from opencost model and analyzer model
if isinstance(assets_report['data'], list):
report.update({'assets': assets_report["data"]})
if isinstance(assets_report['data'], dict):
assets_report_list = [assets_report["data"]]
report.update({'assets': assets_report_list})
self.upload_report_to_aws(
self.bucket_name,
self.build_filename(),
json.dumps(report),
self.account_canonical_id
)


if __name__ == "__main__":
KubecostReportsExporter().start()
Empty file modified stable/kubecost-reports-exporter/docker/cost-exporter.py
100755 → 100644
Empty file.
2 changes: 2 additions & 0 deletions stable/kubecost-reports-exporter/docker/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
boto3>=0.17.0
sentry-sdk==1.3.1
datadog-api-client>=2.0.0
requests
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
{{ $fullname := include "kubecost-reports-exporter.fullname" . }}
{{ $labels := include "kubecost-reports-exporter.labels" . }}
{{ $serviceAccountName := include "kubecost-reports-exporter.serviceAccountName" . }}
{{- range .Values.kubecost.clusters }}
---
apiVersion: batch/v1beta1
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ trunc 49 .name }}-aggregated
name: {{ trunc 49 .name }}-cost-report
labels:
kubecost.cluster: {{ .name }}
{{- $labels | nindent 4 }}
Expand All @@ -23,38 +21,39 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}
labels:
{{- $labels | nindent 12 }}
kubecost.cluster: {{ .name }}
{{- $labels | nindent 12 }}
spec:
{{- with $.Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
{{- toYaml $.Values.podSecurityContext | nindent 12 }}
serviceAccountName: {{ $serviceAccountName }}
serviceAccountName: {{ $.Values.serviceAccountName }}
containers:
- name: {{ $.Chart.Name }}-aggregated
- name: {{ $.Chart.Name }}
securityContext:
{{- toYaml $.Values.securityContext | nindent 14 }}
image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag | default $.Chart.AppVersion }}"
imagePullPolicy: {{ $.Values.image.pullPolicy }}
args:
- cost-exporter
- cost-exporter-v2
envFrom:
- secretRef:
name: {{ $fullname }}
env:
- name: LOG_LEVEL
value: {{ $.Values.kubecost.logLevel | quote }}
- name: BUCKET_NAME
value: {{ required " Aws s3 bucket name is required value" $.Values.kubecost.bucketName | quote }}
- name: CLUSTER_NAME
value: {{ .name | quote }}
- name: REPORT_TYPE
value: "aggregated"
value: {{ required "Cluster name is a required value" .name | quote }}
- name: KUBECOST_ENDPOINT
value: {{ .endpoint | quote }}
- name: KUBECOST_URL
value: {{ required "Kubecost reports url is a required value" $.Values.kubecost.aggregatedCostUrl | quote }}
value: {{ required "Cost-model endpoint is a required value" .endpoint | quote }}
- name: KUBECOST_ALLOCATION_URL
value: {{ required "Kubecost reports url is a required value" $.Values.kubecost.allocationCostUrl | quote }}
- name: KUBECOST_ASSETS_URL
value: {{ required "Kubecost reports url is a required value" $.Values.kubecost.assetsCostUrl | quote }}
- name: KUBECOST_DIAGNOSTICS_URL
value: {{ required "Kubecost reports url is a required value" $.Values.kubecost.diagnosticsUrl | quote }}
restartPolicy: {{ $.Values.restartPolicy }}
{{- end }}

This file was deleted.

12 changes: 6 additions & 6 deletions stable/kubecost-reports-exporter/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ affinity: {}
kubecost:
# kubecost.logLevel -- exporter log level.
logLevel: info
# kubecost.clusterName -- Name of the cluster
clusterName: change_me
# kubecost.bucketName -- S3 Bucket name for reports export
bucketName: kubecost-reports-exporter
# kubecost.aggregatedCostUrl -- Url for aggregated cost report
aggregatedCostUrl: "/model/aggregatedCostModel?window=1d&aggregation=namespace"
# kubecost.nonAggregatedCostUrl -- Url for non-aggregated cost report
nonAggregatedCostUrl: "/model/costDataModel?timeWindow=1d&offset=1d"
# kubecost.allocationCostUrl -- Url for allocation api cost reports
allocationCostUrl: "/model/allocation?window=15m&aggregate=pod"
# kubecost.assetsCostUrl -- Url for assets api cost reports
assetsCostUrl: "/model/assets?window=15m&filterCategories=Compute&filterTypes=Node&filterServices=Kubernetes"
# kubecost.diagnosticsUrl -- Url for prometheus diagnostics
diagnosticsUrl: "/diagnostics/prometheusMetrics"
clusters:
# kubecost.clusters.0.name -- kubecost cluster name
- name: default
Expand Down

0 comments on commit 501af1f

Please sign in to comment.