Cloud Horizon Get the free audit

April 29, 2026 9 min read

Multi-cloud cost attribution without agents (AWS Organizations + Azure Management Groups)

Tag-based plus account-based attribution across AWS and Azure, using the cost data both clouds already give you. No agents, no Terraform, no extra runtime.

The standard advice for multi-cloud cost attribution starts with "install our agent on every workload". You do not need that. AWS and Azure both publish the cost and resource metadata you need, free, in formats designed for exactly this. The work is in the rules, not the plumbing.

Here is how we do it for customers running on AWS Organizations and Azure Management Groups, with no agents, no Terraform changes, and no runtime overhead in their accounts.

The two-layer model: account first, tags second

Most attribution writeups jump straight to tags. Tags are real. They are also incomplete. A typical AWS environment has 60% to 80% tag coverage on cost-bearing resources. The remaining 20% to 40% is the long tail: managed services that do not support tags consistently (some flavors of Elastic Load Balancer attributes, certain AWS Backup line items, support charges, AWS Marketplace fees), resources created by automation that drops the tag, resources older than your tag policy.

Account-level attribution catches that 20 to 40%. If your AWS organizational unit structure already groups accounts by team or product, you have a fallback. The shape we use:

Layer 1: AWS account (or Azure subscription) -> default owner team
Layer 2: tag rules within that account -> override owner per resource
Layer 3: untagged within an account -> falls back to layer 1 default

Tag wins where present. Account is the safety net. No resource is ever un-attributed.

What AWS gives you for free

Three things, all available without changing anything in production.

Cost and Usage Report (CUR). Configure once in your management account, lands as Parquet (or CSV, do not pick CSV) in S3 every six hours. Per-resource granularity, every line item from every linked account, every tag activated for cost allocation. This is the ground truth. Schema is documented and stable.

AWS Organizations API. The ListAccounts and ListOrganizationalUnitsForParent calls give you the full account tree. Pair that with a small mapping file that says "OU ou-platform belongs to platform team" and you have account-level attribution.

Resource Groups Tagging API. tagging:GetResources returns every taggable resource in an account with its tags. Useful for plugging tag holes after the fact: filter for resources missing a required tag, hand the list to the owning team.

Everything you need to attribute every dollar in your AWS bill is in those three sources. Nothing has to run inside your workload accounts.

What Azure gives you for free

Symmetric, with different names.

Cost Management exports. Daily or monthly export of the cost detail (called the "amortized" or "actual" view) to a Storage Account in your tenant. Parquet or CSV, similar shape to CUR. Per resource, per meter, per subscription, per tag.

Management Groups. Subscriptions roll up into Management Groups, which are arbitrary trees you control. You can scope an export to a Management Group root, getting cost data for the whole hierarchy in a single export. The root Management Group is usually the tenant root, and is the right scope for an enterprise export.

Resource Graph. Kusto-flavoured query API over every resource in every subscription you can read. With a service principal granted the Reader role at the root Management Group, you can write one KQL query that returns every resource and its tags. Same role you grant for the cost export.

Two API permissions and one storage export. That is the access footprint to do attribution for the whole Azure side.

The mapping file: where the work actually is

Build a single attribution config in your repo. YAML works. Something like:

defaults:
  team: unowned

aws:
  organizations:
    o-rootid:
      ou-platform: { team: platform }
      ou-product-a: { team: product-a }
      ou-product-b: { team: product-b }
      ou-data: { team: data-platform }
      ou-sandbox: { team: rd, environment: sandbox }
  account_overrides:
    "111111111111": { team: shared-services }
    "222222222222": { team: security }

azure:
  management_groups:
    "/providers/Microsoft.Management/managementGroups/contoso":
      "/providers/Microsoft.Management/managementGroups/platform": { team: platform }
      "/providers/Microsoft.Management/managementGroups/product-a": { team: product-a }
      "/providers/Microsoft.Management/managementGroups/data": { team: data-platform }
  subscription_overrides:
    "00000000-0000-0000-0000-000000000001": { team: shared-services }

tag_rules:
  precedence: [ owner_team, project, cost_center ]
  values:
    owner_team: tag:owner_team
    project: tag:project
    cost_center: tag:cost_center
  fallbacks:
    project_from_account: true

The shape matters. precedence defines which tag keys to check, in order. The first match wins. If none of those tags are present, fall back to the account or subscription mapping. If even that misses, the account default applies. If that misses, you have a hole, and the report says "unowned" instead of silently misattributing.

