Skip to main content

AWS Cost and Usage Report to Snowflake for Margin Analysis

· 22 min read
Puneet Gupta
Founder, Supaflow

Moving the AWS Cost and Usage Report to Snowflake answers a question that Cost Explorer cannot answer alone: does the revenue from each customer justify the cloud cost required to serve them?

The practical architecture is straightforward. Export AWS Cost and Usage Report 2.0 (CUR 2.0) to Amazon S3 as Parquet, load the files into Snowflake, sync revenue data from Stripe, and add customer and segment context from Salesforce. A shared customer identity model then turns three disconnected systems into customer-level cost, revenue, and margin analysis.

AWS Data Exports -> S3 Parquet ---\
\
Stripe ------------------------------> Supaflow -> Snowflake -> dbt -> Margin dashboard
/
Salesforce ------------------------/

This guide explains the pipeline, the allocation decisions that matter, and the Snowflake model needed to calculate gross margin without hiding shared or unattributed costs.

In other words, moving AWS billing data to Snowflake is the data-engineering foundation for cloud unit economics and customer profitability analysis.

What Each Data Source Contributes

No single source contains customer profitability. Each system answers a different part of the question.

How AWS, Stripe, and Salesforce contribute to customer-level margin analysis
SourceWhat it contributesQuestions it can answer
AWS CUR 2.0Service, account, resource, usage, pricing, discounts, tags, and costWhat did the infrastructure cost? Which resources or workloads drove it?
StripeCustomers, subscriptions, invoices, prices, events, and balance transactionsWhat was billed or collected? Which plan and currency applied? What fees or refunds affected cash?
SalesforceAccounts, opportunities, products, owners, industries, and customer segmentsWho is the customer? Which segment, region, owner, or contract should receive the result?
Product usage dataRequests, transactions, tokens, storage, seats, or another consumption driverHow should shared infrastructure be distributed across customers?

AWS provides the cost. Stripe provides the commercial outcome. Salesforce provides the business context. Product telemetry supplies a defensible allocation driver when infrastructure is shared.

That last source is important. Dividing the AWS bill evenly across customers is easy, but it can make a low-usage customer appear unprofitable and a high-usage customer appear healthy. Customer-level margin is only as credible as the allocation model beneath it.

Start With the Unit, Not the Dashboard

Before building the pipeline, decide what one unit of value means for your business.

The FinOps Foundation defines unit economics as connecting technology spend with the value that spend creates. The useful unit might be:

  • one customer or tenant
  • one active user or paid seat
  • one transaction, order, shipment, or claim
  • one gigabyte processed
  • one API request, model response, or token
  • one product, feature, region, or business line

Customer margin is a good executive view, but it should not erase the operational driver. Keep both. A SaaS company might report gross margin by customer while engineering tracks cost per million requests for the same account.

Step 1: Export AWS CUR 2.0 to S3 as Parquet

AWS Data Exports creates recurring deliveries of billing and cost-management data to S3. CUR 2.0 is the best starting table for this analysis because AWS documents it as a fixed schema with cost, usage, product, discount, resource-tag, and Savings Plan data.

Prepare Allocation Tags and Container Data First

If customer_id, tenant_id, or another resource tag will drive allocation, apply the tag to the relevant AWS resources before creating the export. Then open Billing and Cost Management > Cost allocation tags, wait for the tag key to appear, and activate it. AWS says a new tag key can take up to 24 hours to appear and another 24 hours to activate. Only activated cost-allocation tag keys appear in CUR 2.0. See Activating user-defined cost allocation tags and the CUR 2.0 resource-tag dictionary.

If you need ECS task- or EKS pod-level allocation, opt in under Cost Management preferences > Split cost allocation data before enabling it on the export. AWS documents the account and AWS Organizations prerequisites in Enabling split cost allocation data.

In the AWS Billing and Cost Management console:

  1. Open Data Exports and choose Create export.
  2. Select Standard data export.
  3. Choose Cost and Usage Report 2.0 as the table.
  4. Select daily or hourly granularity based on the questions you need to answer.
  5. Enable resource IDs if you need resource-level attribution.
  6. Include split cost allocation data if you completed the ECS or EKS opt-in above.
  7. Choose Parquet as the file format.
  8. Select or create a private S3 bucket and enter a path prefix.
  9. Choose Overwrite existing data export file, then create the export.

