· 7 min read
Multi-Cloud FinOps: Architecting for Zero-Waste Azure and AWS Deployments
Engineering-led FinOps for AWS and Azure: enforced tagging, pre-merge cost estimates, scale-to-zero non-prod, and FOCUS-normalised billing data you can query.
The Problem
Cloud cost problems are rarely caused by one bad decision. They come from thousands of small, unattributed ones: a load-test cluster nobody tore down, a development database sized like production, a cross-region replication job whose data transfer was never estimated. In multi-cloud estates these costs land in two or three billing systems, each with its own schema, discount model and tag semantics.
The traditional response is reactive. Finance receives the invoice weeks after the spend happened, then asks engineering who owns which line items. By then the money is gone, the context has been lost, and the conversation is adversarial. The underlying gap is architectural. Cost is not treated as a design constraint, and attribution is not enforced where resources are created.
The Architectural Solution
The FinOps Framework describes cloud financial management as a shared practice across engineering, finance and the business. For an architect, that becomes four concrete controls, applied as close to resource creation as possible:
- Attribution by construction. Every resource carries ownership tags (
CostCenter,Owner,Environment). Tags are applied by default in infrastructure as code (IaC) and enforced by the cloud control plane. The control-plane enforcement catches resources created outside the pipeline. - Cost visibility before merge. A cost-estimation step, such as Infracost, posts the monthly cost delta of each infrastructure pull request. Large deltas need an explicit approval rather than a silent merge.
- Elastic non-production. Development and test environments scale to zero or shut down on a schedule. They are not left running around the clock.
- One normalised cost dataset. Billing exports from every provider land in a single store using the FOCUS specification (FinOps Open Cost and Usage Specification). Showback, anomaly detection and unit economics then run on a single schema rather than one per cloud.
Enforcement Layers by Provider
IaC defaults are a convenience, not a control. Anything created through the console, CLI or another tool bypasses them. Pair them with control-plane enforcement:
| Layer | AWS | Azure |
|---|---|---|
| Default tags in IaC | Terraform provider default_tags |
Terraform azurerm tag variables / module defaults |
| Block untagged creation | Service control policies (SCPs) with aws:RequestTag conditions |
Azure Policy built-in Require a tag on resources (deny) |
| Normalise tag values | Organizations tag policies (case, allowed values) | Inherit a tag from the resource group if missing (modify, with remediation) |
| Make tags visible in billing | Activate cost allocation tags | Tags flow into Cost Management; optional tag inheritance |
Two AWS details trip up most teams. First, tag policies do not evaluate untagged resources. They standardise tag values but will not block a resource that has no tags, so you need SCPs to require tags at creation. Second, user-defined tags only appear in billing data after you activate them as cost allocation tags. That can take up to 24 hours for the key to appear and another 24 hours to activate.
Architecture Diagram

