Skip to content
Edouard Topin's Blog
Cloud native FinOps / Series 03/03

FinOps cost models: what AWS, Azure and GCP bill — and what VCF calculates

An EKS cluster-hour, an AKS tier, a GKE Pod request and depreciated VCF hardware are not four values of one variable. What each platform bills, and what VCF calculates instead.

Edouard Topin
16 min read
Editorial card: the word finops on a flat ground, a cloud icon and a TCO watermark, under the label finops for cloud native.

Finance asks it in one sentence: “would this cost us less at a hyperscaler?” You have what you need to answer — a per-namespace allocation on vks-fret-01, and a known gap between reserved and consumed capacity. Except that the question compares a price to a calculation, and the two-column table everyone expects from you is wrong before it is filled in.

This article establishes what each platform actually bills — Amazon EKS, Azure AKS, Google GKE — then what VCF Operations calculates instead. It states the four assumptions without which no comparison exists, publishes the formula with its variables, and shows which one decides the outcome. It publishes no monetary amount, and that is a choice, not a shortfall.

Non-commensurabilityFour assumptionsNo amount published

TL;DR

  • The decision: do not produce a comparison figure, produce an interval with its assumption sheet. Three of the four platforms publish a price without a method; the fourth publishes a method without a price. These are not two levels of transparency, they are two natures of figure.
  • The trade-off that costs you: establishing each platform’s billed unit takes half a day of documentation reading, and the result forbids you the four-column table everyone is waiting for.
  • Monday morning: read the configured value of Expected CPU Utilization in VCF Operations. That parameter sits in the denominator of the vendor’s published base rates — it decides the private unit cost, and chances are nobody knows which value is wired in there.

This article’s figure contract

This section is for you, not for the writer: a cost article reads differently depending on what it lets itself quantify. It publishes billing structures quoted verbatim, published durations, dates and SLAs, Broadcom’s formulas as they stand, formulas posed by the article and labelled as reconstructions with their variables visible, API field names and FOCUS column names.

It refuses any amount in any currency, any savings percentage — including those published by AWS and Microsoft on their own pages — any illustrative amount even when announced as fictional, any column aligning amounts from different platforms, and any conclusion naming one platform as the cheapest.

That refusal is counter-intuitive, so it needs justifying. On 2026-08-16, the public Amazon EKS pricing page did render its amounts — but it names no region, shows no effective date and carries no caveat about regional variation. An amount without a region and a date is not a price, it is a memory. So the rule is not “the page does not load”, it is “the page is not enough”.

If your case needs an amount, collect it through an API and publish it in this shape: <amount> <currency> / <unit> — <region>, public on-demand rate collected on <YYYY-MM-DD> via <named API>, excluding contractual discounts. An amount missing any of the four is removed, not corrected. Finally, to be straight about this text’s limits: no lab was run, no bill was read.

What each platform bills

Establish the billed object first, and only then ask whether it is comparable to another.

Amazon EKS — a cluster-hour indexed on the calendar

The documentation publishes the structure in one sentence: “Amazon EKS has per cluster pricing based on Kubernetes cluster version support, pricing for Amazon EKS Auto Mode, and per vCPU pricing for Amazon EKS Hybrid Nodes.” Then the one that matters for a model: “When using Amazon EKS, you pay separately for the AWS resources you use to run your applications on Kubernetes worker nodes.” The published example names three services for a single workload line — EC2, EBS, VPC IPv4 address. Careful, that structure sentence is not exhaustive: the link list on the same page and the pricing page name a fourth axis, Amazon EKS Capabilities.

The most interesting point about the EKS model is structural, not monetary: the cluster rate depends on the age of its Kubernetes version. Standard support for 14 months after release, then extended support for 12 months “at an additional cost per cluster hour”, on by default for new and existing clusters alike. The switch has a date published in advance, with its timezone: “Billing for extended support starts at the beginning of the day that the version reaches end of standard support, in the UTC+0 timezone.” In other words, a billing line changes because time passes, without anyone deploying anything.

Version Upstream release EKS release End of EKS standard support End of EKS extended support
1.36 2026-04-22 2026-06-02 2027-08-02 2028-08-02
1.35 2025-12-17 2026-01-27 2027-03-27 2028-03-27
1.34 2025-08-27 2025-10-02 2026-12-02 2027-12-02

Note the gap: kubernetes.io puts the end of life of 1.35 at 2027-02-28, EKS the end of standard support at 2027-03-27. A managed provider’s lifecycle is not the upstream lifecycle — and that is an assumption item in its own right.

