diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c5a712bc23..7d51c1537c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -57,7 +57,7 @@ repos:
hooks:
- id: sops-encryption
# Add files here if they contain the word 'secret' but should not be encrypted
- exclude: secrets\.md|helm-charts/support/templates/prometheus-ingres-auth/secret\.yaml|helm-charts/basehub/templates/dex/secret\.yaml|helm-charts/basehub/templates/static/secret\.yaml|config/clusters/templates/common/support\.secret\.values\.yaml|helm-charts/basehub/templates/ingress-auth/secret\.yaml
+ exclude: secrets\.md|helm-charts/support/templates/prometheus-ingres-auth/secret\.yaml|helm-charts/basehub/templates/dex/secret\.yaml|helm-charts/basehub/templates/static/secret\.yaml|config/clusters/templates/common/support\.secret\.values\.yaml|helm-charts/basehub/templates/ingress-auth/secret\.yaml|helm-charts/aws-ce-grafana-backend/templates/secret\.yaml
# Prevent known typos from being committed
- repo: https://github.com/codespell-project/codespell
diff --git a/deployer/commands/validate/config.py b/deployer/commands/validate/config.py
index ee42f54d42..a83b29b978 100644
--- a/deployer/commands/validate/config.py
+++ b/deployer/commands/validate/config.py
@@ -64,6 +64,10 @@ def _prepare_helm_charts_dependencies_and_schemas():
_generate_values_schema_json(support_dir)
subprocess.check_call(["helm", "dep", "up", support_dir])
+ aws_ce_grafana_backend = HELM_CHARTS_DIR.joinpath("aws-ce-grafana-backend")
+ _generate_values_schema_json(aws_ce_grafana_backend)
+ subprocess.check_call(["helm", "dep", "up", aws_ce_grafana_backend])
+
def get_list_of_hubs_to_operate_on(cluster_name, hub_name):
config_file_path = find_absolute_path_to_cluster_file(cluster_name)
diff --git a/helm-charts/aws-ce-grafana-backend/.helmignore b/helm-charts/aws-ce-grafana-backend/.helmignore
new file mode 100644
index 0000000000..1205485fae
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/.helmignore
@@ -0,0 +1,33 @@
+# Anything within the root folder of the Helm chart, where Chart.yaml resides,
+# will be embedded into the packaged Helm chart. This is reasonable since only
+# when the templates render after the chart has been packaged and distributed,
+# will the templates logic evaluate that determines if other files were
+# referenced, such as our our files/hub/jupyterhub_config.py.
+#
+# Here are files that we intentionally ignore to avoid them being packaged,
+# because we don't want to reference them from our templates anyhow.
+values.schema.yaml
+
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/helm-charts/aws-ce-grafana-backend/Chart.yaml b/helm-charts/aws-ce-grafana-backend/Chart.yaml
new file mode 100644
index 0000000000..0edc8b92c0
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/Chart.yaml
@@ -0,0 +1,17 @@
+# Chart.yaml v2 reference: https://helm.sh/docs/topics/charts/#the-chartyaml-file
+apiVersion: v2
+name: aws-ce-grafana-backend
+version: "0.0.1-set.by.chartpress"
+appVersion: "1.0.0"
+description:
+ A intermediate backend serving JSON from AWS Cost Explorer API, for use
+ by Grafana dashboard panels via the Infinity datasource plugin to present AWS cloud
+ costs.
+keywords: [aws, cost explorer, grafana, infinity]
+home: https://github.com/2i2c-org/aws-ce-grafana-backend
+sources: [https://github.com/2i2c-org/aws-ce-grafana-backend]
+# icon:
+kubeVersion: ">=1.28.0-0"
+maintainers:
+ - name: Erik Sundell
+ email: erik@2i2c.org
diff --git a/helm-charts/aws-ce-grafana-backend/ce-test-config.yaml b/helm-charts/aws-ce-grafana-backend/ce-test-config.yaml
new file mode 100644
index 0000000000..8ad2443b47
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/ce-test-config.yaml
@@ -0,0 +1,4 @@
+fullnameOverride: ce-test
+serviceAccount:
+ annotations:
+ eks.amazonaws.com/role-arn: arn:aws:iam::783616723547:role/aws_ce_grafana_backend_iam_role
diff --git a/helm-charts/aws-ce-grafana-backend/mounted-files/README.md b/helm-charts/aws-ce-grafana-backend/mounted-files/README.md
new file mode 100644
index 0000000000..73cb11c83b
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/mounted-files/README.md
@@ -0,0 +1,69 @@
+# About code files
+
+The code is meant to help serve grafana with JSON with cost related data,
+initially only from AWS.
+
+## De-coupled from other k8s services
+
+This software doesn't rely to other k8s services, so it can deploy and be tested
+by itself.
+
+## Bundling into Dockerfile vs. mounting in Helm chart
+
+By mounting the code files, development iterations running the code in k8s
+becomes faster.
+
+## Development
+
+### Testing Python changes locally
+
+First authenticate yourself against the AWS openscapes account.
+
+```bash
+cd helm-charts/aws-ce-grafana-backend/mounted-files
+python -m flask --app=webserver run --port=8080
+
+# visit http://localhost:8080/aws
+```
+
+### Testing Python changes in k8s
+
+This is currently being developed in the openscapes cluster. It depends on a k8s
+ServiceAccount coupled to an IAM Role there as well.
+
+The image shouldn't need to be rebuilt unless additional dependencies needs to
+be installed etc, so if you've only made code changes, you can do the following
+to re-deploy.
+
+```bash
+deployer use-cluster-credentials openscapes
+
+cd helm-charts/aws-ce-grafana-backend
+helm upgrade --install --create-namespace -n ce-test --values ce-test-config.yaml ce-test .
+
+# note that port-forward to a service is just a way to port-forward to a pod
+# behind the service, so you need to do the port-forwarding again if the pod
+# restarts.
+kubectl port-forward -n ce-test service/ce-test 8080:http
+
+# visit http://localhost:8080/aws
+```
+
+### Testing image changes in k8s
+
+```bash
+
+cd helm-charts
+
+# before doing this: commit the image change, and stash other changes
+# git status should not report anything
+chartpress --push
+
+# commit the updated image tag
+git add aws-ce-grafana-backend/values.yaml
+git commit -m "aws-ce-grafana-backend chart: update image to deploy"
+
+# WARNING: cleanup of uncommitted files, should be ok if your git status was
+# clean before running chartpress --push
+git reset --hard HEAD
+```
diff --git a/helm-charts/aws-ce-grafana-backend/mounted-files/__init__.py b/helm-charts/aws-ce-grafana-backend/mounted-files/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/helm-charts/aws-ce-grafana-backend/mounted-files/aws.py b/helm-charts/aws-ce-grafana-backend/mounted-files/aws.py
new file mode 100644
index 0000000000..6f57058e21
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/mounted-files/aws.py
@@ -0,0 +1,164 @@
+import boto3
+
+# AWS client functions most likely:
+#
+# - get_cost_and_usage
+# - get_cost_categories
+# - get_tags
+# - list_cost_allocation_tags
+#
+
+
+def query_aws_cost_explorer():
+ aws_ce_client = boto3.client("ce")
+
+ # 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=["UnblendedCost"],
+ Granularity="DAILY",
+ TimePeriod={
+ "Start": "2024-07-01",
+ "End": "2024-08-01",
+ },
+ Filter={
+ "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"],
+ },
+ },
+ GroupBy=[
+ {
+ "Type": "DIMENSION",
+ "Key": "SERVICE",
+ },
+ ],
+ )
+ return response["ResultsByTime"]
+
+
+# Granularity:
+#
+# - HOURLY, DAILY, or MONTHLY
+#
+# - Hourly resolution is only available for the last two days, so we'll use a
+# daily resolution which is available for the last 13 months.
+#
+#
+#
+# Metrics:
+#
+# - Valid choices:
+# - AmortizedCost
+# - BlendedCosts
+# - NetAmortizedCost
+# - NetUnblendedCost
+# - NormalizedUsageAmount
+# - UnblendedCosts
+# - UsageQuantity
+#
+# - UnblendedCosts is the default metric presented in the web console, it
+# represents costs for an individual AWS account. When combining costs in an
+# organization, 1 + 1 <= 2, because the accounts cumulative use can reduce
+# rates.
+#
+# - We'll focus on UnblendedCosts though, because makes the service cost
+# decoupled from other cloud accounts usage.
+#
+# Filter:
+#
+# - RECORD_TYPE is what's named Charge type in the web console, and looking at
+# "Usage" only that helps us avoid responses related to credits, tax, etc.
+#
+# - Dimensions:
+# - AZ
+# - INSTANCE_TYPE
+# - LINKED_ACCOUNT
+# - LINKED_ACCOUNT_NAME
+# - OPERATION
+# - PURCHASE_TYPE
+# - REGION
+# - SERVICE
+# - SERVICE_CODE
+# - USAGE_TYPE
+# - USAGE_TYPE_GROUP
+# - RECORD_TYPE
+# - OPERATING_SYSTEM
+# - TENANCY
+# - SCOPE
+# - PLATFORM
+# - SUBSCRIPTION_ID
+# - LEGAL_ENTITY_NAME
+# - DEPLOYMENT_OPTION
+# - DATABASE_ENGINE
+# - CACHE_ENGINE
+# - INSTANCE_TYPE_FAMILY
+# - BILLING_ENTITY
+# - RESERVATION_ID
+# - RESOURCE_ID (available only for the last 14 days of usage)
+# - RIGHTSIZING_TYPE
+# - SAVINGS_PLANS_TYPE
+# - SAVINGS_PLAN_ARN
+# - PAYMENT_OPTION
+# - AGREEMENT_END_DATE_TIME_AFTER
+# - AGREEMENT_END_DATE_TIME_BEFORE
+# - INVOICING_ENTITY
+# - ANOMALY_TOTAL_IMPACT_ABSOLUTE
+# - ANOMALY_TOTAL_IMPACT_PERCENTAGE
+# - Tags:
+# - Refers to Cost Allocation Tags.
+# - CostCategories:
+# - Can include Cost Allocation Tags, but also references various services
+# etc.
+#
+# GroupBy
+#
+# - Can be an array with up to two string elements, being either:
+# - DIMENSION
+# - TAG
+# - COST_CATEGORY
+#
+
+# Description of Grafana panels wanted by Yuvi:
+# ref: https://github.com/2i2c-org/infrastructure/issues/4453#issuecomment-2298076415
+#
+# Currently our AWS tag 2i2c:hub-name is only capturing a fraction of the costs,
+# so initially only the following panels are easy to work on.
+#
+# - total cost (4)
+# - total cost per component (2)
+#
+# The following panels are dependent on the 2i2c:hub-name tag though.
+#
+# - total cost per hub (1)
+# - total cost per component, repeated per hub (3)
+#
+# Summarized notes about user facing labels:
+#
+# - fixed:
+# - core nodepool
+# - any PV needed for support chart or hub databases
+# - Kubernetes master API
+# - load balancer services
+# - compute:
+# - disks
+# - networking
+# - gpus
+# - home storage:
+# - backups
+# - object storage:
+# - tagged buckets
+# - not counting requester pays
+# - total:
+# - all 2i2c managed infra
+#
+# Working against cost tags directly or cost categories
+#
+# Cost categories vs Cost allocation tags
+#
+# - It seems cost categories could be suitable to group misc data under
+# categories, and split things like core node pool.
+# - I think its worth exploring if we could offload all complexity about user
+# facing labels etc by using cost categories to group and label costs.
+#
diff --git a/helm-charts/aws-ce-grafana-backend/mounted-files/webserver.py b/helm-charts/aws-ce-grafana-backend/mounted-files/webserver.py
new file mode 100644
index 0000000000..de1a515b20
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/mounted-files/webserver.py
@@ -0,0 +1,20 @@
+from flask import Flask
+
+from .aws import query_aws_cost_explorer
+
+app = Flask(__name__)
+
+
+@app.route("/")
+def hello_world():
+ return "
Hello, World!
"
+
+
+@app.route("/health/ready")
+def ready():
+ return ("", 204)
+
+
+@app.route("/aws")
+def aws():
+ return query_aws_cost_explorer()
diff --git a/helm-charts/aws-ce-grafana-backend/templates/NOTES.txt b/helm-charts/aws-ce-grafana-backend/templates/NOTES.txt
new file mode 100644
index 0000000000..632e0d43b4
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/NOTES.txt
@@ -0,0 +1,73 @@
+{{- /* Generated with https://patorjk.com/software/taag/#p=display&h=0&f=Slant&t=BinderHub */}}
+. ____ _ _ __ _____ _____ ___
+ / __ `/| | /| / / / ___/ ______ / ___/ / _ \ ______
+/ /_/ / | |/ |/ / (__ ) /_____// /__ / __//_____/
+\__,_/ |__/|__/ /____/ ____\___/ \___/
+ ____ _ _____ ____ _ / __/ ____ _ ____ ____ _
+ / __ `/ / ___/ / __ `/ / /_ / __ `/ / __ \ / __ `/ ______
+ / /_/ / / / / /_/ / / __/ / /_/ / / / / // /_/ / /_____/
+ \__, / /_/ \__,_/ /_/ \__,_/ /_/ /_/ \__,_/
+/____/ __ __
+ / /_ ____ _ _____ / /__ ___ ____ ____/ /
+ / __ \ / __ `/ / ___/ / //_/ / _ \ / __ \ / __ /
+ / /_/ // /_/ / / /__ / ,< / __/ / / / // /_/ /
+/_.___/ \__,_/ \___/ /_/|_| \___/ /_/ /_/ \__,_/
+
+You have successfully installed the AWS Cost Explorer Grafana Backend Helm chart!
+
+### Installation info
+
+ - Kubernetes namespace: {{ .Release.Namespace }}
+ - Helm release name: {{ .Release.Name }}
+ - Helm chart version: {{ .Chart.Version }}
+ - Python packages: See https://github.com/2i2c-org/aws-ce-grafana-backend/blob/{{ include "aws-ce-grafana-backend.chart-version-to-git-ref" .Chart.Version }}/images/aws-ce-grafana-backend/requirements.txt
+
+### Followup links
+
+ - Documentation: https://github.com/2i2c-org/aws-ce-grafana-backend#readme
+ - Issue tracking: https://github.com/2i2c-org/aws-ce-grafana-backend/issues
+
+### Post-installation checklist
+
+ - Verify that created Pods enter a Running state:
+
+ kubectl --namespace={{ .Release.Namespace }} get pod
+
+ If a pod is stuck with a Pending or ContainerCreating status, diagnose with:
+
+ kubectl --namespace={{ .Release.Namespace }} describe pod
+
+ If a pod keeps restarting, diagnose with:
+
+ kubectl --namespace={{ .Release.Namespace }} logs --previous
+ {{- println }}
+
+
+
+{{- /*
+ Breaking changes.
+*/}}
+
+{{- $breaking := "" }}
+{{- $breaking_title := "\n" }}
+{{- $breaking_title = print $breaking_title "\n#################################################################################" }}
+{{- $breaking_title = print $breaking_title "\n###### BREAKING: The config values passed contained no longer accepted #####" }}
+{{- $breaking_title = print $breaking_title "\n###### options. See the messages below for more details. #####" }}
+{{- $breaking_title = print $breaking_title "\n###### #####" }}
+{{- $breaking_title = print $breaking_title "\n###### To verify your updated config is accepted, you can use #####" }}
+{{- $breaking_title = print $breaking_title "\n###### the `helm template` command. #####" }}
+{{- $breaking_title = print $breaking_title "\n#################################################################################" }}
+
+
+{{- /*
+ This is an example (in a helm template comment) on how to detect and
+ communicate with regards to a breaking chart config change.
+
+ {{- if hasKey .Values.singleuser.cloudMetadata "enabled" }}
+ {{- $breaking = print $breaking "\n\nCHANGED: singleuser.cloudMetadata.enabled must as of 1.0.0 be configured using singleuser.cloudMetadata.blockWithIptables with the opposite value." }}
+ {{- end }}
+*/}}
+
+{{- if $breaking }}
+{{- fail (print $breaking_title $breaking "\n\n") }}
+{{- end }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/_helpers-extra-files.tpl b/helm-charts/aws-ce-grafana-backend/templates/_helpers-extra-files.tpl
new file mode 100644
index 0000000000..889f13091e
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/_helpers-extra-files.tpl
@@ -0,0 +1,72 @@
+{{- /*
+ aws-ce-grafana-backend.extraFiles.data:
+ Renders content for a k8s Secret's data field, coming from extraFiles with
+ binaryData entries.
+*/}}
+{{- define "aws-ce-grafana-backend.extraFiles.data.withNewLineSuffix" -}}
+ {{- range $file_key, $file_details := . }}
+ {{- include "aws-ce-grafana-backend.extraFiles.validate-file" (list $file_key $file_details) }}
+ {{- if $file_details.binaryData }}
+ {{- $file_key | quote }}: {{ $file_details.binaryData | nospace | quote }}{{ println }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+{{- define "aws-ce-grafana-backend.extraFiles.data" -}}
+ {{- include "aws-ce-grafana-backend.extraFiles.data.withNewLineSuffix" . | trimSuffix "\n" }}
+{{- end }}
+
+{{- /*
+ aws-ce-grafana-backend.extraFiles.stringData:
+ Renders content for a k8s Secret's stringData field, coming from extraFiles
+ with either data or stringData entries.
+*/}}
+{{- define "aws-ce-grafana-backend.extraFiles.stringData.withNewLineSuffix" -}}
+ {{- range $file_key, $file_details := . }}
+ {{- include "aws-ce-grafana-backend.extraFiles.validate-file" (list $file_key $file_details) }}
+ {{- $file_name := $file_details.mountPath | base }}
+ {{- if $file_details.stringData }}
+ {{- $file_key | quote }}: |
+ {{- $file_details.stringData | trimSuffix "\n" | nindent 2 }}{{ println }}
+ {{- end }}
+ {{- if $file_details.data }}
+ {{- $file_key | quote }}: |
+ {{- if or (eq (ext $file_name) ".yaml") (eq (ext $file_name) ".yml") }}
+ {{- $file_details.data | toYaml | nindent 2 }}{{ println }}
+ {{- else if eq (ext $file_name) ".json" }}
+ {{- $file_details.data | toJson | nindent 2 }}{{ println }}
+ {{- else if eq (ext $file_name) ".toml" }}
+ {{- $file_details.data | toToml | trimSuffix "\n" | nindent 2 }}{{ println }}
+ {{- else }}
+ {{- print "\n\nextraFiles entries with 'data' (" $file_key " > " $file_details.mountPath ") needs to have a filename extension of .yaml, .yml, .json, or .toml!" | fail }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+{{- define "aws-ce-grafana-backend.extraFiles.stringData" -}}
+ {{- include "aws-ce-grafana-backend.extraFiles.stringData.withNewLineSuffix" . | trimSuffix "\n" }}
+{{- end }}
+
+{{- define "aws-ce-grafana-backend.extraFiles.validate-file" -}}
+ {{- $file_key := index . 0 }}
+ {{- $file_details := index . 1 }}
+
+ {{- /* Use of mountPath. */}}
+ {{- if not ($file_details.mountPath) }}
+ {{- print "\n\nextraFiles entries (" $file_key ") must contain the field 'mountPath'." | fail }}
+ {{- end }}
+
+ {{- /* Use one of stringData, binaryData, data. */}}
+ {{- $field_count := 0 }}
+ {{- if $file_details.data }}
+ {{- $field_count = add1 $field_count }}
+ {{- end }}
+ {{- if $file_details.stringData }}
+ {{- $field_count = add1 $field_count }}
+ {{- end }}
+ {{- if $file_details.binaryData }}
+ {{- $field_count = add1 $field_count }}
+ {{- end }}
+ {{- if ne $field_count 1 }}
+ {{- print "\n\nextraFiles entries (" $file_key ") must only contain one of the fields: 'data', 'stringData', and 'binaryData'." | fail }}
+ {{- end }}
+{{- end }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/_helpers-labels.tpl b/helm-charts/aws-ce-grafana-backend/templates/_helpers-labels.tpl
new file mode 100644
index 0000000000..0e3702d123
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/_helpers-labels.tpl
@@ -0,0 +1,19 @@
+{{- /*
+ Common labels
+*/}}
+{{- define "aws-ce-grafana-backend.labels" -}}
+helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{ include "aws-ce-grafana-backend.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{- /*
+ Selector labels
+*/}}
+{{- define "aws-ce-grafana-backend.selectorLabels" -}}
+app.kubernetes.io/name: {{ .Values.nameOverride | default .Chart.Name | trunc 63 | trimSuffix "-" }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/_helpers-names.tpl b/helm-charts/aws-ce-grafana-backend/templates/_helpers-names.tpl
new file mode 100644
index 0000000000..37d9cacd20
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/_helpers-names.tpl
@@ -0,0 +1,113 @@
+{{- /*
+ These helpers encapsulates logic on how we name resources. They also enable
+ parent charts to reference these dynamic resource names.
+
+ To avoid duplicating documentation, for more information, please see the the
+ fullnameOverride entry the jupyterhub chart's configuration reference:
+ https://z2jh.jupyter.org/en/latest/resources/reference.html#fullnameOverride
+*/}}
+
+
+
+{{- /*
+ Utility templates
+*/}}
+
+{{- /*
+ Renders to a prefix for the chart's resource names. This prefix is assumed to
+ make the resource name cluster unique.
+*/}}
+{{- define "aws-ce-grafana-backend.fullname" -}}
+ {{- /*
+ We have implemented a trick to allow a parent chart depending on this
+ chart to call these named templates.
+
+ Caveats and notes:
+
+ 1. While parent charts can reference these, grandparent charts can't.
+ 2. Parent charts must not use an alias for this chart.
+ 3. There is no failsafe workaround to above due to
+ https://github.com/helm/helm/issues/9214.
+ 4. .Chart is of its own type (*chart.Metadata) and needs to be casted
+ using "toYaml | fromYaml" in order to be able to use normal helm
+ template functions on it.
+ */}}
+ {{- $fullname_override := .Values.fullnameOverride }}
+ {{- $name_override := .Values.nameOverride }}
+ {{- if ne .Chart.Name "aws-ce-grafana-backend" }}
+ {{- if .Values.jupyterhub }}
+ {{- $fullname_override = .Values.jupyterhub.fullnameOverride }}
+ {{- $name_override = .Values.jupyterhub.nameOverride }}
+ {{- end }}
+ {{- end }}
+
+ {{- if eq (typeOf $fullname_override) "string" }}
+ {{- $fullname_override }}
+ {{- else }}
+ {{- $name := $name_override | default .Chart.Name }}
+ {{- if contains $name .Release.Name }}
+ {{- .Release.Name }}
+ {{- else }}
+ {{- .Release.Name }}-{{ $name }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{- /*
+ Renders to a blank string or if the fullname template is truthy renders to it
+ with an appended dash.
+*/}}
+{{- define "aws-ce-grafana-backend.fullname.dash" -}}
+ {{- if (include "aws-ce-grafana-backend.fullname" .) }}
+ {{- include "aws-ce-grafana-backend.fullname" . }}-
+ {{- end }}
+{{- end }}
+
+
+
+{{- /*
+ Namespaced resources
+*/}}
+
+{{- /* webserver resources' default name */}}
+{{- define "aws-ce-grafana-backend.webserver.fullname" -}}
+ {{- if (include "aws-ce-grafana-backend.fullname" .) }}
+ {{- include "aws-ce-grafana-backend.fullname" . }}
+ {{- else -}}
+ aws-ce-grafana-backend
+ {{- end }}
+{{- end }}
+
+{{- /* webserver's ServiceAccount name */}}
+{{- define "aws-ce-grafana-backend.webserver.serviceaccount.fullname" -}}
+ {{- if .Values.serviceAccount.create }}
+ {{- .Values.serviceAccount.name | default (include "aws-ce-grafana-backend.webserver.fullname" .) }}
+ {{- else }}
+ {{- .Values.serviceAccount.name }}
+ {{- end }}
+{{- end }}
+
+{{- /* webserver's Ingress name */}}
+{{- define "aws-ce-grafana-backend.webserver.ingress.fullname" -}}
+ {{- if (include "aws-ce-grafana-backend.fullname" .) }}
+ {{- include "aws-ce-grafana-backend.fullname" . }}
+ {{- else -}}
+ aws-ce-grafana-backend
+ {{- end }}
+{{- end }}
+
+
+
+{{- /*
+ Cluster wide resources
+
+ We enforce uniqueness of names for our cluster wide resources. We assume that
+ the prefix from setting fullnameOverride to null or a string will be cluster
+ unique.
+*/}}
+
+{{- /*
+ We currently have no cluster wide resources, but if you add one below in the
+ future, remove this comment and add an entry mimicking how the jupyterhub helm
+ chart does it.
+*/}}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/_helpers.tpl b/helm-charts/aws-ce-grafana-backend/templates/_helpers.tpl
new file mode 100644
index 0000000000..fc86130493
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/_helpers.tpl
@@ -0,0 +1,18 @@
+{{- /*
+ aws-ce-grafana-backend.chart-version-to-git-ref:
+ Renders a valid git reference from a chartpress generated version string.
+ In practice, either a git tag or a git commit hash will be returned.
+
+ - The version string will follow a chartpress pattern,
+ like "0.1.0-0.dev.git.17.h8368bc0", see
+ https://github.com/jupyterhub/chartpress#examples-chart-versions-and-image-tags.
+
+ - The regexReplaceAll function is a sprig library function, see
+ https://masterminds.github.io/sprig/strings.html.
+
+ - The regular expression is in golang syntax, but \d had to become \\d for
+ example.
+*/}}
+{{- define "aws-ce-grafana-backend.chart-version-to-git-ref" -}}
+{{- regexReplaceAll ".*\\.git\\.\\d+\\.h(.*)" . "${1}" }}
+{{- end }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/deployment.yaml b/helm-charts/aws-ce-grafana-backend/templates/deployment.yaml
new file mode 100644
index 0000000000..a9489f8776
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/deployment.yaml
@@ -0,0 +1,76 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "aws-ce-grafana-backend.webserver.fullname" . }}
+ labels:
+ {{- include "aws-ce-grafana-backend.labels" . | nindent 4 }}
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ {{- include "aws-ce-grafana-backend.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ annotations:
+ checksum/mounted-secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }}
+ checksum/service-account: {{ include (print .Template.BasePath "/serviceaccount.yaml") . | sha256sum }}
+ {{- with .Values.podAnnotations }}
+ {{- . | toYaml | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include "aws-ce-grafana-backend.labels" . | nindent 8 }}
+ spec:
+ volumes:
+ - name: secret
+ secret:
+ secretName: {{ include "aws-ce-grafana-backend.webserver.fullname" . }}
+ containers:
+ - name: webserver
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ {{- with .Values.image.pullPolicy }}
+ imagePullPolicy: {{ . }}
+ {{- end }}
+ ports:
+ - name: http
+ containerPort: 8080
+ volumeMounts:
+ - name: secret
+ mountPath: /srv/aws-ce-grafana-backend
+ readOnly: true
+ {{- with .Values.extraEnv }}
+ env:
+ {{- tpl (. | toYaml) $ | nindent 12 }}
+ {{- end }}
+ resources:
+ {{- .Values.resources | toYaml | nindent 12 }}
+ securityContext:
+ {{- .Values.securityContext | toYaml | nindent 12 }}
+ startupProbe:
+ periodSeconds: 1
+ failureThreshold: 60
+ httpGet:
+ path: /health/ready
+ port: http
+ {{- with .Values.image.pullSecrets }}
+ imagePullSecrets:
+ {{- . | toYaml | nindent 8 }}
+ {{- end }}
+ {{- with include "aws-ce-grafana-backend.webserver.serviceaccount.fullname" . }}
+ serviceAccountName: {{ . }}
+ {{- end }}
+ {{- with .Values.podSecurityContext }}
+ securityContext:
+ {{- . | toYaml | nindent 8 }}
+ {{- end }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- . | toYaml | nindent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+ {{- . | toYaml | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- . | toYaml | nindent 8 }}
+ {{- end }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/ingress.yaml b/helm-charts/aws-ce-grafana-backend/templates/ingress.yaml
new file mode 100644
index 0000000000..7e2bb5674c
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/ingress.yaml
@@ -0,0 +1,35 @@
+{{- if .Values.ingress.enabled -}}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ include "aws-ce-grafana-backend.webserver.ingress.fullname" . }}
+ labels:
+ {{- include "aws-ce-grafana-backend.labels" . | nindent 4 }}
+ {{- with .Values.ingress.annotations }}
+ annotations:
+ {{- . | toYaml | nindent 4 }}
+ {{- end }}
+spec:
+ {{- with .Values.ingress.ingressClassName }}
+ ingressClassName: "{{ . }}"
+ {{- end }}
+ rules:
+ {{- range $host := .Values.ingress.hosts | default (list "") }}
+ - http:
+ paths:
+ - path: {{ $.Values.config.BinderHub.base_url | trimSuffix "/" }}/{{ $.Values.ingress.pathSuffix }}
+ pathType: {{ $.Values.ingress.pathType }}
+ backend:
+ service:
+ name: {{ include "aws-ce-grafana-backend.webserver.fullname" $ }}
+ port:
+ name: http
+ {{- if $host }}
+ host: {{ $host | quote }}
+ {{- end }}
+ {{- end }}
+ {{- with .Values.ingress.tls }}
+ tls:
+ {{- . | toYaml | nindent 4 }}
+ {{- end }}
+{{- end }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/secret.yaml b/helm-charts/aws-ce-grafana-backend/templates/secret.yaml
new file mode 100644
index 0000000000..01ed4c283b
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/secret.yaml
@@ -0,0 +1,13 @@
+{{- /*
+ Changes to this rendered manifest triggers a restart of the aws-ce-grafana-backend
+ pod as the pod specification includes an annotation with a checksum of this.
+*/ -}}
+kind: Secret
+apiVersion: v1
+metadata:
+ name: {{ include "aws-ce-grafana-backend.webserver.fullname" . }}
+ labels:
+ {{- include "aws-ce-grafana-backend.labels" . | nindent 4 }}
+type: Opaque
+stringData:
+ {{- (.Files.Glob "mounted-files/*").AsConfig | nindent 2 }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/service.yaml b/helm-charts/aws-ce-grafana-backend/templates/service.yaml
new file mode 100644
index 0000000000..f3822329b3
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/service.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "aws-ce-grafana-backend.webserver.fullname" . }}
+ labels: {{- include "aws-ce-grafana-backend.labels" . | nindent 4 }}
+ {{- with .Values.service.annotations }}
+ annotations:
+ {{- . | toYaml | nindent 4 }}
+ {{- end }}
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - name: http
+ port: {{ .Values.service.port }}
+ targetPort: http
+ {{- with .Values.service.nodePort }}
+ nodePort: {{ . }}
+ {{- end }}
+ selector: {{- include "aws-ce-grafana-backend.selectorLabels" . | nindent 4 }}
diff --git a/helm-charts/aws-ce-grafana-backend/templates/serviceaccount.yaml b/helm-charts/aws-ce-grafana-backend/templates/serviceaccount.yaml
new file mode 100644
index 0000000000..8fc552ece9
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/templates/serviceaccount.yaml
@@ -0,0 +1,12 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "aws-ce-grafana-backend.webserver.serviceaccount.fullname" . }}
+ labels:
+ {{- include "aws-ce-grafana-backend.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- . | toYaml | nindent 4 }}
+ {{- end }}
+{{- end }}
diff --git a/helm-charts/aws-ce-grafana-backend/values.schema.yaml b/helm-charts/aws-ce-grafana-backend/values.schema.yaml
new file mode 100644
index 0000000000..2d793009bb
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/values.schema.yaml
@@ -0,0 +1,140 @@
+# This schema (a JSONSchema in YAML format) is used to generate
+# values.schema.json to be packaged with the Helm chart.
+#
+# This schema is also planned to be used by our documentation system to build
+# the configuration reference section based on the description fields. See
+# docs/source/conf.py for that logic in the future!
+#
+# We look to document everything we have default values for in values.yaml, but
+# we don't look to enforce the perfect validation logic within this file.
+#
+# ref: https://helm.sh/docs/topics/charts/#schema-files
+# ref: https://json-schema.org/learn/getting-started-step-by-step.html
+#
+$schema: http://json-schema.org/draft-07/schema#
+type: object
+additionalProperties: false
+required:
+ # General configuration
+ - global
+ # Deployment resource
+ - image
+ # Other resources
+ - serviceAccount
+ - service
+ - ingress
+properties:
+ # Flag to conditionally install the chart
+ # ---------------------------------------------------------------------------
+ #
+ enabled:
+ type: boolean
+ description: |
+ Configuration flag for charts depending on aws-ce-grafana-backend to toggle installing it.
+
+ # General configuration
+ # ---------------------------------------------------------------------------
+ #
+ nameOverride:
+ type: [string, "null"]
+ fullnameOverride:
+ type: [string, "null"]
+ global:
+ type: object
+ additionalProperties: true
+
+ # Deployment resource
+ # ---------------------------------------------------------------------------
+ #
+ replicas:
+ type: integer
+ extraEnv:
+ type: array
+ image: &image
+ type: object
+ additionalProperties: false
+ required: [repository, tag]
+ properties:
+ repository:
+ type: string
+ tag:
+ type: string
+ pullPolicy:
+ enum: [null, "", IfNotPresent, Always, Never]
+ pullSecrets:
+ type: array
+ resources: &resources
+ type: object
+ additionalProperties: true
+ securityContext: &securityContext
+ type: object
+ additionalProperties: true
+ podSecurityContext: &podSecurityContext
+ type: object
+ additionalProperties: true
+ podAnnotations: &labels-and-annotations
+ type: object
+ additionalProperties: false
+ patternProperties:
+ ".*":
+ type: string
+ nodeSelector: &nodeSelector
+ type: object
+ additionalProperties: true
+ affinity: &affinity
+ type: object
+ additionalProperties: true
+ tolerations: &tolerations
+ type: array
+
+ # ServiceAccount resource
+ # ---------------------------------------------------------------------------
+ #
+ serviceAccount:
+ type: object
+ additionalProperties: false
+ required: [create, name]
+ properties:
+ create:
+ type: boolean
+ name:
+ type: string
+ annotations: *labels-and-annotations
+
+ # Service resource
+ # ---------------------------------------------------------------------------
+ #
+ service:
+ type: object
+ additionalProperties: false
+ required: [type, port]
+ properties:
+ type:
+ type: string
+ port:
+ type: integer
+ nodePort:
+ type: integer
+ annotations: *labels-and-annotations
+
+ # Ingress resource
+ # ---------------------------------------------------------------------------
+ #
+ ingress:
+ type: object
+ additionalProperties: false
+ required: [enabled]
+ properties:
+ enabled:
+ type: boolean
+ annotations: *labels-and-annotations
+ ingressClassName:
+ type: [string, "null"]
+ hosts:
+ type: array
+ pathSuffix:
+ type: [string, "null"]
+ pathType:
+ enum: [Prefix, Exact, ImplementationSpecific]
+ tls:
+ type: array
diff --git a/helm-charts/aws-ce-grafana-backend/values.yaml b/helm-charts/aws-ce-grafana-backend/values.yaml
new file mode 100644
index 0000000000..479a18b45c
--- /dev/null
+++ b/helm-charts/aws-ce-grafana-backend/values.yaml
@@ -0,0 +1,59 @@
+# General configuration
+# -----------------------------------------------------------------------------
+#
+nameOverride: ""
+fullnameOverride: ""
+global: {}
+
+# Deployment resource
+# -----------------------------------------------------------------------------
+#
+replicas: 1
+extraEnv: []
+image:
+ repository: quay.io/2i2c/aws-ce-grafana-backend
+ tag: "0.0.1-0.dev.git.10263.hc87b65cf"
+ pullPolicy: ""
+ pullSecrets: []
+resources: {}
+securityContext:
+ capabilities:
+ drop:
+ - ALL
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsUser: 65534 # nobody user
+ runAsGroup: 65534 # nobody group
+
+podSecurityContext: {}
+podAnnotations: {}
+nodeSelector: {}
+affinity: {}
+tolerations: []
+
+# ServiceAccount resource
+# -----------------------------------------------------------------------------
+#
+serviceAccount:
+ create: true
+ name: ""
+ annotations: {}
+
+# Service resource
+# -----------------------------------------------------------------------------
+#
+service:
+ type: ClusterIP
+ port: 80
+
+# Ingress resource
+# -----------------------------------------------------------------------------
+#
+ingress:
+ enabled: false
+ annotations: {}
+ ingressClassName:
+ hosts: []
+ pathSuffix:
+ pathType: Prefix
+ tls: []
diff --git a/helm-charts/chartpress.yaml b/helm-charts/chartpress.yaml
index f016fedb34..1948922525 100644
--- a/helm-charts/chartpress.yaml
+++ b/helm-charts/chartpress.yaml
@@ -27,3 +27,8 @@ charts:
gcp-filestore-backups:
imageName: quay.io/2i2c/gcp-filestore-backups
valuesPath: gcpFilestoreBackups.image
+ - name: aws-ce-grafana-backend
+ images:
+ aws-ce-grafana-backend:
+ imageName: quay.io/2i2c/aws-ce-grafana-backend
+ valuesPath: image
diff --git a/helm-charts/images/aws-ce-grafana-backend/Dockerfile b/helm-charts/images/aws-ce-grafana-backend/Dockerfile
new file mode 100644
index 0000000000..0b9615d447
--- /dev/null
+++ b/helm-charts/images/aws-ce-grafana-backend/Dockerfile
@@ -0,0 +1,53 @@
+# syntax = docker/dockerfile:1.3
+
+
+# The build stage
+# ---------------
+# This stage is building Python wheels for use in later stages by using a base
+# image that has more pre-requisites to do so, such as a C++ compiler.
+#
+FROM python:3.12-bullseye as build-stage
+
+# Build wheels
+#
+# We set pip's cache directory and expose it across build stages via an
+# ephemeral docker cache (--mount=type=cache,target=${PIP_CACHE_DIR}). We use
+# the same technique for the directory /tmp/wheels.
+#
+COPY requirements.txt requirements.txt
+ARG PIP_CACHE_DIR=/tmp/pip-cache
+RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
+ pip wheel \
+ --wheel-dir=/tmp/wheels \
+ -r requirements.txt
+
+
+# The final stage
+# ---------------
+#
+FROM python:3.12-slim-bullseye as slim-stage
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update \
+ && apt-get upgrade -y \
+ && apt-get install -y --no-install-recommends \
+ tini \
+ && rm -rf /var/lib/apt/lists/*
+
+# install wheels built in the build stage
+# --no-index ensures _only_ wheels from the build stage are installed
+COPY requirements.txt /tmp/requirements.txt
+ARG PIP_CACHE_DIR=/tmp/pip-cache
+RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
+ --mount=type=cache,from=build-stage,source=/tmp/wheels,target=/tmp/wheels \
+ pip install \
+ --no-index \
+ --find-links=/tmp/wheels/ \
+ -r /tmp/requirements.txt
+
+WORKDIR /srv/aws-ce-grafana-backend
+
+USER 65534
+EXPOSE 8080
+ENTRYPOINT ["tini", "--"]
+CMD ["python", "-m", "flask", "--app=webserver", "run", "--host=0.0.0.0", "--port=8080"]
diff --git a/helm-charts/images/aws-ce-grafana-backend/requirements.txt b/helm-charts/images/aws-ce-grafana-backend/requirements.txt
new file mode 100644
index 0000000000..aa55d9895a
--- /dev/null
+++ b/helm-charts/images/aws-ce-grafana-backend/requirements.txt
@@ -0,0 +1,2 @@
+flask
+boto3
diff --git a/helm-charts/support/Chart.yaml b/helm-charts/support/Chart.yaml
index 7c58c47de8..4c3f8b3708 100644
--- a/helm-charts/support/Chart.yaml
+++ b/helm-charts/support/Chart.yaml
@@ -45,3 +45,10 @@ dependencies:
version: "0.3.1-0.dev.git.143.hfc89744"
repository: https://cryptnono.github.io/cryptnono/
condition: cryptnono.enabled
+
+ # aws-ce-grafana-backend, exposes AWS Cost Explorer API info to Grafana
+ # Source code: https://github.com/2i2c/infrastructure/
+ - name: aws-ce-grafana-backend
+ version: "0.0.1-set.by.chartpress"
+ repository: "file://../aws-ce-grafana-backend"
+ condition: aws-ce-grafana-backend.enabled
diff --git a/helm-charts/support/values.schema.yaml b/helm-charts/support/values.schema.yaml
index 3b29dfe2cc..4f140aadf5 100644
--- a/helm-charts/support/values.schema.yaml
+++ b/helm-charts/support/values.schema.yaml
@@ -20,6 +20,7 @@ required:
- cryptnono
- redirects
- gcpFilestoreBackups
+ - aws-ce-grafana-backend
- global
properties:
# cluster-autoscaler is a dependent helm chart, we rely on its schema
@@ -46,6 +47,12 @@ properties:
grafana:
type: object
additionalProperties: true
+ # aws-ce-grafana-backend is a dependent helm chart, we rely on its schema
+ # validation for values passed to it and are not imposing restrictions on them
+ # in this helm chart.
+ aws-ce-grafana-backend:
+ type: object
+ additionalProperties: true
# Enables https://github.com/yuvipanda/cryptnono/ to prevent cryptomining
cryptnono:
type: object
diff --git a/helm-charts/support/values.yaml b/helm-charts/support/values.yaml
index 512facb164..18b6e3837b 100644
--- a/helm-charts/support/values.yaml
+++ b/helm-charts/support/values.yaml
@@ -469,6 +469,13 @@ cryptnono:
memory: 64Mi
cpu: 1m
+# aws-ce-grafana-backend exposes AWS Cost Explorer API info to Grafana
+#
+# values ref: https://github.com/2i2c-org/infrastructure/blob/main/helm-charts/aws-ce-grafana-backend/values.yaml
+#
+aws-ce-grafana-backend:
+ enabled: false
+
# Configuration of templates provided directly by this chart
# -------------------------------------------------------------------------------
#
diff --git a/terraform/aws/aws-ce-grafana-backend-iam.tf b/terraform/aws/aws-ce-grafana-backend-iam.tf
new file mode 100644
index 0000000000..225a600655
--- /dev/null
+++ b/terraform/aws/aws-ce-grafana-backend-iam.tf
@@ -0,0 +1,48 @@
+resource "aws_iam_role" "aws_ce_grafana_backend_iam_role" {
+ count = var.enable_aws_ce_grafana_backend_iam ? 1 : 0
+
+ name = "aws_ce_grafana_backend_iam_role"
+ tags = var.tags
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Effect = "Allow",
+ Action = "sts:AssumeRoleWithWebIdentity",
+ Principal = {
+ Federated = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, "https://", "")}"
+ },
+
+ # FIXME: Below we have a string including ce-test:ce-test, it should be support:
+
+ Condition = {
+ StringEquals = {
+ "${replace(data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, "https://", "")}:sub" = "system:serviceaccount:ce-test:ce-test"
+ }
+ },
+ }]
+ })
+
+ inline_policy {
+ name = "aws_ce_grafana_backend_iam_policy"
+
+ # ref: https://docs.aws.amazon.com/service-authorization/latest/reference/list_awscostexplorerservice.html
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Effect = "Allow",
+ Action = [
+ "ce:Get*",
+ "ce:List*",
+ ],
+ Resource = "*",
+ },
+ ]
+ })
+ }
+}
+
+output "aws_ce_grafana_backend_k8s_sa_annotation" {
+ value = var.enable_aws_ce_grafana_backend_iam ? "eks.amazonaws.com/role-arn: ${aws_iam_role.aws_ce_grafana_backend_iam_role[0].arn}" : null
+}
diff --git a/terraform/aws/projects/openscapes.tfvars b/terraform/aws/projects/openscapes.tfvars
index 16bba945ea..a1470a00ef 100644
--- a/terraform/aws/projects/openscapes.tfvars
+++ b/terraform/aws/projects/openscapes.tfvars
@@ -8,9 +8,10 @@ default_budget_alert = {
"enabled" : false,
}
-enable_grafana_athena_iam = true
-athena_write_storage_bucket = "openscapes-cost-usage-report"
-athena_read_storage_bucket = "openscapes-2i2c-cur"
+enable_grafana_athena_iam = true
+enable_aws_ce_grafana_backend_iam = true
+athena_write_storage_bucket = "openscapes-cost-usage-report"
+athena_read_storage_bucket = "openscapes-2i2c-cur"
# Remove this variable to tag all our resources with {"ManagedBy": "2i2c"}
diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf
index 568d62f9e6..226d58a55c 100644
--- a/terraform/aws/variables.tf
+++ b/terraform/aws/variables.tf
@@ -273,4 +273,12 @@ variable "enable_grafana_athena_iam" {
Create an IAM role with attached policy to permit a connection between a
Grafana instance and AWS Athena service.
EOT
-}
\ No newline at end of file
+}
+
+variable "enable_aws_ce_grafana_backend_iam" {
+ type = bool
+ default = false
+ description = <<-EOT
+ Create an IAM role with attached policy to permit read use of AWS Cost Explorer API.
+ EOT
+}