m requires normalization, centralized aggregation, and automated optimization rules. The following implementation uses TypeScript to extract, transform, and report cost data across AWS, Azure, and GCP.
Step-by-Step Implementation
- Enforce cost allocation tags across all providers (
CostCenter, Environment, Team, Workload, DataGravity).
- Configure billing exports to a unified storage layer (S3, Blob, Cloud Storage) or query directly via provider APIs.
- Build a TypeScript aggregator that authenticates, paginates, normalizes currency/units, and joins cost data with resource metadata.
- Apply optimization rules (right-sizing recommendations, commitment utilization checks, egress routing analysis).
- Expose results via CLI, dashboard, or CI/CD gate for pre-deployment cost validation.
Architecture Decisions & Rationale
- Direct API extraction over third-party SaaS: Reduces vendor lock-in in the FinOps toolchain. Native APIs provide line-item granularity that aggregated dashboards obscure.
- TypeScript for aggregation: Strong typing prevents financial data corruption.
bigint/decimal handling ensures precision. Easy integration with IaC (Terraform/CDK) and CI/CD pipelines.
- Normalized schema: All providers map to a unified interface with ISO-8601 timestamps, USD conversion, and standardized resource types. This enables apples-to-apples comparison.
- Scheduled extraction + anomaly detection: Daily pulls prevent data staleness. Threshold-based alerts catch cost drift before end-of-month billing shocks.
Code Example: Cross-Cloud Cost Aggregator
import { CostExplorerClient, GetCostAndUsageCommand } from "@aws-sdk/client-cost-explorer";
import { CostManagementClient, QueryDefinition } from "@azure/arm-cost-management";
import { BigQuery } from "@google-cloud/bigquery";
import { Decimal } from "decimal.js";
interface CostRecord {
provider: "aws" | "azure" | "gcp";
date: string;
service: string;
region: string;
costUsd: Decimal;
usageUnit: string;
tags: Record<string, string>;
}
class MultiCloudCostAggregator {
private awsClient: CostExplorerClient;
private azureClient: CostManagementClient;
private gcpClient: BigQuery;
constructor() {
this.awsClient = new CostExplorerClient({ region: "us-east-1" });
this.azureClient = new CostManagementClient(new DefaultAzureCredential());
this.gcpClient = new BigQuery({ projectId: process.env.GCP_PROJECT_ID });
}
async fetchAwsCost(startDate: string, endDate: string): Promise<CostRecord[]> {
const command = new GetCostAndUsageCommand({
TimePeriod: { Start: startDate, End: endDate },
Granularity: "DAILY",
Metrics: ["UnblendedCost"],
GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }, { Type: "TAG", Key: "Environment" }],
});
const response = await this.awsClient.send(command);
return (response.ResultsByTime ?? []).flatMap(day =>
day.Groups?.map(group => ({
provider: "aws" as const,
date: day.TimePeriod?.Start ?? "",
service: group.Keys?.[0] ?? "unknown",
region: "us-east-1",
costUsd: new Decimal(group.Metrics?.UnblendedCost?.Amount ?? "0"),
usageUnit: "USD",
tags: { Environment: group.Keys?.[1] ?? "" },
})) ?? []
);
}
async fetchAzureCost(startDate: string, endDate: string): Promise<CostRecord[]> {
const query: QueryDefinition = {
type: "ActualCost",
timeframe: "Custom",
timePeriod: { from: startDate, to: endDate },
dataset: {
granularity: "daily",
aggregation: { totalCost: { name: "Cost", function: "Sum" } },
grouping: [{ type: "Dimension", name: "ServiceName" }, { type: "TagKey", name: "Environment" }],
},
};
const response = await this.azureClient.query("providers/Microsoft.Billing/billingAccounts/123456:scope", query);
return (response.rows ?? []).map(row => ({
provider: "azure" as const,
date: row[0] as string,
service: row[1] as string,
region: "East US",
costUsd: new Decimal(row[2] as string),
usageUnit: "USD",
tags: { Environment: row[3] as string },
}));
}
async fetchGcpCost(startDate: string, endDate: string): Promise<CostRecord[]> {
const query = `
SELECT
DATE(usage_start_time) AS date,
service.description AS service,
location.region AS region,
SUM(cost) AS costUsd,
ARRAY_TO_STRING(ARRAY(SELECT label.value FROM UNNEST(labels) AS label WHERE label.key = 'Environment'), '') AS env
FROM \`project_id.billing_dataset.gcp_billing_export\`
WHERE usage_start_time BETWEEN @start AND @end
GROUP BY date, service, region, env
`;
const [rows] = await this.gcpClient.query({
query,
params: { start: startDate, end: endDate },
});
return rows.map(row => ({
provider: "gcp" as const,
date: row.date,
service: row.service,
region: row.region,
costUsd: new Decimal(row.costUsd),
usageUnit: "USD",
tags: { Environment: row.env },
}));
}
async normalizeAndCompare(startDate: string, endDate: string): Promise<Record<string, Decimal>> {
const [aws, azure, gcp] = await Promise.all([
this.fetchAwsCost(startDate, endDate),
this.fetchAzureCost(startDate, endDate),
this.fetchGcpCost(startDate, endDate),
]);
const allRecords = [...aws, ...azure, ...gcp];
const grouped: Record<string, Decimal> = {};
for (const record of allRecords) {
const key = `${record.provider}:${record.service}`;
grouped[key] = (grouped[key] ?? new Decimal(0)).add(record.costUsd);
}
return grouped;
}
}
This aggregator handles authentication via environment variables or IAM roles, normalizes daily cost data, and groups by provider + service. Production deployments should add:
- Currency conversion via
exchangerate-api
- Commitment utilization tracking (RI/CUD amortization)
- Egress-specific filtering using provider billing line items (
DataTransfer-Out-Bytes)
- CI/CD integration for pre-deployment cost gates
Pitfall Guide
1. Ignoring Data Egress as a Primary Cost Driver
Teams optimize compute while leaving cross-cloud data pipelines unmonitored. Egress pricing scales linearly with volume and often dominates TCO. Best practice: Tag data pipelines with DataGravity and route egress through provider-native peering or CDN edges. Cap cross-cloud replication to synchronous-only paths.
2. Mismatched Discount Commitments
Purchasing 1-year AWS Savings Plans while workloads shift to GCP after six months leaves 40β60% of commitments unused. Best practice: Implement commitment portfolio tracking. Use flexible savings plans where available, and align procurement with quarterly workload forecasts.
Untagged resources become financial black holes. Providers enforce different tag schemas and propagation delays. Best practice: Deploy organization-level tag policies via AWS SCPs, Azure Policy, and GCP Org Policy. Enforce CostCenter, Environment, and Owner as mandatory. Automate tagging via IaC hooks.
4. Over-Provisioning Managed Services
Managed databases and message queues charge for provisioned capacity, not actual usage. Teams default to high-IOPS SSDs and multi-AZ deployments without workload validation. Best practice: Use burstable instances for dev/test, enable autoscaling, and switch to serverless/autoscaling tiers for variable workloads. Review IOPS vs throughput monthly.
5. Cross-Region Replication Blind Spots
Multi-region deployments incur replication egress, snapshot storage, and failover testing costs that are rarely budgeted. Best practice: Model replication costs explicitly in architecture diagrams. Use provider-native cross-region transfer discounts where available. Limit synchronous replication to critical state only.
6. Treating List Prices as Actual Cost
Spot/preemptible pricing, sustained-use discounts, and tiered egress rates make list prices irrelevant for production workloads. Best practice: Build cost models using actual billing exports, not marketing pages. Implement price elasticity thresholds in deployment pipelines.
7. Lack of Real-Time Cost Anomaly Detection
Monthly billing cycles delay detection of runaway resources. A misconfigured load balancer or unbounded log export can drain thousands before month-end. Best practice: Deploy daily cost anomaly detection using statistical thresholds (e.g., >15% daily variance). Integrate alerts with Slack/PagerDuty and auto-suspend non-critical environments.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Data-heavy analytics (ETL, ML training) | GCP or Azure with native data lakes | Lower egress rates, sustained-use discounts, optimized storage tiers | 20β30% reduction vs AWS |
| Enterprise hybrid (on-prem + cloud) | Azure with Azure Arc + Hybrid Benefit | Seamless identity, licensing reuse, enterprise support | 15β25% savings on Windows/SQL |
| Global low-latency serving | AWS + CloudFront/Global Accelerator | Mature edge network, predictable egress tiers, RI flexibility | 10β18% reduction with proper caching |
| Startup / variable workload | GCP + preemptible + autoscaling | Sustained-use discounts, no upfront commitments, fast scale-down | 25β35% reduction vs reserved models |
| Compliance-heavy (finance, healthcare) | AWS or Azure with dedicated regions | Audit trails, compliance certifications, isolated networking | Neutral cost, higher operational overhead |
Configuration Template
{
"aggregator": {
"schedule": "0 6 * * *",
"lookbackDays": 30,
"currency": "USD",
"thresholds": {
"dailyVariancePercent": 15,
"egressCostCapUsd": 5000,
"commitmentUtilizationMin": 75
},
"providers": {
"aws": {
"region": "us-east-1",
"roleArn": "arn:aws:iam::123456789012:role/CostAggregator",
"tagKeys": ["CostCenter", "Environment", "DataGravity"]
},
"azure": {
"tenantId": "xxxx-xxxx-xxxx-xxxx",
"subscriptionId": "yyyy-yyyy-yyyy-yyyy",
"scope": "providers/Microsoft.Billing/billingAccounts/xxxx",
"tagKeys": ["CostCenter", "Environment", "Team"]
},
"gcp": {
"projectId": "prod-multi-cloud-01",
"billingDataset": "gcp_billing_export",
"tagKeys": ["cost-center", "env", "owner"]
}
},
"output": {
"format": "json",
"destination": "s3://cost-analytics-bucket/multi-cloud/",
"alertChannels": ["slack", "pagerduty"]
}
}
}
Quick Start Guide
- Install dependencies:
npm install @aws-sdk/client-cost-explorer @azure/arm-cost-management @google-cloud/bigquery decimal.js
- Configure authentication: Set
AWS_PROFILE, AZURE_CLIENT_ID/SECRET/TENANT_ID, and GOOGLE_APPLICATION_CREDENTIALS environment variables.
- Deploy the aggregator: Run
node dist/aggregator.js --start 2024-01-01 --end 2024-01-31 --config config.json
- Validate output: Check
output.json for normalized provider/service costs. Verify variance thresholds trigger alerts if daily spend exceeds 15%.
- Schedule execution: Add a cron job or GitHub Actions workflow to run daily. Integrate results into your FinOps dashboard or CI/CD cost gate.
Multi-cloud cost comparison is not a pricing exercise. It is an architecture discipline. When you normalize billing data, enforce tagging, and align workloads with provider strengths, cost optimization becomes a predictable engineering outcome rather than a monthly firefight.