Azure AKS — a management tier, not a resource

AKS bills three cluster management tiers, and the free tier’s description says exactly what is free: “Free cluster management. Pay as you go for consumed resources.”

Tier What is published Nodes SLA
Free “Includes all current AKS features. […] No financially backed uptime SLA.” ≤ 1,000 best-effort
Standard “Uptime SLA enabled by default. Higher reliability profile.” ≤ 5,000 99.95% with zones, 99.9% without
Premium Microsoft maintenance beyond community support, 24-month long-term support ≤ 5,000 99.95% with zones, 99.9% without

So what an AKS tier buys is not compute: it is an availability commitment and a support duration. Consumed resources are still billed separately in all three tiers.

Google GKE — two billing models inside the same cluster

Pod-based billing covers “Pods that run on the container-optimized compute platform in Autopilot clusters or Standard clusters” as well as “Pods that select the Balanced or Scale-Out built-in ComputeClasses in Autopilot clusters”. Node-based billing covers the rest: “Pods that select specific hardware, such as Compute Engine machine series or hardware accelerators, use a node-based billing model. In this model, you pay for the underlying hardware and a node management premium.”

Heavy consequence: two Pods in the same cluster can fall under two different billed units, depending on what their spec asks for. The cost unit is a property of the Pod, not of the cluster — a GKE model reasoning “per node” is wrong for part of the workloads. An acknowledged gap: the GKE pricing page came back truncated on loading, twice. Neither the node management premium nor an Autopilot / Standard comparison could be established; this article builds neither.

VCF — it does not bill, it calculates

Ten cost drivers are published for the vCenter infrastructure type: Server Hardware: Traditional, Server Hardware: Hyper-Converged, Storage, License, Applications, Maintenance, Labor, Network, Facilities, Additional Cost. The mechanism fits in one sentence: “The total cost set by you is distributed across resources in the data center.” Three of those drivers have no equivalent inside a public instance priceLabor, Facilities, Network. They come back as the fourth assumption.

The VCF calculation chain, stage by stage

Stage 1 — the input is an amount you enter, the total of the ten cost drivers, then distributed across data centre objects.

Stage 2 — hardware depreciates, with a published formula: “Yearly straight line depreciation = [(original cost - accumulated depreciation) / number of remaining depreciation years]”. Two methods are offered, Straight Line and Max of Double or Straight, “you can set the depreciation period from two to five years”, and “The default discount % value is zero.”

Stage 3 — the base rate, with the utilization rate in the denominator: “Per GHz CPU base rate = (Cluster Total CPU Cost) / (Expected CPU Utilization * Cluster CPU Capacity in gHZ)”, and its memory equivalent. The origin of the expected percentages is published — “These percentages are arrived based on historical actual use of clusters” — as is their period: “The base rates are monthly rates.”

Stage 4 — aggregation up to Kubernetes: “VCF Operations calculates the cost for VKS cluster, supervisor cluster, and Kubernetes node, by aggregating the cost of the supporting VMs.” So the Kubernetes cost in VCF Operations is an infrastructure cost pushed up, not a consumption cost pushed down: the exact opposite of OpenCost’s path, described in OpenCost: seeing before acting on Kubernetes spend. Both produce a figure per Kubernetes object; they do not build it the same way round.

The 9.1 release notes add costing “across your entire Kubernetes ecosystem, including nodes, clusters, vSphere namespaces, projects, and organizations” and chargeback “specifically for VMware Kubernetes Service (VKS) nodes in addition to the traditional VM pricing”.

The legitimate table: properties, not amounts

The columns are platforms, but the rows are properties of the model. That is what makes the table defensible — and why it has no price row.

Amazon EKS Azure AKS Google GKE VCF / VCF Operations
Billed object the cluster, per hour the management tier the Pod or the node, depending on the Pod not billed — cost entered, then distributed
What triggers the amount the cluster existing + the age of its version the tier you choose the Pod spec writing the cost drivers
What is included managed control plane; control-plane-side traffic absorbed full features in all 3 tiers; SLA from Standard up hardware + management premium in the node model nothing — everything is a driver
Billed separately EC2, EBS, VPC IPv4, customer-side traffic, Auto Mode, Hybrid Nodes, Capabilities every consumed resource depends on the model applying to the Pod not applicable
Who sets the amount the provider the provider the provider the operator
What it depends on region, version, duration tier, region applicable model, hardware depreciation, expected utilization, cost ratio
Published granularity billed resource billed resource Pod (Pod model) node / vSphere namespace
Published method no — a rate, not a formula no no yes — base rates and aggregation