AWS provides the current console procedure in Creating a standard export. The CUR 2.0 table dictionary lists the available configurations and column groups.

Recommended AWS Data Export starting settings for warehouse analysis
SettingRecommended starting valueWhy
TableCost and Usage Report 2.0Fixed schema and detailed AWS billing dimensions
Time granularityDailySufficient for monthly margin and most cost-allocation models
Include resource IDsOn when attribution needs individual resourcesAdds a resource identifier but increases row and file volume
Split cost allocationOn for relevant ECS or EKS workloadsAdds container-level allocation data for supported shared compute
FormatParquetCompressed, typed, and efficient for warehouse ingestion
VersioningOverwrite existing data export fileKeeps one complete, replaceable snapshot for each billing period
S3 accessPrivate bucket with Block Public Access enabledBilling exports contain commercially sensitive account and resource data

AWS delivers data beneath a predictable path:

s3://<bucket>/<prefix>/<export-name>/data/BILLING_PERIOD=YYYY-MM/

The export also includes manifest metadata. AWS describes the complete layout in Understanding export delivery.

Treat the Current Billing Period as Revisable

AWS can update a billing period as usage, credits, refunds, discounts, and late charges settle. It can also update the previous period during the first two weeks after that period ends. Treat every overwrite delivery as a complete replacement snapshot, not as new accounting events to append.

Use this executable loading pattern:

  1. In AWS, use Overwrite existing data export file.
  2. In Supaflow, select the open BILLING_PERIOD=YYYY-MM source object and use Historical Only ingestion with Truncate and Load.
  3. Observe when the daily AWS delivery normally reaches S3, then schedule the weekly Supaflow run several hours later or the following morning. AWS does not provide a user-selected delivery time.
  4. During the first two weeks of a new month, keep both the current and previous billing-period objects selected. After that adjustment window, deselect closed periods from recurring runs.
  5. Before publishing the dashboard, reconcile each replaced Snowflake table and confirm that the billing period's Manifest.json advanced. If it did not, rerun after AWS publishes the next manifest.

Each run reads the complete selected period, truncates its Snowflake landing table, and loads the new snapshot. No CUR merge key is required, and rows removed by an AWS restatement disappear from Snowflake when the table is replaced.

Supaflow scans the Parquet files beneath the configured S3 prefix; it does not use the file list inside the AWS manifest, and an ordinary Supaflow schedule does not wait conditionally for a manifest change. Conservative timing plus reconciliation is sufficient for this weekly analytical workflow. If your controls require manifest-gated execution or execution-level reproducibility, add an external Amazon S3 EventBridge orchestration step, publish only the manifest-listed files to a controlled prefix, or use a manifest-aware loader.

This pattern prevents refreshed files from duplicating cost and prevents removed snapshot rows from surviving downstream.

Choose a Finance-Approved AWS Cost Basis

Do not sum whichever cost column is easiest to find. Reserved Instances and Savings Plans move cost between usage, negation, and fee line items, so an undefined basis can materially change customer margin.

AWS cost bases for customer unit economics
Cost basisUse it forDecision
Unblended costInvoice and raw line-item reconciliationKeep as a control total, but do not use it as the default allocation basis because commitment charges are not spread to the usage they support
Amortized costOperational unit economics before applicable net or private discountsRecommended default; spreads RI and Savings Plan commitment cost across covered usage and exposes unused commitment
Net amortized costUnit economics after applicable private discountsUse only when the CUR net_* fields are populated and finance wants after-discount economics; reconcile it independently

For a reproducible amortized basis, implement the line-item logic AWS publishes for its cost and usage dashboard:

CASE
WHEN line_item_line_item_type = 'SavingsPlanCoveredUsage'
THEN savings_plan_savings_plan_effective_cost
WHEN line_item_line_item_type = 'SavingsPlanRecurringFee'
THEN savings_plan_total_commitment_to_date
- savings_plan_used_commitment
WHEN line_item_line_item_type IN (
'SavingsPlanNegation', 'SavingsPlanUpfrontFee'
)
THEN 0
WHEN line_item_line_item_type = 'DiscountedUsage'
THEN reservation_effective_cost
WHEN line_item_line_item_type = 'RIFee'
THEN reservation_unused_amortized_upfront_fee_for_billing_period
+ reservation_unused_recurring_fee
WHEN line_item_line_item_type = 'Fee'
AND COALESCE(reservation_reservation_a_r_n, '') <> ''
THEN 0
ELSE COALESCE(line_item_unblended_cost, 0)
END AS amortized_cost

