AWS Setup
Install the Cost Operator (trezr) on EKS with an IRSA role, and set up the CUR 2.0 export that gives each node its billed cost.
AWS Setup
On EKS, trezr reads AWS through an IAM role bound to its ServiceAccount. This is IRSA (IAM Roles for Service Accounts). No AWS keys are stored in the cluster.
Prerequisites
- An EKS cluster on Kubernetes 1.31 or later
- The Read Operator installed
aws,eksctl,kubectl, andhelm- IAM rights to create a policy and a role in the cluster's account
Option 1: Install with the CLI
The CLI creates the IAM policy, the IRSA role, and the trust policy, then installs trezr:
dz install costTo use a role you already manage:
dz install cost --irsa-role-arn arn:aws:iam::123456789012:role/TrezrRoleThe CLI does not create the CUR 2.0 export. To get per-node billed cost, also run steps 5 and 6 below.
Option 2: Install by hand
Associate an IAM OIDC provider
Skip this if the cluster already has one.
eksctl utils associate-iam-oidc-provider \
--cluster <your-cluster> \
--region <your-region> \
--approveCreate the IAM policy
This is the least-privilege policy. The CLI and dz-installer create the same one. If your CUR export bucket is not named devzero-trezr-cur-*, change the two S3 resources.
cat > /tmp/trezr-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeReservedInstances",
"savingsplans:DescribeSavingsPlans",
"savingsplans:DescribeSavingsPlanRates",
"ce:GetReservationUtilization",
"ce:GetSavingsPlansUtilization",
"ce:GetCostAndUsage"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["bcm-data-exports:ListExports", "bcm-data-exports:GetExport"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSubnets",
"ec2:DescribeRouteTables",
"ec2:DescribeNatGateways",
"ec2:DescribeVpcEndpoints"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::devzero-trezr-cur-*"
},
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::devzero-trezr-cur-*/*"
}
]
}
EOF
aws iam create-policy \
--policy-name trezr-cost-collector \
--policy-document file:///tmp/trezr-policy.jsonWhat each group is for:
| Actions | Used for | Without them |
|---|---|---|
ec2:DescribeReservedInstances | Listing Reserved Instances | No Reserved Instances. Access tier is Minimal. |
savingsplans:* | Listing Savings Plans and their rates | No Savings Plans. Access tier is Standard. |
ce:* | Utilization and spend from Cost Explorer | No utilization or spend figures |
bcm-data-exports:* | Listing CUR exports | The export list in the UI is empty |
s3:ListBucket, s3:GetObject | Reading the CUR export | No per-node billed cost |
ec2:Describe* (network) | A snapshot of the cluster's VPCs | NAT gateway charges are not collected, and traffic to private addresses stays unpriced |
Each network Describe call fails on its own. A role without them loses only the VPC snapshot, never node billing.
Create the IRSA ServiceAccount
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
eksctl create iamserviceaccount \
--name trezr --namespace devzero-system \
--cluster <your-cluster> \
--region <your-region> \
--attach-policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/trezr-cost-collector \
--override-existing-serviceaccounts \
--approveeksctl creates the ServiceAccount itself. Helm refuses to take over an object it did not create, so add Helm's ownership labels:
kubectl label sa trezr -n devzero-system \
app.kubernetes.io/managed-by=Helm --overwrite
kubectl annotate sa trezr -n devzero-system \
meta.helm.sh/release-name=trezr \
meta.helm.sh/release-namespace=devzero-system --overwriteIf you already manage a role with these actions, skip this step. Pass the role to Helm instead with --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=<role-arn>.
Install trezr
helm upgrade --install trezr oci://registry-1.docker.io/devzeroinc/trezr \
--version 0.0.13 \
--namespace devzero-system --create-namespace \
--set trezr.clusterName=<your-cluster> \
--set trezr.region=<your-region> \
--set trezr.cloudProvider=awstrezr reads the cluster token from the Read Operator, so no token flag is needed. Check that the role annotation survived the install:
kubectl get sa trezr -n devzero-system \
-o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}'Set Up the CUR 2.0 Export
These two steps are optional. They give each node its billed cost, after Reserved Instances, Savings Plans, and private pricing. Run them once per AWS account, not once per cluster.
Create the export bucket
The Data Exports API only runs in us-east-1, whatever your cluster's region. The bucket policy lets AWS Data Exports write files, and read the policy to check it can.
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
CUR_BUCKET=devzero-trezr-cur-${ACCOUNT_ID}
aws s3api create-bucket --bucket ${CUR_BUCKET} --region us-east-1
aws s3api put-public-access-block --bucket ${CUR_BUCKET} \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
cat > /tmp/trezr-cur-bucket-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnableAWSDataExportsToWriteToS3AndCheckPolicy",
"Effect": "Allow",
"Principal": { "Service": "bcm-data-exports.amazonaws.com" },
"Action": ["s3:PutObject", "s3:GetBucketPolicy"],
"Resource": [
"arn:aws:s3:::${CUR_BUCKET}",
"arn:aws:s3:::${CUR_BUCKET}/*"
],
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:bcm-data-exports:us-east-1:${ACCOUNT_ID}:export/*"
},
"StringEquals": { "aws:SourceAccount": "${ACCOUNT_ID}" }
}
}
]
}
EOF
aws s3api put-bucket-policy --bucket ${CUR_BUCKET} \
--policy file:///tmp/trezr-cur-bucket-policy.jsonCreate the hourly CUR 2.0 export
This export holds only EC2 line items and the 23 columns trezr reads. NAT gateway charges are EC2 line items, so they are included. VPC endpoint charges are billed under AmazonVPC, so this export leaves them out.
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
CUR_BUCKET=devzero-trezr-cur-${ACCOUNT_ID}
cat > /tmp/trezr-cur-export.json <<EOF
{
"Export": {
"Name": "trezr-node-billing",
"DataQuery": {
"QueryStatement": "SELECT bill_payer_account_id, line_item_line_item_type, line_item_net_unblended_cost, line_item_operation, line_item_product_code, line_item_resource_id, line_item_unblended_cost, line_item_usage_account_id, line_item_usage_amount, line_item_usage_end_date, line_item_usage_start_date, line_item_usage_type, product_instance_type, product_region_code, pricing_public_on_demand_cost, pricing_public_on_demand_rate, reservation_effective_cost, reservation_net_effective_cost, reservation_reservation_a_r_n, savings_plan_net_savings_plan_effective_cost, savings_plan_offering_type, savings_plan_savings_plan_a_r_n, savings_plan_savings_plan_effective_cost FROM COST_AND_USAGE_REPORT WHERE line_item_product_code = 'AmazonEC2'",
"TableConfigurations": {
"COST_AND_USAGE_REPORT": {
"BILLING_VIEW_ARN": "arn:aws:billing::${ACCOUNT_ID}:billingview/primary",
"INCLUDE_RESOURCES": "TRUE",
"TIME_GRANULARITY": "HOURLY",
"INCLUDE_CAPACITY_RESERVATION_DATA": "FALSE",
"INCLUDE_IAM_PRINCIPAL_DATA": "FALSE",
"INCLUDE_MANUAL_DISCOUNT_COMPATIBILITY": "FALSE",
"INCLUDE_SPLIT_COST_ALLOCATION_DATA": "FALSE"
}
}
},
"DestinationConfigurations": {
"S3Destination": {
"S3Bucket": "${CUR_BUCKET}",
"S3BucketOwner": "${ACCOUNT_ID}",
"S3Prefix": "trezr-data",
"S3Region": "us-east-1",
"S3OutputConfigurations": {
"OutputType": "ATHENA",
"Format": "PARQUET",
"Compression": "PARQUET",
"Overwrite": "OVERWRITE_REPORT"
}
}
},
"RefreshCadence": { "Frequency": "SYNCHRONOUS" }
}
}
EOF
aws bcm-data-exports create-export --region us-east-1 \
--cli-input-json file:///tmp/trezr-cur-export.jsonAWS delivers the first files up to about 24 hours later.
Pick the export in DevZero
Open the cluster in DevZero, go to Cost & Resources → Commitments, and choose the export under Billing data (AWS CUR 2.0). See Billing Exports.
The export bucket must be in the same AWS account as the cluster. trezr reads it with its own IRSA role and does not assume a role in another account.
Next Steps
- Billing Exports -- what trezr reads from the export, and when
- Configuration -- intervals, org-wide commitments, and other Helm values
- Troubleshooting -- empty export list, Minimal access tier
Overview
The Cost Operator (trezr) reads your cloud bill, commitments, and discounts so DevZero shows what your nodes actually cost, not a list-price estimate.
GCP Setup
Install the Cost Operator (trezr) on GKE with Workload Identity, and connect the BigQuery billing export that gives each node its billed cost.