The last row inverts the intuition. Hyperscalers publish a price without a method; VCF publishes a method without a price. A price needs no method: it is enforceable, it is what will be invoiced. A method without a price is not — it only becomes a figure once you have poured your own amounts into it. Hence the non-commensurability in one sentence: an EKS figure is the outcome of a contract, a VCF figure the outcome of a calculation whose inputs the operator supplies. Putting them side by side without saying so is comparing a bank statement to a budget.

The one honest bridge: commitment against depreciation

All three hyperscalers publish a commitment mechanism. AWS: “Savings Plans provide savings beyond On-Demand rates in exchange for a commitment of using a specified amount of compute power (measured per hour) for a one or three year period”, with the rate locked for the term. Azure applies the discount automatically by SKU, region and scope. GCP commits on a minimum hourly spend or on a quantity of eligible resources, and publishes the clause that defines the risk: “Any overage usage that takes your hourly spend amount over your committed amount is charged at the on-demand rate.”

A multi-year commitment is not a discount: it is a transfer of risk in exchange for a lower rate, exactly the structure of buying hardware depreciated over two to five years. So the only naturally commensurable comparison is not “on-demand against private”, it is commitment against depreciation — with the same penalty for forecasting badly: committed capacity you never consume on one side, an occupancy rate that never rises on the other.

The four assumptions, the formula, the sensitivity

These assumptions are not an academic frame bolted onto a product: two of them are published parameters of VCF Operations, so they are arguable on evidence rather than on opinion.

# assumption sheet — ⚠️ this block is NOT a Kubernetes manifest: no apiVersion, no kind.
# It travels with every figure this model produces. A figure without its sheet is removed, not corrected.
assumptions:
  - id: H1
    name: "hardware depreciation period"
    variable: REPLACE_WITH_DEPRECIATION_YEARS
    published_bound: "two to five years — Cost Settings for Financial Accounting Model"
    published_method: "Straight Line | Max of Double or Straight"
    applies_to: "the hardware share only; license, maintenance, labor, facilities stay recurring"
  - id: H2
    name: "expected utilization rate of the private platform"
    variable: REPLACE_WITH_UTILIZATION_RATE
    published_status: "Expected CPU/Memory Utilization sits in the DENOMINATOR of the published base rates"
    note: "private unit cost varies as 1/H2; the public rate does not vary with H2"
  - id: H3
    name: "lifetime and workload profile"
    variable: REPLACE_WITH_WORKLOAD_PROFILE
    values: [permanent, elastic, seasonal, bounded_project]
    note: "permanent → commitment vs depreciation; elastic → on-demand rate vs capacity already bought"
  - id: H4
    name: "cost scope retained on each side"
    variable: REPLACE_WITH_SCOPE
    private_list: "the ten cost drivers, including Labor, Facilities and Network"
    public_list: "what a reservation does not cover; what EKS bills separately"
    rule: "an item present on one side and absent on the other is added to both sides, or removed from both"

The formula below is a reconstruction: it chains official pages together, it is published nowhere in this form, and none of its variants produces a publishable result. It produces an interval, with its sheet attached.

# public side — unit cost of a vCPU-hour on a managed cluster
PUBLIC_UNIT_COST_VCPU_HOUR =
    (   REPLACE_WITH_NODE_HOURLY_RATE
      + REPLACE_WITH_CLUSTER_HOURLY_FEE / REPLACE_WITH_NODES_PER_CLUSTER
      + REPLACE_WITH_ATTACHED_STORAGE_HOURLY
      + REPLACE_WITH_DATA_TRANSFER_HOURLY
      + REPLACE_WITH_MANAGED_ADDONS_HOURLY )
    / REPLACE_WITH_NODE_VCPU_COUNT
# every term ! come from a dated lookup, on REPLACE_WITH_REGION, at REPLACE_WITH_LOOKUP_DATE
# REPLACE_WITH_CLUSTER_HOURLY_FEE depends on the cluster version (standard or extended support)
#   ∴ this term moves with the calendar, not with the load

# private side — the chain published by VCF Operations, rewritten in variables
CLUSTER_COMPUTE_COST = TOTAL_INFRA_COST - STORAGE_COST - DIRECT_VM_COST
YEARLY_STRAIGHT_LINE_DEPRECIATION =
    ( REPLACE_WITH_HARDWARE_CAPEX - ACCUMULATED_DEPRECIATION ) / REMAINING_YEARS
    # REMAINING_YEARS bounded by REPLACE_WITH_DEPRECIATION_YEARS ∈ [2..5]