AWS documents this exact line-item decision in the amortized_cost definition. If private pricing makes net cost the approved basis, use the corresponding line_item_net_unblended_cost, reservation_net_*, and savings_plan_net_* fields with fallbacks, following AWS's net amortized cost guidance.

Preserve the complete amortized total for reconciliation, then classify it before customer allocation:

  • Allocate RI and Savings Plan effective cost with the covered usage. Put unused commitment from RIFee and SavingsPlanRecurringFee into an explicit __SHARED_COMMITMENT__ pool unless finance approves another driver.
  • Preserve credits and refunds as negative amounts. Publish both gross and credit-adjusted views if finance excludes promotional, contractual, or one-time credits from operating unit economics.
  • Include fees only when the fee is part of the approved cost of service. Keep support, Marketplace, and other shared fees in named pools until their treatment is approved.
  • Keep taxes in a separate cost category. Exclude them from the default operational contribution-margin view unless finance explicitly treats them as cost of revenue.

Store cost_basis, cost_policy_version, and the original line-item type with every curated cost row. Finance should approve both the basis and the classification policy before the result is called gross margin.

Keep the Bucket Private and Separate Write From Read

The bucket policy that lets the AWS Data Exports service write files is different from the IAM role Supaflow assumes to read them.

Use these controls as the baseline:

  • enable all S3 Block Public Access settings
  • use Bucket owner enforced Object Ownership so ACLs remain disabled
  • let AWS Data Exports write only to the intended bucket and prefix
  • create a separate, read-only cross-account role for Supaflow
  • require an external ID in the role trust policy
  • scope s3:ListBucket and s3:GetObject to the export prefix
  • add kms:Decrypt only when the objects use a customer-managed KMS key
  • do not grant Supaflow PutObject, DeleteObject, or ACL permissions

The Supaflow Amazon S3 Data Lake source guide contains the role and prefix requirements. AWS documents the service-writer policy separately in Setting up an Amazon S3 bucket for Data Exports.

Step 2: Load the AWS Billing Export Into Snowflake

In Supaflow, create an Amazon S3 Data Lake source and a Snowflake destination.

CUR 2.0 places Parquet files directly inside BILLING_PERIOD=YYYY-MM folders. That path does not contain the {schema}/{table} hierarchy required by Supaflow's Auto-detect layout. Configure the source explicitly:

Supaflow S3 source settings for an AWS CUR 2.0 export
Supaflow settingValue
S3 Bucket Name / AWS RegionThe private export bucket and its Region
S3 Prefix Path<prefix>/<export-name>/data
AuthenticationThe read-only IAM role ARN and matching external ID
Table FormatParquet
Data Lake LayoutTable mapping
Table GroupingOne table per folder
Table Folder Depth1
Schema Refresh Interval0 to refresh discovery before every pipeline execution

With those settings, this AWS path:

s3://my-billing-bucket/finops/customer-margin/data/
`-- BILLING_PERIOD=2026-08/
+-- customer-margin-00001.snappy.parquet
`-- customer-margin-00002.snappy.parquet

is discovered as one normalized source object such as billing_period_2026_08, and every Parquet file beneath that folder belongs to the object. Do not select Auto-detect for this path.

Connect the Snowflake destination, select the current billing-period object, and configure the pipeline as follows:

  • Ingestion Mode: Historical Only
  • Load Mode: Truncate and Load
  • Schema Evolution Mode: Allow All Changes

With Schema Refresh Interval = 0, Supaflow discovers S3 objects before every execution. Allow All Changes automatically creates a selected mapping for each newly discovered billing_period_YYYY_MM object, so no manual schema refresh or object-addition step is required. After the late-adjustment window, deselect closed periods from recurring runs; their landed Snowflake tables remain available for history.