Core Implementation
Tagging and Scale-to-Zero in Terraform
The example below combines provider-level default tags, input validation that rejects malformed cost centres at plan time, and an Aurora Serverless v2 cluster that pauses to zero capacity outside production.
variable "environment" {
type = string
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment must be dev, test or prod."
}
}
variable "cost_center" {
type = string
validation {
condition = can(regex("^CC-[0-9]{4}$", var.cost_center))
error_message = "cost_center must match CC-0000."
}
}
provider "aws" {
region = "us-east-1"
# Applied to every taggable resource this provider manages
# (aws_autoscaling_group is the documented exception).
default_tags {
tags = {
Environment = var.environment
CostCenter = var.cost_center
Owner = "platform-engineering"
ManagedBy = "terraform"
}
}
}
locals {
is_prod = var.environment == "prod"
}
resource "aws_rds_cluster" "feature_store" {
cluster_identifier = "feature-store-${var.environment}"
engine = "aurora-postgresql"
engine_mode = "provisioned" # Serverless v2 uses provisioned mode
storage_encrypted = true
# Credentials, networking and engine_version omitted for brevity.
serverlessv2_scaling_configuration {
min_capacity = local.is_prod ? 2 : 0 # 0 ACU = auto-pause
max_capacity = local.is_prod ? 64 : 4
seconds_until_auto_pause = local.is_prod ? null : 600 # only valid when min = 0
}
}
resource "aws_rds_cluster_instance" "feature_store" {
cluster_identifier = aws_rds_cluster.feature_store.id
instance_class = "db.serverless"
engine = aws_rds_cluster.feature_store.engine
}
Some notes for production use:
- Version requirements. Scale-to-zero requires a sufficiently recent Aurora engine version and AWS provider. Older providers reject
min_capacity = 0. - Cold starts. The first connection after a pause waits while the cluster resumes. That is acceptable for development but rarely for latency-sensitive production paths.
- Serverless v1 is gone. The older
engine_mode = "serverless"with ascaling_configurationblock belonged to Aurora Serverless v1, which reached end of life on 31 March 2025. Do not copy it from older examples. Use Aurora Serverless v2 instead.
Querying FOCUS Data Across Clouds
When both providers export FOCUS-formatted data, a single query gives you showback by cost centre. The example uses DuckDB over Parquet exports:
-- Monthly effective cost by cost centre and service category, AWS + Azure.
-- EffectiveCost amortises commitment purchases; BilledCost is the invoice view.
SELECT
COALESCE(json_extract_string(Tags, '$.CostCenter'), '(unallocated)') AS cost_center,
ServiceCategory,
ROUND(SUM(EffectiveCost), 2) AS effective_cost
FROM read_parquet(['exports/aws/*.parquet', 'exports/azure/*.parquet'],
union_by_name = true)
WHERE ChargePeriodStart >= TIMESTAMP '2026-09-01'
AND ChargePeriodStart < TIMESTAMP '2026-10-01'
AND ChargeCategory = 'Usage'
GROUP BY ALL
ORDER BY effective_cost DESC;
Treat the (unallocated) row as a key performance indicator. Its share of total spend is the most honest measure of your tagging maturity. Two practical caveats apply. Providers support different FOCUS versions and may serialise Tags differently (JSON string vs. map), and tag keys may need case normalisation before you join datasets. Validate the schema of each export before trusting the totals.
Edge Cases & Limitations
-
Shared platforms resist tagging. A Kubernetes cluster, Kafka bus or central egress firewall has one cloud bill line but dozens of consumers. Allocation then needs workload-level telemetry. OpenCost, a CNCF incubating project, allocates cluster cost to namespaces and labels based on resource requests and usage, and reports idle capacity separately. Agree up front with Finance whether idle and shared overhead is spread proportionally or held centrally. Most chargeback disputes come from that decision, not from the tooling.
-
Estimates are not invoices. Pre-merge estimators price the resources declared in code. Usage-driven charges, such as data transfer, request counts and log ingestion, depend on traffic the tool cannot see. Infracost lets you supply assumptions through an
infracost-usage.ymlfile, but the result is only as good as those assumptions. A modest compute change that adds cross-region replication can cost far more in transfer than in compute. Review data-flow diagrams, not just resource lists. -
Cost gates create friction. If every pull request with a cost increase needs a VP’s signature, engineers will batch changes or route around the pipeline. Use thresholds as a percentage of the service’s current run-rate. Make the default action “notify the owning team” and reserve hard blocks for large absolute increases.
-
Scheduled shutdowns break things quietly. Overnight shutdown of non-production environments saves real money, but it breaks overnight batch tests, offshore teams in other time zones, and stateful services that do not resume cleanly. Publish the schedule, provide a self-service “keep alive” override that expires, and exclude anything tagged as shared.
-
Tagging is not optimisation. Perfect attribution tells you who spent the money, not whether they should have. Rightsizing, commitment coverage (Savings Plans, Reservations) and architectural choices, such as storage tiers and data locality, usually move the bill more than governance alone.
Key Strategic Takeaways
- Enforce attribution in two places. Use IaC defaults for convenience and control-plane policy (SCPs, Azure Policy) for guarantees. Activate the tags in billing, or they are invisible.
- Show cost before merge, block rarely. Visibility changes behaviour. Hard gates should be reserved for exceptional deltas.
- Make non-production elastic by default. Scale-to-zero databases and scheduled shutdowns remove a large class of idle spend, provided teams have a documented override.
- Normalise on FOCUS. One schema across providers turns multi-cloud showback from a spreadsheet exercise into a query.
- Measure unallocated spend. It is the clearest single indicator of whether your FinOps architecture actually works. Delivering these controls through GitOps keeps them consistent across accounts and subscriptions.
Recommended Industry Reading & References
- FinOps Foundation: FinOps Framework - The operating model, personas and capabilities for cloud financial management.
- FinOps Foundation: FOCUS Specification - The current FinOps Open Cost and Usage Specification for normalised billing data.
- AWS: Implementing and Enforcing Tagging - AWS guidance on combining tag policies, SCPs and IaC.
- AWS: Activating User-Defined Cost Allocation Tags - Why tags do not appear in billing until activated.
- Microsoft Learn: Policy Definitions for Tagging Resources - Built-in Azure Policy definitions to require and inherit tags.
- AWS Well-Architected Framework: Cost Optimization Pillar - Design principles for cost-efficient workloads.
- OpenCost (CNCF): OpenCost Documentation - Open-source Kubernetes cost allocation.