CPU_BASE_RATE_PER_GHZ = CLUSTER_CPU_COST / ( REPLACE_WITH_UTILIZATION_RATE * CLUSTER_CPU_CAPACITY_GHZ )
RAM_BASE_RATE_PER_GB  = CLUSTER_RAM_COST / ( REPLACE_WITH_UTILIZATION_RATE_MEM * CLUSTER_RAM_CAPACITY_GB )
VKS_NODE_COST    = SUM( cost of the VMs supporting the node )
VKS_CLUSTER_COST = SUM( VKS_NODE_COST )
# ⚠️ the published chain stops here: no Pod, no container

# the break-even point is a RATE, not a saving
BREAK_EVEN_UTILIZATION_RATE =
    PRIVATE_UNIT_COST_AT_100_UTILIZATION / PUBLIC_UNIT_COST_VCPU_HOUR

Without a single amount, these formulas give a direction of variation, and it is decisive. The private curve against utilization is a hyperbola: unit cost varies as 1/H2. The public line is horizontal — an instance rate does not change because your cluster is half empty. So an under-occupied private cluster is not “a bit more expensive”: it gets more expensive the emptier it is, with no floor. That is what rightsizing with VPA and KRR makes measurable: capacity reserved and never consumed degrades H2.

Sensitivity to H1 is decreasing but floored: H1 only covers the hardware share, and license, maintenance, labor and facilities are recurring. A model that only becomes favourable to private by stretching depreciation postpones a decision, it does not justify one. Neither curve is a measurement: they are the shapes imposed by the published formulas. You can know a model’s direction of variation without knowing a single one of its amounts.

Collecting it yourself: the three APIs and the FOCUS vocabulary

Two values are fixed first and never move again: REPLACE_WITH_REGION and REPLACE_WITH_LOOKUP_DATE. A comparison carries a region and a date, the way a bill carries a month.

# AWS — Price List Query API, GetProducts action; SigV4-signed request ∴ AWS account required
aws pricing get-products \
  --service-code AmazonEC2 --format-version aws_v1 --max-results 100 \
  --filters \
      Type=TERM_MATCH,Field=instanceType,Value=REPLACE_WITH_INSTANCE_TYPE \
      Type=TERM_MATCH,Field=operatingSystem,Value=REPLACE_WITH_OS \
      Type=TERM_MATCH,Field=tenancy,Value=Shared
# keep: terms.OnDemand[].priceDimensions[].pricePerUnit, unit, description,
#   effectiveDate, publicationDate, and the product attribute location carrying the region in clear
# ⚠️ no regional filter field name is published: read the region from the attributes,
#   ⊥ invent a plausible field name

# Azure — Retail Prices API, unauthenticated
curl -s -G "https://prices.azure.com/api/retail/prices" \
  --data-urlencode "api-version=2023-01-01-preview" \
  --data-urlencode "\$filter=serviceName eq 'Virtual Machines' \
      and armRegionName eq 'REPLACE_WITH_REGION' \
      and armSkuName eq 'REPLACE_WITH_INSTANCE_TYPE' \
      and priceType eq 'Consumption'"
# case-sensitive since 2023-01-01-preview; 1,000 records max per page + NextPageLink;
#   commitment rates are only returned on this preview version

# GCP — Cloud Billing Catalog API, services.skus.list
curl -s "https://cloudbilling.googleapis.com/v1/services/REPLACE_WITH_SERVICE_ID/skus?currencyCode=REPLACE_WITH_CURRENCY" \
  -H "Authorization: Bearer REPLACE_WITH_ACCESS_TOKEN"
# filter client-side on serviceRegions[] == REPLACE_WITH_REGION
# rate = tieredRates[].unitPrice.units + tieredRates[].unitPrice.nanos / 1e9
# startTime/endTime must sit within a single calendar month

Microsoft publishes a currency caveat worth relaying: “The currency that Microsoft uses to price all Azure services is USD. […] Other non-USD prices returned by the API are for your reference to help you estimate budget expenses.” An amount returned by that API in another currency is a conversion estimate, not a rate.

What remains is saying which cost you are talking about, and FOCUS gives the exact words. Specification 1.4, ratified on 2026-06-04, publishes: “BilledCost represents the cash-based view […]. EffectiveCost represents the accrual-based view: costs recognized when resources are consumed, services are used, or contract commitments are recognized. ListCost provides the pre-discount baseline. ContractedCost reflects pricing after negotiated discounts.”