Create stg_aws_cur2 as a UNION ALL across the landed billing_period_% tables so downstream allocation models see one stable fact relation. Generate that union from Snowflake metadata or maintain it in a dbt macro when a new period table appears. Do not union separate refresh executions inside one period: the truncate-and-load table already represents the latest complete snapshot.

The first validation is financial, not merely technical:

landed raw cost
= curated AWS cost before allocation
= allocated customer cost + shared cost + explicitly unallocated cost

If those values do not reconcile for the same billing period, stop before joining revenue.

Step 3: Sync Stripe Revenue Data

Create a Stripe source in Supaflow using a restricted, read-only API key. For this model, the most useful objects are:

  • Customer for the Stripe customer identifier
  • Subscription and Price for plan context
  • Invoice for billed and paid amounts
  • Balance Transaction for charges, refunds, fees, and payout reconciliation
  • Event when recent lifecycle history is useful

Do not combine all of those amounts into one number. Decide which revenue basis the dashboard represents.

Three revenue views that should remain explicit in the model
Revenue viewTypical sourceSuitable forImportant limitation
Billed revenueStripe invoicesBilling operations and contract trendsAn invoice is not necessarily collected or recognized revenue
Collected cashStripe balance transactions and invoice payment stateCash and payment reconciliationCollection timing can differ from the service period
Recognized revenueFinance-approved revenue scheduleGAAP or management reportingUsually requires accounting rules beyond the Stripe invoice alone

For an operational margin dashboard, paid invoice amounts can be a useful starting point. Label the metric accurately. Do not call it recognized revenue unless the model implements your finance team's recognition policy.

Normalize all amounts to one reporting currency before aggregating customers or periods. Preserve the source currency and exchange rate used so finance can reproduce the result.

Step 4: Add Salesforce Customer Context

The Salesforce source supplies the dimensions that make margin actionable:

  • Account ID and account name
  • customer segment, industry, region, and owner
  • contract or entitlement attributes stored on the account
  • opportunity and product context for commercial analysis
  • custom fields that hold a tenant, workspace, or billing identifier

Use Stripe for actual billing outcomes and Salesforce for customer context. Opportunity amount is useful for pipeline and forecast analysis, but it should not silently replace invoice or recognized revenue in an actual-margin calculation.

Build a Durable Customer Identity Bridge

The hardest join is rarely SQL. It is identity.

AWS resources might carry a customer_id tag. Stripe uses a Customer ID. Salesforce uses an Account ID. Your application might use a tenant or workspace ID. Create one explicit bridge instead of joining names or email addresses at query time.

Recommended customer identity bridge columns
ColumnPurpose
customer_idDurable warehouse key used by margin models
tenant_idProduct or application tenant identifier
stripe_customer_idJoin to Stripe customers, invoices, and subscriptions
salesforce_account_idJoin to Salesforce account attributes
aws_allocation_keyJoin to a tag, account, workload, or usage-allocation result
valid_from / valid_toPreserve historical ownership and account changes
mapping_sourceRecord whether the mapping came from product data, CRM, billing metadata, or manual review

The cleanest pattern is to write your internal tenant ID into both the Stripe customer record and a Salesforce external-ID field when the account is provisioned. If that is not available, maintain a governed mapping table and surface exceptions for review.

Do not join customers by company name. Names change, collide, and vary across systems. Email-domain matching can help generate candidates, but it is not a safe accounting key.

Allocate AWS Cost Without Hiding Uncertainty

Separate cost into four pools before calculating customer margin.

A practical hierarchy for allocating AWS cost to customers
Cost poolAllocation methodExample
Directly attributableResource tag, linked account, or dedicated workloadA customer-dedicated database tagged with customer_id
Usage attributableProduct telemetry proportional to consumptionShared compute distributed by requests, tokens, jobs, or GB processed
Shared business costDocumented driver reviewed by finance and engineeringObservability allocated by each service's share of monitored workload
UnallocatedKeep visible until a defensible driver existsSupport charges, untagged networking, or an unidentified shared resource

For a usage-allocated cost pool:

customer allocated cost
= shared cost pool
x customer usage units / total usage units

Choose a driver that causes the cost. Requests may explain API compute. Stored bytes may explain storage. Tokens may explain model inference. Revenue is usually a weak infrastructure driver because allocating more cost to the largest contract can conceal a high-usage, low-margin customer.