Two real-world rules we always recommend:

  • Reject ambiguous tag values. Half the cost allocation tags in production are typos: owner_team=Platform versus platform versus plat. Define a canonical set of team names in the same config and reject anything else. Print the rejected values in the weekly report so the owning teams notice.
  • Treat untaggable line items as account-level. AWS Support charges, AWS Marketplace fees, RI/SP unused commitment, enterprise discount program adjustments. None of these have tags. They all live in a specific account. Attribute by account and move on.

The query side: a single SQL view per cloud

With CUR landed in S3 and Azure exports landed in a Storage Account, the attribution layer is a SQL view. We use Athena for AWS and the same approach works in Synapse Serverless or Databricks for Azure.

Pseudocode for the AWS side:

CREATE VIEW attributed_cost AS
SELECT
  bill_billing_period_start_date AS billing_month,
  line_item_usage_start_date     AS usage_date,
  line_item_usage_account_id     AS account_id,
  product_servicecode            AS service,
  line_item_usage_type           AS usage_type,
  line_item_resource_id          AS resource_id,
  COALESCE(
    NULLIF(resource_tags_user_owner_team, ''),
    NULLIF(resource_tags_user_project,  ''),
    account_default_team(line_item_usage_account_id),
    'unowned'
  ) AS team,
  line_item_unblended_cost AS cost
FROM cur_table
WHERE line_item_line_item_type IN ('Usage', 'DiscountedUsage', 'Tax')
   OR line_item_line_item_type LIKE 'SavingsPlan%'
   OR line_item_line_item_type IN ('Refund', 'Credit', 'Fee');

Two notes. First, the resource_tags_user_* columns only exist for tags you have activated as cost allocation tags in the AWS Billing console. Activate them, do not skip the step. Second, the line item type filter matters. RIFee and SavingsPlanRecurringFee are upfront commitment charges, attribute them by account, not by resource. The Discounted line items are where the resource lives.

The Azure side is structurally the same, just different column names in the cost export.

What this gets you

After the views are in place and the mapping config is filled in, you can answer questions like:

  • What did the data platform team spend last month, across AWS and Azure, broken down by service?
  • Of the platform team's spend, how much is attributed by tag versus falling back to account?
  • Show me every resource over $1,000 last month with no owner_team tag.
  • Compare cost per team this month versus the same month last quarter, AWS plus Azure together.

Without an agent. Without a Terraform module. Without changing anything in your workload accounts. The whole thing reads from data the cloud already produces.

Where it gets hard

Three real problems worth flagging up front.

Shared infrastructure. A NAT Gateway in a shared VPC, a centralized monitoring account, a Transit Gateway that ten workloads use. You can either pick one team to absorb the cost, or split it. Splitting is harder than it sounds: pro-rata by data transferred is reasonable for the NAT, but the monitoring account is often a fixed cost regardless of usage. Pick a method per shared resource and document it.

Reserved Instance and Savings Plan benefits. AWS applies SP and RI benefits across the organization to whichever usage matches. The "RI applied" line lands in the account that owns the matching usage, which may not be the account that bought the RI. The default behaviour is fine for FinOps reporting (it shows the real net cost) but if internal chargeback expects the buying account to keep the discount, you need a separate accounting layer. Decide on day one.

Tag drift over time. A team renames itself. A new cost allocation tag rolls out. The mapping file gets stale. Two habits help: keep the mapping config in the same repo as your IaC, review it quarterly, and put a "unowned" alert in your weekly cost digest. If "unowned" goes from 5% to 15% in a month, somebody pushed bad tags somewhere.

The shape of the access we ask for

For the curious, here is what we actually ask for when we set this up for a customer.

AWS: a CloudFormation stack you deploy in your management account. It creates one IAM role we assume. The role has read-only Cost Explorer permissions, S3 read on the CUR bucket, AWS Organizations list permissions, and Resource Groups Tagging read. No write permissions. Setup takes 10 minutes.

Azure: a service principal you create in your tenant. Grant it Reader and Cost Management Reader at the root Management Group. We use the same SP to read the cost export Storage Account. No write permissions. Setup takes 10 minutes.

The total surface area of "what runs in your environment" is two read roles. That is the whole footprint.


Cloud Horizon builds attribution like the above for our customers and keeps it current. If you would rather we ran it than build it yourself, the free 14-day audit includes a pass on tag coverage and account-level attribution across AWS and Azure. We send you the spreadsheet of findings either way.