Figure FOCUS column Where you meet it
what OpenCost shows by default — “the on-demand list pricing” ListCost article 1
what reconciliation with the negotiated bill produces ContractedCost, then EffectiveCost article 1, commercial tier
what you see on your invoice BilledCost this article
what a commitment changes CommitmentDiscount* columns this article

Two honest clarifications on FOCUS and VCF. The fact first: a post on Broadcom’s VCF blog, dated 2026-06-03, announces “we are aligning VCF 9.1 with the FinOps Open Cost & Usage Specification (FOCUS)” — but no product documentation page that was loaded confirms it, and no FOCUS version is named there, while the FOCUS site’s vendor adoption page returned no list. So one writes neither “VCF is FOCUS-compliant” nor “VCF produces no FOCUS dataset”: one writes the dated announcement and the absence of confirmation.

The deeper limit next, which will survive any product update: FOCUS normalizes the schema, not the nature of the figure. A perfectly conformant FOCUS export of a VCF cost would carry a BilledCost column — but nobody invoiced that amount: it was entered as cost drivers, then distributed. The columns would be identical, the meaning would not.

Pitfalls

  • The example amount inside API documentation. Response examples contain real amounts, stale by construction: GetProducts carries an effectiveDate from 2017, the Azure examples effectiveStartDate values from 2019 to 2021.
  • The vendor’s savings percentage. AWS and Microsoft each publish a maximum savings figure on their commitment pages. Those figures compare a commitment rate to the on-demand rate of the same provider, on a permanently running workload: that is not a gain against your current state. A gain is told as a mechanism — a node removed, capacity not bought — never as a percentage.
  • Comparing a discounted rate on one side with a public rate on the other. A public ListCost against a private cost that contains real hardware and license discounts mostly measures your negotiating position. Compare the same FOCUS column on both sides, and say which one.
  • The asymmetric scope. VCF exposes Labor, Facilities and Network; an Azure reservation “doesn’t cover additional software, Windows, networking, or storage charges”. The ten drivers on one side and an instance rate on the other is a complete cost against a partial one — as wrong as its mirror image.
  • Attributing to VCF a granularity it does not publish. The chain stops at the node and the vSphere namespace, and the published method is an aggregation of supporting VMs, not an allocation by consumption. Symmetrically, do not write “VCF sees nothing on the Kubernetes side”: that has been false since 9.1.
  • The wired-in utilization rate nobody chose. The expected percentages “are arrived based on historical actual use of clusters”, but the documentation does not say who sets them, over which window, or whether they can be changed. Read the configured value; do not infer a window.
  • The model that stays correct and goes stale. A rate changes without notice, a version tips into extended support, a depreciation period ends — the model signals nothing, it keeps calculating. Hence the expiry date written into the sheet.

Conclusion

The question as asked has no single answer, and saying so is a result, not an admission of failure. What you hand back to finance is not two columns: it is three questions in return — over what period do we depreciate, at what utilization rate do we run, and did we put the same items on both sides? An organisation that can answer those three knows how to build its own figure, and how to defend it.

The billed unit comes first

A cluster-hour, a management tier, a Pod request and depreciated hardware are not four values of one variable.

H2 decides the outcome

Private unit cost varies as 1/utilization; the public rate does not move as your cluster empties. The vendor says so before we do.

The deliverable is a pair

A figure and its sheet — region, date, API, four assumptions, scope, expiry. Separated from its sheet, a figure is withdrawn.

The loop closes back on the start of the series for a mechanical reason: the utilization rate that decides the comparison is exactly the quantity that per-namespace allocation makes readable and that rightsizing moves. Multi-cloud comparison is not the step after measurement — measurement is what makes it possible.

Get the next one by email

New articles and series, sent when they are published. No other mail.

One click to unsubscribe, any time.

Back to blog
Share

Related articles

  1. 16 min read

    OpenCost: seeing before acting on Kubernetes spend

    OpenCost makes cluster spend readable per namespace. We look at its allocation model, what its default pricing really is, and where the open source ends.

  2. 19 min read

    Rightsizing Kubernetes workloads with VPA and KRR

    VPA recommends and applies, KRR recommends and explains. VPA's six update modes, and what each one actually does to a Pod now that in-place resize is stable.

  3. 23 min read

    Network policies and Cilium: building a defensible default-deny

    The NetworkPolicy API ships with Kubernetes; enforcing it is the CNI's job. What Cilium adds, what stays standard, and how to reach default-deny by watching real flows before blocking any.

Follow along

New articles, thoughts, and updates.