NewCompare CPU & GPU pricing across AWS, Azure & GCP
Cost Operator

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, and helm
  • 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 cost

To use a role you already manage:

dz install cost --irsa-role-arn arn:aws:iam::123456789012:role/TrezrRole

The 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> \
  --approve

Create 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.json

What each group is for:

ActionsUsed forWithout them
ec2:DescribeReservedInstancesListing Reserved InstancesNo Reserved Instances. Access tier is Minimal.
savingsplans:*Listing Savings Plans and their ratesNo Savings Plans. Access tier is Standard.
ce:*Utilization and spend from Cost ExplorerNo utilization or spend figures
bcm-data-exports:*Listing CUR exportsThe export list in the UI is empty
s3:ListBucket, s3:GetObjectReading the CUR exportNo per-node billed cost
ec2:Describe* (network)A snapshot of the cluster's VPCsNAT 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 \
  --approve

eksctl 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 --overwrite

If 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=aws

trezr 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.json

Create 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.json

AWS 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

On this page