Cloud
Building a Modernization Index from CUR Data: A CUDOS / Athena / QuickSight Guide
Every leadership team eventually asks how modern is our AWS estate? Here is how to answer it with one honest number, computed from Cost and Usage Report data, and how to avoid turning it into a vanity metric.
· 11 MIN READ
Sooner or later, someone in leadership asks the question: how modern is our AWS estate?
It is a fair question and a slippery one. Everyone has an intuition. Nobody has a number. The infrastructure team points at the shiny serverless workload they shipped last quarter. Finance points at the EC2 bill that refuses to shrink. Both are right, and neither answer settles the argument.
A Modernization Index is an attempt to settle it with a single percentage, computed from data you already have: your Cost and Usage Report. Not a survey. Not a maturity workshop with sticky notes. A number that falls out of your actual spend, refreshes daily, and drills down from the whole estate to a single workload.
This post is how I build one. It is opinionated about what counts as modern, honest about where the metric misleads, and specific enough that you can stand up a working dashboard from it.
The Concept: One Number, Computed From Spend
The Modernization Index answers one question: what share of your applicable AWS spend runs on modern services versus traditional lift-and-shift infrastructure?
You compute it from CUR data, grouped by service, per payer account. The simplest form is a spend ratio:
Index = Modern Spend / Total Applicable Spend x 100
If 40 percent of your applicable spend runs on Lambda, containers, managed databases, analytics, and AI/ML services, your index is 40. Run it monthly and you have a trend line.
The appeal is that it is objective. Spend does not lie about what you are actually running. A team can claim it modernized; the bill shows whether it did.
The Problem: Spend Does Not Equal Maturity
The pure spend ratio has a flaw, and it is worth being honest about it up front.
Spend is a proxy for adoption, not for maturity. A team can adopt a genuinely modern service, wire it into production, and it barely moves the index because the service is cheap. Lambda at scale can cost less than the EC2 instances it replaced. DynamoDB for a high-traffic table can cost a fraction of the self-managed database it succeeded. You modernized, you cut cost, and the index went down because your denominator shifted.
That is a perverse incentive. If the number punishes you for adopting cheap modern services, the number is wrong.
The fix is to blend spend share with breadth of adoption:
Index = [0.7 x (Modern Spend / Total Spend) + 0.3 x (Modern Services Used / N)] x 100
The 0.7 weight keeps spend as the primary signal, because spend still reflects where the workload weight actually sits. The 0.3 weight rewards breadth: how many distinct modern services you use, divided by N, the number available in the reference taxonomy. Adopt a new modern service and the breadth term rises even if the service is nearly free.
There is a third term worth adding once you have history:
x [1 + alpha x growth]
A growth multiplier that rewards month-over-month improvement. It needs at least two billing periods to mean anything, so you add it later. Its real value is framing: it turns the index from a target you hit or miss into a trend you move. More on that at the end, because that framing is the whole game.
The N denominator deserves a decision. You can derive it from your estate (distinct modern services actually in use) or fix it against a reference list of around forty services. Fixing it makes comparisons across payers and across time stable. Deriving it makes the number flatter to your own reality. I lean toward the fixed reference list and document it as an explicit assumption, because a metric whose denominator drifts is a metric nobody trusts.
What Counts as Modern
This is where the arguments happen, so define it once and write it down.
I use six modernization pathways as the taxonomy for what lands in the numerator:
- Cloud Native (serverless, event-driven: Lambda, Step Functions, EventBridge)
- Container services (ECS, EKS, and yes, Red Hat OpenShift on AWS)
- Managed Data services (RDS, Aurora, DynamoDB, ElastiCache)
- AI/ML (SageMaker, Bedrock)
- Managed Analytics (Athena, Glue, QuickSight, OpenSearch, MSK)
- Modern DevOps (managed CI/CD and delivery tooling)
Three classification calls are worth defending because people push back on them.
RDS counts as modern. This is contentious. A lifted-and-shifted Oracle instance on RDS is not exactly cloud-native nirvana. But RDS is a managed database, the customer no longer patches the engine or babysits backups, and the pathway taxonomy is about the operating model, not architectural purity. It counts. If you want to be stricter, split RDS into its own reported tier and let the reader judge.
ROSA counts as modern. Red Hat OpenShift on AWS is a managed container platform. Whatever you think of OpenShift, it is not someone SSHing into EC2 to run Docker by hand.
Security tooling does not count. GuardDuty, Security Hub, WAF, Macie, Inspector, Network Firewall, Firewall Manager: these are baseline hygiene, not modernization evidence. Running GuardDuty does not make your estate modern any more than locking your front door makes your house new. Report security spend separately so it is visible, but keep it out of the numerator.
Then there is what you exclude from the denominator entirely, because including it distorts the ratio:
- Tax, Support charges
- AWS Marketplace purchases (third-party software billed through AWS)
- Savings Plans and Reserved Instance fees, discounts, credits, refunds
- Data Transfer
These are not workloads. They are financial artifacts or third-party line items, and leaving them in the denominator makes your index look worse for reasons that have nothing to do with how you build.
Traditional, finally, is the residue: EC2, EBS, VPC, ELB, and self-managed workloads. This is the denominator weight you are trying to shrink over time.
Architecture: Why CUR and Athena, Not the Cost Explorer API
You have two ways to get at this data. The Cost Explorer API (and the MCP servers built on it) is fast to reach for and fine for a one-off question. For a production dashboard it falls short, and the reasons are worth knowing before you invest.
| Dimension | CUR to Athena | Cost Explorer API |
|---|---|---|
| Granularity | Line-item, resource-level, hourly | Daily or monthly aggregates |
| History | Unlimited (your own S3) | 13 months maximum |
| Grouping | Arbitrary SQL GROUP BY | Two dimensions per call |
| Payer x service x tag drill-down | One query | Fan-out calls, stitched client-side |
| Multi-payer | CUR replicated to one account | Scoped per payer, assume-role each |
| Tag drill-down | Tag map column in SQL | One tag key per call |
For a metric you want to slice by payer, then by service, then by workload tag, the Cost Explorer API turns into a fan-out of calls that you stitch together by hand. CUR 2.0 delivered to S3, queried through Athena, is one SQL statement.
The important framing: this is not a net-new build. If you already run the Cloud Intelligence Dashboards (CUDOS, CID, KPI), you already have the pieces: CUR replicated to a data collection account, a Glue database, an Athena workgroup, QuickSight with SPICE. The Modernization Index is an Athena view on top of the CUR table. A data-layer customization, not a new platform.
And if you later want an analyst to ask the estate questions in natural language, point an Athena-backed agent at the same views. Dashboard and agent share one data layer, so the executive report and the analyst chat query never diverge. The aws-samples/sample-finops-agent project shows this pattern with Athena MCP alongside CUDOS.
The Build: From CUR Table to Index View
Two Athena views do the work. The first classifies every line item into a tier.
CREATE OR REPLACE VIEW modernization_index_view AS
SELECT
bill_payer_account_id AS payer_id,
line_item_usage_account_id AS account_id,
line_item_product_code AS service,
resource_tags['user:application-name'] AS app_name,
billing_period,
SUM(line_item_unblended_cost) AS cost,
CASE
WHEN line_item_line_item_type IN
('Tax','RIFee','SavingsPlanRecurringFee','SavingsPlanNegation','Fee')
OR line_item_product_code IN
('AWS Support (Enterprise)','AWS Support (Business)')
OR bill_billing_entity = 'AWS Marketplace'
OR line_item_product_code IN
('Amazon GuardDuty','AWS Security Hub','AWS WAF','Amazon Macie',
'Amazon Inspector','AWS Network Firewall','AWS Firewall Manager')
THEN 'excluded'
WHEN line_item_product_code IN
('AWS Lambda','Amazon Elastic Container Service',
'Amazon Elastic Container Service for Kubernetes',
'Red Hat OpenShift Service on AWS',
'Amazon Relational Database Service','Amazon Aurora',
'Amazon DynamoDB','Amazon ElastiCache','Amazon SageMaker',
'Amazon Bedrock','Amazon Athena','AWS Glue','Amazon QuickSight',
'Amazon OpenSearch Service',
'Amazon Managed Streaming for Apache Kafka')
THEN 'modern'
ELSE 'traditional'
END AS tier
FROM cid_data_export.cur_table
WHERE line_item_line_item_type NOT IN ('Credit','Refund')
GROUP BY 1,2,3,4,5;
The second aggregates that into the index per payer per period.
CREATE OR REPLACE VIEW modernization_index_summary AS
SELECT
payer_id, billing_period,
SUM(CASE WHEN tier='modern' THEN cost ELSE 0 END) AS modern_spend,
SUM(CASE WHEN tier<>'excluded' THEN cost ELSE 0 END) AS applicable_spend,
ROUND(100.0 * SUM(CASE WHEN tier='modern' THEN cost ELSE 0 END)
/ NULLIF(SUM(CASE WHEN tier<>'excluded' THEN cost ELSE 0 END),0),2)
AS spend_index,
COUNT(DISTINCT CASE WHEN tier='modern' THEN service END)
AS modern_services_used
FROM modernization_index_view
GROUP BY payer_id, billing_period;
That modern_services_used count is what feeds the breadth term of the blended index. You compute the blend as a calculated field in QuickSight rather than baking N into SQL, so the denominator stays a visible, adjustable assumption.
From here it is standard CUDOS mechanics. Register both views as QuickSight datasets, import to SPICE, schedule a daily refresh aligned to CUR delivery. Then decide how to surface it:
- Extend the existing CUDOS analysis by adding a Modernization Index sheet. Cheapest, but managed template updates can overwrite custom sheets. Duplicate the analysis first if you go this way.
- Build a companion dashboard from the MI datasets as a separate analysis. This is what I recommend. It is isolated from CUDOS template updates and shares the same Glue, Athena, and SPICE data layer, so there is no number divergence between it and your cost dashboards.
Drill-down follows the natural hierarchy: a KPI card for the global weighted index, a bar and table for payer by index, a service-level view filtered to the modern tier, and a workload table keyed on the application-name tag with a minimum-index parameter so you can filter to the laggards.
The Tag Trap
One thing will silently break your workload drill-down: tags.
In CUR 2.0, resource tags are a map column, accessed as resource_tags['user:application-name'], not the scalar columns CUR 1.0 used. If you migrated from CUR 1.0 to 2.0, queries that referenced the old columns fail quietly or return nulls. Check this first.
Cost allocation tag activation takes 24 to 48 hours and does not backfill. If you activate application-name today, you get workload attribution from today forward, not for last quarter. Activate before you start computing, not after.
Tags are case-sensitive and drift across payers: application-name in one account, Application-Name in another. Normalize in the view or your workload rollups will fragment.
And route null tags into an explicit untagged bucket rather than dropping them. The share of untagged spend is itself a useful FinOps signal: it tells you how much of your estate you cannot attribute, which is often the more urgent finding than the index itself.
Honest Recommendation
Build the index. It is cheap if you already run CUDOS, it is objective, and it gives leadership the number they keep asking for. The single most valuable thing it produces is not the headline percentage; it is the workload drill-down that shows which applications are dragging the estate down, ranked by traditional spend. That list is your modernization backlog, sorted by impact, generated for free from data you already collect.
But be disciplined about one thing: treat it as a trend, not a target.
The moment a Modernization Index becomes a number someone is measured against, it stops measuring modernization and starts measuring the ability to game a ratio. Teams will reclassify workloads, shuffle spend, and argue about whether RDS counts, instead of actually modernizing anything. A cheap modern service that quietly went into production is real progress the index barely rewards; a service reclassified in a spreadsheet is fake progress the index rewards fully. If you point a bonus at the number, you will get the second kind.
So publish it monthly, watch the direction, and use the drill-down to pick the next workload to modernize. Frame every review around the slope of the line, not the value on any single month. Used that way, it is one of the more honest metrics in a FinOps practice. Used as a target on a scorecard, it becomes theater. The formula is the easy part. The discipline is the rest.
ABOUT THE AUTHOR
ONE LETTER A MONTH · NO TRACKER · UNSUBSCRIBE ANYTIME
CONTINUE READING
Related dispatches
Attributing Bedrock Costs per User and per Project: 4 Mechanisms That Actually Work
10 MIN READ
Cloud WAN vs Transit Gateway — 3 Things It Actually Adds, 5 Things It Does Not
6 MIN READ
AWS Transform + GenAI for VM Migration: The Assisted Data-Center Exit
9 MIN READ
Comments
Sign in to leave a comment