Keep an allocation_method and allocation_version on the output. When the methodology changes, you should be able to explain why historical margin moved.

Never force every dollar into a customer row merely to make a chart add to 100 percent. An explicit unallocated bucket is more trustworthy than false precision.

Keep raw ingestion separate from finance-approved business logic.

A warehouse model for AWS cost, Stripe revenue, and Salesforce context
ModelGrainResponsibility
stg_aws_cur2AWS line itemNormalize CUR types, dates, cost fields, tags, and resource identifiers
stg_stripe_invoiceStripe invoiceNormalize customer, status, currency, and invoice amounts
stg_stripe_balance_transactionStripe balance transactionPreserve cash movement, fees, refunds, and reporting currency
stg_salesforce_accountSalesforce account versionNormalize customer attributes and ownership
bridge_customer_identityCross-system identifier versionMap tenant, Stripe, Salesforce, and AWS allocation keys
fct_aws_cost_allocatedCustomer, day, cost poolStore direct, usage-based, shared, and unallocated AWS cost
fct_customer_revenueCustomer, month, currencyStore the explicitly named revenue basis
fct_customer_marginCustomer, monthCombine revenue, cost of service, gross profit, and margin

The final model can remain simple when the upstream definitions are explicit:

WITH revenue AS (
SELECT
revenue_month,
COALESCE(NULLIF(TRIM(customer_id), ''), '__UNMAPPED_REVENUE__')
AS customer_id,
SUM(net_revenue_reporting_currency) AS net_revenue
FROM analytics.fct_customer_revenue
GROUP BY 1, 2
),
cloud_cost AS (
SELECT
DATE_TRUNC('month', usage_date) AS cost_month,
COALESCE(NULLIF(TRIM(customer_id), ''), '__UNALLOCATED_COST__')
AS customer_id,
SUM(allocated_aws_cost) AS aws_cost
FROM analytics.fct_aws_cost_allocated
GROUP BY 1, 2
),
customer_months AS (
SELECT revenue_month AS month, customer_id FROM revenue
UNION
SELECT cost_month AS month, customer_id FROM cloud_cost
)
SELECT
cm.month,
cm.customer_id,
sf.account_name,
sf.segment,
sf.industry,
COALESCE(r.net_revenue, 0) AS net_revenue,
COALESCE(c.aws_cost, 0) AS aws_cost,
COALESCE(r.net_revenue, 0) - COALESCE(c.aws_cost, 0) AS contribution_profit,
(COALESCE(r.net_revenue, 0) - COALESCE(c.aws_cost, 0))
/ NULLIF(r.net_revenue, 0) AS contribution_margin
FROM customer_months cm
LEFT JOIN revenue r
ON cm.month = r.revenue_month
AND cm.customer_id = r.customer_id
LEFT JOIN cloud_cost c
ON cm.month = c.cost_month
AND cm.customer_id = c.customer_id
LEFT JOIN analytics.dim_customer sf
ON cm.customer_id = sf.customer_id
AND cm.month::TIMESTAMP_NTZ >= sf.valid_from
AND cm.month::TIMESTAMP_NTZ < COALESCE(
sf.valid_to,
'9999-12-31'::TIMESTAMP_NTZ
);

The source-specific sentinels keep unmapped Stripe revenue and unallocated AWS cost on separate rows. They cannot join and net against each other merely because they occurred in the same month, and neither exception disappears through null equality. The Salesforce join uses the customer attributes effective on the first day of the reporting month. Enforce non-overlapping valid_from and valid_to intervals so one customer-month cannot join multiple dimension versions. If the business needs attributes as of the exact usage or invoice date, make that effective-date join at daily grain before aggregating to month.

This query calls the result contribution margin because it subtracts only the AWS cost included in this model. Rename it gross margin only after finance confirms that all required cost-of-revenue components are present, such as payment fees, customer support, third-party APIs, and production operations.

Dashboard Metrics That Drive Decisions

Once the model reconciles, publish metrics that show both outcome and data quality:

  • net revenue, allocated AWS cost, and contribution profit by month
  • contribution margin by customer, segment, product, region, and owner
  • AWS cost as a percentage of revenue
  • cost per active customer, transaction, request, token, or GB
  • month-over-month unit-cost and margin movement
  • customers with cost but no revenue
  • customers with revenue but no mapped infrastructure cost
  • direct, usage-allocated, shared, and unallocated cost percentages
  • identity-mapping coverage and untagged AWS cost

The unallocated and mapping-coverage metrics belong on the same dashboard as margin. Otherwise a cleaner-looking margin number can simply reflect missing cost.

Reconciliation Tests to Automate

Add tests before stakeholders begin using the result for pricing or account decisions.

  1. AWS completeness: Curated AWS cost equals the finance-approved amortized or net amortized basis for the same billing period.
  2. Allocation conservation: Direct, shared, usage-allocated, and unallocated outputs sum to the curated AWS total.
  3. Stripe reconciliation: The selected invoice or balance-transaction total agrees with the Stripe reporting view for the same status, currency, and period.
  4. Identity coverage: Every material Stripe customer and AWS allocation key maps to one governed customer record or an exception queue.
  5. No many-to-many explosion: Joining the identity bridge does not multiply invoices or AWS line items.
  6. Currency consistency: Every reported amount has a source currency, conversion date, rate, and reporting currency.
  7. Restatement safety: A new manifest followed by a replacement run updates the billing-period snapshot, removes rows absent from the new snapshot, and does not duplicate prior cost.
  8. Temporal dimension uniqueness: Every customer-month joins at most one effective Salesforce dimension version.

Common Mistakes

Treating the AWS bill as customer cost

The bill is the input. Customer cost requires tags, telemetry, and an allocation policy.

Treating invoice amount as recognized revenue

Invoices, collections, and recognized revenue answer different questions. Name the metric precisely and let finance approve the accounting basis.

Allocating shared cost by revenue by default

Revenue is easy to obtain but often unrelated to infrastructure consumption. Prefer a causal usage driver when one exists.

Joining Salesforce and Stripe by name

Names are not stable identifiers. Create a bridge with durable keys and effective dates.

Hiding unattributed spend

An unallocated bucket is a control signal. Track it, assign owners, and reduce it over time.

Ignoring AWS restatements

CUR data for an open billing period can change. Model the delivery as a revisable snapshot and make the warehouse load idempotent.

Frequently Asked Questions

How do I load an AWS Cost and Usage Report into Snowflake?

Create a CUR 2.0 standard export in AWS Data Exports, deliver it to a private S3 bucket as Parquet, and configure the Supaflow S3 source as Table mapping > One table per folder > Folder depth 1 with Schema Refresh Interval = 0. Use Allow All Changes, Historical Only, and Truncate and Load, then union the landed monthly tables in dbt.

What is the difference between legacy AWS CUR and CUR 2.0?

AWS documents CUR 2.0 as a fixed schema with nested map columns for data such as resource tags, products, discounts, and cost categories. Legacy CUR can change columns based on account activity and report configuration. AWS provides a CUR-to-CUR-2.0 migration comparison.

Can AWS calculate cost per customer automatically?

AWS can group spend using accounts, cost categories, and activated cost-allocation tags. Customer-level cost for shared SaaS infrastructure usually requires additional allocation data, such as requests, storage, tokens, jobs, or another product-usage measure.

Which Stripe amount should I use for margin analysis?

Use the amount that matches the dashboard's stated purpose. Paid invoice amounts can support operational billing analysis, balance transactions can support cash and fee reconciliation, and recognized revenue requires your finance team's revenue-recognition rules. Do not mix these definitions under one generic revenue label.

Is customer revenue minus AWS cost the same as gross margin?

Only when AWS cost is the complete cost of revenue under your accounting policy. Most companies need additional direct costs before calling the result gross margin. Until then, label the metric contribution profit or cloud contribution margin.

Build the Pipeline With Supaflow

Supaflow can read the Parquet export from your S3 bucket, sync Stripe and Salesforce data, load the results into Snowflake, and run dbt Core models in the same orchestration.

Start with the Amazon S3 Data Lake source, Stripe source, Salesforce source, and Snowflake destination. Then add the customer identity, allocation, and margin models as a dbt transformation after ingestion.

Start a free Supaflow workspace or book a demo to build the AWS-cost-to-customer-margin workflow with your own account structure and revenue definition.