Overview
At the end of the month, a CTO stared at a 350K RMB cloud bill and asked: “Where did all this money go?”
Nobody could answer.
120 ECS instances, 3 K8s clusters, 50+ SLBs, scattered across two cloud accounts (Alibaba Cloud and Tencent Cloud). Finance only saw the aggregate bill. The engineering team only knew the instances they had created themselves. There was no unified view. Worse, after a full inventory, more than 40% of resources were running at under 30% utilization — idle ECS instances humming along, over-provisioned CPUs wasting cycles, Pods spinning empty during off-peak hours, and SLBs with no backend still billing by the hour.
This is not an isolated case. The FinOps Foundation 2025 annual report shows that enterprises waste 30-35% of cloud spend on average. The problem is not that the cloud is too expensive; it’s that cost is invisible, unattributable, and uncontrollable.
I ran a cloud cost governance initiative at a ride-hailing project before, pulling a runaway monthly bill back onto a track that was predictable, attributable, and optimizable. This article breaks down the 6 key engineering decisions from that practice — from tagging strategy to cost-as-code, from K8s cost breakdown to anomaly detection, with pitfalls and measured data at every step.
一、The Cost Blind Spot: Why Cloud Bills Always Run Out of Control
1.1 Three properties of cloud cost make it inherently uncontrollable
Cloud resources have three characteristics that are fundamentally different from traditional data centers:
| Property | Traditional Data Center | Cloud Platform | Cost Impact |
|---|---|---|---|
| Acquisition speed | 2-4 weeks for procurement approval | 30 seconds via API | Extremely low barrier to provisioning; people spin up instances they don’t need |
| Billing granularity | Asset depreciation, fixed monthly | Per-second/hour, dynamic | Bills fluctuate and are hard to predict |
| Resource ownership | Registered in the asset management system | Creator equals user; tags are optional | Cost attribution is hard; the “big bill” problem |
The FinOps maturity model (from the FinOps Foundation) roughly maps to these stages:
| Stage | Name | Typical Behavior | Adoption in the Industry |
|---|---|---|---|
| Crawl | Bill-first | Wait for the bill to arrive, then reconcile passively | ~60% |
| Walk | Proactive monitoring | Deploy Kubecost/OpenCost and watch trends daily | ~25% |
| Run | Cost tied to business | Allocate cost by team/product, set budget alerts | ~12% |
| Fly | Automated optimization | Auto-scaling based on load, Spot scheduling, idle resource recycling | ~3% |
Most teams are stuck at “export the bill to Excel and reconcile at month end.” The problem is that by the time the bill arrives, the money is already spent. Reconciliation only records history; it cannot change the outcome.
Real FinOps pushes cost control forward — tag resources at creation time, embed budget gates in the deployment pipeline, and detect anomalies at runtime in real time. This is not a finance department job; it’s an engineering team job.
If you’ve read SRE’s Perspective on FinOps: Cloud Cost Visibility and Optimization Strategy, that article focused on the theoretical framework of FinOps and the SRE role. This one focuses on engineering implementation — the complete chain from tags to code to alerting.
二、Tagging Strategy: The First Gate of Cost Attribution
2.1 Why tags are the foundation of FinOps
Cost attribution on cloud platforms entirely depends on tags. Without tags, you only see a “big bill” — you know you spent 350K, but not how much each team, product line, or environment spent.
When I took over cost governance at the ride-hailing project, the first thing I did was inventory the existing tags. The results were alarming:
| Problem | Count | Impact |
|---|---|---|
| ECS without tags | 47/120 instances | 39% of compute cost unattributable |
| Inconsistent tags (case, spelling) | 23 instances | Fragmented attribution data, cannot aggregate |
| Missing business-line tag | 68 instances | Cannot allocate cost by product line |
| Missing environment tag | 52 instances | Cannot separate production/testing/development cost |
47 out of 120 instances had no tags — the creators had probably left the company long ago, and no one knew what those machines were running.
2.2 Tag taxonomy design: 5 mandatory dimensions
Based on practice, I recommend a minimal viable tag taxonomy:
# Mandatory tags (5 dimensions)
Owner = "Zhang San" # Person responsible; you know who to call when something breaks
Team = "payment" # Team, used for cost allocation
Project = "order-service" # Project / product line
Environment = "production" # Environment: production/staging/development
CostCenter = "CC-001" # Cost center, linked to the finance system
Why these 5 and not more? If you define too many tags, nobody fills them in and the system becomes dead weight. These 5 dimensions exactly cover “who is responsible, which team, what project, what environment, how finance books it” — fewer breaks attribution, more raises the cost of compliance.
2.3 Enforcing the tagging policy
Defining tags is useless without enforcement. On Alibaba Cloud, use Tag Policy; on Tencent Cloud, use Tag Association. The core logic is the same: no tag, no resource creation.
#!/usr/bin/env python3
"""
Tag compliance check script: scans all ECS instances and checks whether
the mandatory tags are present. Used as a resource creation gate in CI/CD
pipelines, or as a scheduled inspection.
"""
import json
import sys
from alibabacloud_ecs20140526.client import Client as EcsClient
from alibabacloud_tea_openapi.models import Config
REQUIRED_TAGS = ["Owner", "Team", "Project", "Environment", "CostCenter"]
def check_tag_compliance(access_key_id, access_key_secret, region_id="cn-hangzhou"):
"""Scan tag compliance for all ECS instances in the specified region"""
config = Config(
access_key_id=access_key_id,
access_key_secret=access_key_secret,
region_id=region_id,
endpoint=f"ecs.{region_id}.aliyuncs.com"
)
client = EcsClient(config)
# Get all instances
instances = client.describe_instances(
describe_instances_request={}
).body.instances.instance
violations = []
for inst in instances:
instance_id = inst.instance_id
instance_name = inst.instance_name
existing_tags = {tag.tag_key: tag.tag_value for tag in (inst.tags.tag or [])}
missing = [t for t in REQUIRED_TAGS if t not in existing_tags]
if missing:
violations.append({
"instance_id": instance_id,
"instance_name": instance_name,
"missing_tags": missing,
"existing_tags": existing_tags
})
return violations
if __name__ == "__main__":
violations = check_tag_compliance(
access_key_id=sys.argv[1],
access_key_secret=sys.argv[2]
)
if violations:
print(f"[FAIL] Found {len(violations)} instances with non-compliant tags:")
for v in violations:
print(f" {v['instance_id']} ({v['instance_name']}): missing {v['missing_tags']}")
sys.exit(1)
else:
print("[PASS] All instances have compliant tags")
2.4 Pitfall: the case-sensitivity trap of tags
A real pitfall: Alibaba Cloud tag keys are case-sensitive — Team and team are two different tags. In one project, resources belonging to the same team were tagged in two different cases, so cost attribution split them into two entries, and the report showed that team’s cost as half of what it actually was.
The fix is tag standardization — normalize to UpperCamelCase at the creation entry point, and clean up existing tags in one pass:
# Batch fix tag case (Alibaba Cloud CLI example)
# First list all tags and find instances that need fixing
aliyun ecs DescribeTags --RegionId cn-hangzhou | \
jq '.Tags.Tag[] | select(.TagKey | test("team"))' | \
jq -r '.InstanceId'
# For each instance, delete the old tag and add the standardized one
for instance_id in $(aliyun ecs DescribeInstances --RegionId cn-hangzhou | \
jq -r '.Instances.Instance[] | select(.Tags.Tag[]? | .TagKey == "team") | .InstanceId'); do
# Delete the lowercase tag
aliyun ecs RemoveTags --ResourceType instance --ResourceId "$instance_id" \
--Tag.1.Key team --RegionId cn-hangzhou
# Read the value of the lowercase tag
tag_value=$(aliyun ecs DescribeInstances --InstanceIds "[\"$instance_id\"]" | \
jq -r '.Instances.Instance[0].Tags.Tag[] | select(.TagKey == "team") | .TagValue')
# Add the uppercase tag
aliyun ecs AddTags --ResourceType instance --ResourceId "$instance_id" \
--Tag.1.Key Team --Tag.1.Value "$tag_value" --RegionId cn-hangzhou
done
The cleanup took 2 days and fixed tags on 23 instances. After that, we enforced tag case normalization at both the cloud console and API gateway layers to prevent recurrence.
三、Kubernetes Cost Breakdown: From “Big Bill” to “Itemized Bill”
3.1 The K8s cost black box
One of the hardest parts of cloud cost governance is Kubernetes. A cluster runs dozens of microservices, and the cloud bill only shows cluster-level charges (nodes, storage, load balancers). How much each namespace, Pod, or service costs is invisible on the bill.
This is the classic “big bill” problem — the whole cluster costs 80K a month, but you don’t know whether the order service spent 30K or 5K.
At the ride-hailing project, we had 3 K8s clusters with 120+ microservices. The tech center’s monthly cloud bill was 200K, all charged to the tech center, while business departments had zero visibility into their costs. The CTO demanded that costs be broken down by business line so that each business line owner knew exactly what they were spending.
3.2 OpenCost: the native K8s cost metric standard
OpenCost is a CNCF sandbox project open-sourced by the Kubecost team, and it’s currently the de facto standard for K8s cost measurement. Its core value: break down cluster costs by namespace, Pod, tag, Service, and other dimensions, turning the “big bill” into an “itemized bill.”
How OpenCost works:
┌─────────────────────────────────────────────────────┐
│ OpenCost Architecture │
├─────────────────────────────────────────────────────┤
│ │
│ Prometheus ──→ OpenCost ──→ Cost data │
│ (metrics) (cost engine) (API + UI) │
│ │
│ Inputs: │
│ • node-exporter → node price, specs │
│ • kube-state-metrics → Pod resource requests │
│ • cAdvisor → actual resource usage │
│ • cloud pricing API → node unit price │
│ │
│ Outputs: │
│ • namespace cost │
│ • Pod-level cost │
│ • tag-dimension cost │
│ • resource efficiency (usage vs requests) │
│ │
└─────────────────────────────────────────────────────┘
Deploying OpenCost is simple, provided you already have Prometheus:
# Install OpenCost (requires an existing Prometheus)
kubectl apply -f https://raw.githubusercontent.com/opencost/opencost/develop/kubernetes/opencost.yaml -n opencost
# Verify the deployment
kubectl get pods -n opencost
# Expected output: opencost 1/1 Running
# Port-forward to access the UI
kubectl port-forward -n opencost service/opencost 9003 9090
# Visit http://localhost:9090 for the cost dashboard
3.3 Namespace-level cost allocation
OpenCost’s core capability is splitting cost by namespace. But namespace alone is not enough — multiple business lines can run inside the same namespace. Truly usable cost allocation requires combining namespaces with tags:
# OpenCost custom cost allocation rules
# Business-line cost attribution via Pod labels
apiVersion: v1
kind: ConfigMap
metadata:
name: opencost-config
namespace: opencost
data:
# Allocate cost by labels
costLabels: "team,project,env"
# Node price overrides (required for self-hosted IDC or non-cloud environments)
# Otherwise OpenCost fetches node prices automatically via the cloud API
pricing: |
{
"defaultCPUHourlyCost": 0.0,
"defaultRAMHourlyCost": 0.0,
"customPricesEnabled": true
}
Once Pods carry the team, project, and env labels, OpenCost can aggregate cost along these dimensions:
# Pod label example: cost attribution labels
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
template:
metadata:
labels:
app: order-service
team: payment # team
project: order-system # project
env: production # environment
3.4 Measured data: before and after cost breakdown
We ran a cost breakdown at the ride-hailing project, with these results:
| Business line | Monthly cost (before breakdown) | Monthly cost (after breakdown) | Utilization | Status |
|---|---|---|---|---|
| Order system | 200K (all) | 72K | 62% | Reasonable |
| Payment system | — | 48K | 71% | Reasonable |
| User center | — | 31K | 45% | Low |
| Log service | — | 28K | 28% | Severe waste |
| Test environment | — | 21K | 15% | Severe waste |
The breakdown exposed two cost black holes:
- Log service cost 28K/month at 28% utilization — the Elasticsearch cluster was over-provisioned; 4 of 6 nodes had CPU < 10% year-round
- Test environment cost 21K/month at 15% utilization — it ran 24/7, but was actually only used during working hours (9:00-21:00)
These two alone accounted for 24.5% of total cost — and only 40% of the allocated resources were actually needed.
For the fundamentals of K8s resource management, see Kubernetes Resource Management: Requests and Limits — understanding the relationship between requests and limits is a prerequisite for K8s cost optimization.
四、Resource Right-sizing: From Over-provisioning to Precise Fit
4.1 Over-provisioning is the biggest hidden waste
Right-sizing means adjusting cloud resource specs to match actual load. It is the highest ROI optimization in FinOps — no architecture changes, no code changes, just spec adjustments.
The ECS resource inventory at the ride-hailing project:
| Spec problem | Instances | Monthly waste | Typical symptom |
|---|---|---|---|
| CPU over-provisioning | 35 | 52K | 8-core instances at CPU < 10% year-round |
| Memory over-provisioning | 28 | 38K | 32G of RAM holding 4G of data |
| Oversized disks | 15 | 8K | 500G SSD with 30G used |
| Total | 78 | 98K | 28% of total bill |
78 instances had spec problems, wasting 98K per month. This is not a “should we save” question; it’s a “should we spend at all” question.
4.2 The right-sizing decision framework: smaller is not always better
Right-sizing is not simply swapping large specs for small ones. The core is finding the balance between “cost” and “performance headroom.”
My decision framework:
| Metric | Threshold | Action |
|---|---|---|
| CPU utilization P95 < 15% | Sustained 7 days | Downsize 50% |
| CPU utilization P95 15-30% | Sustained 7 days | Downsize 30% |
| CPU utilization P95 30-60% | Sustained 7 days | Keep as-is |
| CPU utilization P95 > 60% | Sustained 7 days | Upgrade or scale out |
| Memory utilization < 30% | Sustained 7 days | Reduce memory 50% |
| Memory utilization > 80% | Sustained 3 days | Scale up immediately |
Why P95 instead of the average? Averages hide spikes — an instance with a 20% average CPU may spike to 80%. If you downsize based on the average alone, it will buckle under the spike. P95 covers 95% of the time window, leaving reasonable headroom.
4.3 Automated right-sizing inspection script
Manually inspecting 120 instances is unrealistic. I wrote an automated script that periodically scans and generates right-sizing recommendations:
#!/usr/bin/env python3
"""
ECS right-sizing inspection: generates downsizing recommendations
based on CloudMonitor metrics.
Dependencies: alibabacloud_cloudmonitor20190801
"""
import json
from datetime import datetime, timedelta
from alibabacloud_cloudmonitor20190801.client import Client as CmsClient
from alibabacloud_tea_openapi.models import Config
def get_cpu_utilization(client, instance_id, days=7):
"""Get the CPU utilization P95 of an instance over the past N days"""
end_time = datetime.now()
start_time = end_time - timedelta(days=days)
period = 300 # one data point every 5 minutes
response = client.describe_metric_list(
describe_metric_list_request={
"MetricName": "CPUUtilization",
"Dimensions": json.dumps({"instanceId": instance_id}),
"StartTime": start_time.strftime("%Y-%m-%d %H:%M:%S"),
"EndTime": end_time.strftime("%Y-%m-%d %H:%M:%S"),
"Period": str(period)
}
)
values = []
for datapoint in response.body.datapoints:
dp = json.loads(datapoint) if isinstance(datapoint, str) else datapoint
if "Average" in dp:
values.append(dp["Average"])
if not values:
return None
# Compute P95
values.sort()
p95_index = int(len(values) * 0.95)
return values[p95_index]
def generate_rightsizing_report(client, instances):
"""Generate a right-sizing recommendation report"""
report = []
for inst in instances:
instance_id = inst["instance_id"]
instance_type = inst["instance_type"]
cpu_p95 = get_cpu_utilization(client, instance_id)
if cpu_p95 is None:
continue
if cpu_p95 < 15:
suggestion = "Downsize 50%"
priority = "High"
elif cpu_p95 < 30:
suggestion = "Downsize 30%"
priority = "Medium"
elif cpu_p95 < 60:
suggestion = "Keep as-is"
priority = "Low"
else:
suggestion = "Upgrade or scale out"
priority = "Urgent"
report.append({
"instance_id": instance_id,
"instance_type": instance_type,
"cpu_p95": round(cpu_p95, 1),
"suggestion": suggestion,
"priority": priority
})
return report
if __name__ == "__main__":
config = Config(
access_key_id="YOUR_AK",
access_key_secret="YOUR_SK",
endpoint="metrics.cn-hangzhou.aliyuncs.com"
)
cms_client = CmsClient(config)
# Instance list (actually fetched from the ECS API)
instances = [
{"instance_id": "i-bp1xxx", "instance_type": "ecs.c5.2xlarge"},
{"instance_id": "i-bp1yyy", "instance_type": "ecs.r5.large"},
]
report = generate_rightsizing_report(cms_client, instances)
print(f"{'InstanceID':<20} {'Spec':<20} {'CPU P95':<10} {'Suggestion':<15} {'Priority'}")
print("-" * 80)
for r in report:
print(f"{r['instance_id']:<20} {r['instance_type']:<20} {r['cpu_p95']:<10} {r['suggestion']:<15} {r['priority']}")
4.4 Executing downsizing: gray release first, never all at once
Downsizing is not just changing a spec. Downsizing in production carries risk — changing specs requires stopping and restarting, and if you cut too deep, the instance may not survive the restart under traffic.
My execution flow:
- Downsize the test environment first: verify the downsized instance can pass the full test suite
- Downsize production one instance at a time: only 1-2 instances at a time, observe for 24 hours with no anomalies before continuing
- Keep a rollback path: record the original spec before downsizing; restore immediately if anything goes wrong
- Choose a maintenance window: execute during the business off-peak (2-5 AM)
We hit a pitfall during one downsizing: after an 8-core instance was reduced to 4 cores, CPU utilization jumped from 12% to 85% — because that instance was running a scheduled batch job that did a full data sync for 1 hour every night at 2 AM. Low average CPU does not mean the instance doesn’t need the compute; the peak during the batch window is what matters.
Lesson: right-sizing must look not only at steady-state utilization, but also at periodic peaks. For bursty workloads like batch jobs, scheduled tasks, and backup windows, P95 won’t catch them — you need to look at P99 or the absolute peak.
五、Committed Discounts and Elastic Strategy: Combining RI/SP with Spot
5.1 On-demand is the most expensive option
Cloud providers typically offer three billing models:
| Billing model | Discount | Best for | Risk |
|---|---|---|---|
| On-demand | 1.0x (baseline) | Short-term testing, bursty load | None |
| Reserved Instances (RI) / Savings Plans (SP) | 0.4-0.6x | Long-term stable load | 1-3 year commitment, upfront payment |
| Spot instances | 0.1-0.3x | Fault-tolerant batch processing | Can be reclaimed at any time |
For the same 8-core 32G instance, on-demand costs about 1200 RMB/month, RI about 540 RMB, and Spot about 180 RMB. The gap is huge.
5.2 Layered committed discount strategy
Not every workload suits the same billing model. I recommend a layered combination:
| Workload type | Characteristics | Recommended model | Share |
|---|---|---|---|
| Core databases | 7x24 stable, must not be interrupted | RI 3-year prepaid | ~30% |
| Core services | 7x24 stable, brief interruption acceptable | SP 1-year monthly | ~40% |
| Elastic scaling layer | Highly volatile traffic | On-demand + Spot mix | ~20% |
| Batch/CI | Fault-tolerant, interruptible | Spot pure bidding | ~10% |
This ratio is not pulled out of thin air. At the ride-hailing project, our cost structure was:
- RI covering the core database layer (30% of resources) at a 55% discount, saving 42K per month
- SP covering the core service layer (40% of resources) at a 40% discount, saving 31K per month
- Spot covering CI/CD runners and batch processing (10% of resources) at an 80% discount, saving 8K per month
- On-demand as the elasticity fallback (20% of resources), no discount but flexible
Combined, committed discounts and Spot drove the monthly bill from 350K down to around 260K — a 25% reduction.
5.3 The Spot trap: exit gracefully when reclaimed
The biggest risk with Spot instances is that they can be reclaimed at any time. When cloud provider resources are tight, you get 2-5 minutes of warning, then the instance is forcibly reclaimed.
If you run long-lived connection services directly on Spot, a reclamation means broken connections, failed requests, and degraded user experience. The right approach:
- Run only stateless services on Spot instances, keep stateful data in persistent storage
- Configure graceful shutdown hooks: after receiving a reclamation notice, stop accepting new requests, finish in-flight requests, then exit
- Use K8s Spot node pools + PDB to ensure only a limited number of Pods are reclaimed at once
# K8s Spot node pool PodDisruptionBudget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: spot-service-pdb
namespace: production
spec:
minAvailable: 2 # keep at least 2 Pods available
selector:
matchLabels:
node-role: spot
---
# Deployment on Spot nodes
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-processor
spec:
replicas: 6
template:
spec:
nodeSelector:
node-role: spot
tolerations:
- key: "spot-instance"
operator: "Exists"
containers:
- name: processor
image: registry.example.com/processor:v1.0
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# Graceful exit: deregister from the load balancer, wait for in-flight requests to finish
curl -s http://lb-controller/deregister
sleep 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
One production incident: a CI Runner on a Spot instance was mid-build when it was reclaimed. The build was interrupted, the artifacts were corrupted, and the deployment failed. We later added a checkpoint/resume mechanism for build jobs — if interrupted mid-build, resume from the last cached step instead of starting over. This change took the CI Spot interruption rate from “fails every time” to “occasionally costs an extra 2 minutes to recover.”
六、Cost-as-Code: Embedding Budget Gates into CI/CD
6.1 Discovering a sky-high bill after deployment is too late
The ultimate goal of FinOps is not post-hoc optimization but prevention. The ideal scenario: at commit time, you see how much this change will add to cloud cost, and if it exceeds the budget, the deployment is blocked.
Infracost is currently the most mature IaC cost estimation tool, supporting Terraform, Pulumi, and more. At the PR stage, it scans infrastructure code changes, estimates the cost difference, and displays it in a PR comment.
6.2 Integrating Infracost into CI/CD
# GitLab CI integration with Infracost cost estimation
# .gitlab-ci.yml
stages:
- validate
- cost-check
- plan
- apply
infracost-cost-check:
stage: cost-check
image: infracost/infracost:latest
variables:
INFRACOST_API_KEY: $INFRACOST_API_KEY
script:
# Generate the cost estimate report
- infracost breakdown --path . --format json --out-file infracost.json
# Generate a diff report (vs. the main branch baseline)
- infracost diff --path . --compare-to infracost-base.json --format json --out-file infracost-diff.json
# Extract the monthly cost change
- |
MONTHLY_COST=$(jq -r '.projects[0].breakdown.totalMonthlyCost // 0' infracost.json)
COST_DIFF=$(jq -r '.projects[0].diff.totalMonthlyCost // 0' infracost-diff.json)
echo "Monthly cost of this change: ${MONTHLY_COST} USD"
echo "Change vs. baseline: ${COST_DIFF} USD"
# Budget gate: monthly increase over 5000 USD requires approval
THRESHOLD=5000
if (( $(echo "${COST_DIFF} > ${THRESHOLD}" | bc -l) )); then
echo "⚠️ Cost increase exceeds threshold (${THRESHOLD} USD/month), requires architect approval"
exit 1
fi
artifacts:
paths:
- infracost.json
- infracost-diff.json
only:
- merge_requests
6.3 A three-tier cost gate design
Cost estimation alone is not enough. In practice, I designed a three-tier cost gate:
| Tier | Gate rule | Trigger action | Approver |
|---|---|---|---|
| L1: PR comment | All PRs containing IaC changes | Auto-comment with cost estimate | None (informational) |
| L2: Budget alert | Monthly increase > 2000 USD | Block auto-merge | Architect approval |
| L3: Hard gate | Monthly increase > 5000 USD, or new instance > 1000 USD/month | Block deployment | CTO approval |
L1 is the information layer — it tells developers “how much this PR will cost.” Most developers have no idea that changing one line of Terraform can cost thousands of dollars. After seeing the cost comment, many proactively adjust their specs.
L2 is the approval layer — exceeding budget requires an architect to confirm necessity. This isn’t about blocking business; it’s about making decisions traceable.
L3 is the hard gate — large cost increases require CTO sign-off. It prevents incidents like “opened 10 GPU instances in the test environment and forgot to shut them down.”
For the layered design of CI/CD pipelines, I covered the architecture decisions of our self-built DAG scheduling engine in Don’t Let Jenkins Dictate Your Release Rhythm. Cost gates are one dimension of pipeline design, complementary to release rhythm and rollback strategy.
七、Cost Anomaly Detection: From Monthly Reconciliation to Real-time Discovery
7.1 The lag problem with traditional reconciliation
The biggest problem with monthly reconciliation is lag — by the time you spot an anomaly at month end, the money has been spent for a full month.
One incident: a developer opened a 32-core 128G GPU instance in the test environment to train a model, and forgot to shut it down after finishing. The instance ran for 18 days before being discovered, adding 12K to the bill. With monthly reconciliation, it would have taken until month end at best.
7.2 The cost anomaly detection design
My cost anomaly detection design has two layers:
Layer one: budget alerts
Set a budget threshold in the cloud platform; trigger an alert when daily spend exceeds 1/30 of the budget:
#!/usr/bin/env python3
"""
Daily spend alert: checks whether today's cloud spend exceeds the daily average budget.
Fetches real-time spend via the Alibaba Cloud BSS (billing center) API.
"""
import json
from alibabacloud_bssopenapi20171214.client import Client as BssClient
from alibabacloud_tea_openapi.models import Config
def check_daily_cost(access_key_id, access_key_secret, monthly_budget=280000):
"""
Check whether today's spend is abnormal.
monthly_budget: monthly budget (RMB), daily budget = monthly_budget / 30
"""
config = Config(
access_key_id=access_key_id,
access_key_secret=access_key_secret,
endpoint="business.aliyuncs.com"
)
client = BssClient(config)
# Query this month's bill
from alibabacloud_bssopenapi20171214.models import QueryBillRequest
import datetime
now = datetime.datetime.now()
response = client.query_bill(
query_bill_request=QueryBillRequest(
billing_cycle=f"{now.year}-{now.month:02d}"
)
)
# Sum up today's spend
daily_cost = 0
for item in response.body.data.account_bill_list.items:
if item.billing_date == now.strftime("%Y-%m-%d"):
daily_cost += float(item.pretax_amount)
daily_budget = monthly_budget / 30
ratio = daily_cost / daily_budget
if ratio > 1.5:
level = "CRITICAL"
elif ratio > 1.2:
level = "WARNING"
elif ratio > 1.0:
level = "NOTICE"
else:
level = "OK"
return {
"daily_cost": round(daily_cost, 2),
"daily_budget": round(daily_budget, 2),
"ratio": round(ratio, 2),
"level": level
}
if __name__ == "__main__":
result = check_daily_cost("YOUR_AK", "YOUR_SK", monthly_budget=280000)
print(json.dumps(result, indent=2, ensure_ascii=False))
# Send an alert via DingTalk/Feishu bot when level is CRITICAL
Layer two: day-over-day anomaly detection
Even more sensitive than budgets is day-over-day detection — compare today’s cost against the average of the past 7 days; alert if it suddenly spikes. The problem with budget alerts is that you have to know the budget first. But many cost anomalies are unexpected events (an instance mistakenly created, Spot prices surging), not a wrong budget.
def detect_cost_anomaly(client, instance_ids, days=7):
"""
Day-over-day anomaly detection: compares today's cost with the daily
average of the past N days. If today's cost exceeds 200% of the N-day
average, it is flagged as an anomaly.
"""
import datetime
now = datetime.datetime.now()
costs = []
for i in range(days + 1):
date = (now - datetime.timedelta(days=i)).strftime("%Y-%m-%d")
daily_cost = get_daily_cost(client, date)
costs.append({"date": date, "cost": daily_cost})
today_cost = costs[0]["cost"]
avg_cost = sum(c["cost"] for c in costs[1:]) / days
if avg_cost > 0 and today_cost > avg_cost * 2:
return {
"anomaly": True,
"today_cost": today_cost,
"avg_cost": round(avg_cost, 2),
"increase_ratio": round(today_cost / avg_cost, 2),
"message": f"Today's cost {today_cost} RMB exceeds the {days}-day average of {round(avg_cost, 2)} RMB by {round(today_cost / avg_cost, 2)}x"
}
return {"anomaly": False, "today_cost": today_cost, "avg_cost": round(avg_cost, 2)}
7.3 Automated idle resource inspection
Beyond cost anomalies, idle resources need periodic inspection. This inspection logic can run as a scheduled job:
#!/usr/bin/env python3
"""
Idle resource inspection: scans low-CPU ECS instances, SLBs without backends,
and unattached cloud disks. Runs daily; results are sent via DingTalk/Feishu.
"""
import json
import datetime
def scan_idle_ecs(client, threshold=5, days=7):
"""Scan ECS instances whose CPU utilization stays below the threshold"""
idle_instances = []
# Get all running instances
instances = client.describe_instances(
describe_instances_request={"status": "Running"}
).body.instances.instance
for inst in instances:
cpu_avg = get_cpu_avg(client, inst.instance_id, days)
if cpu_avg is not None and cpu_avg < threshold:
idle_instances.append({
"instance_id": inst.instance_id,
"instance_name": inst.instance_name,
"instance_type": inst.instance_type,
"cpu_avg": round(cpu_avg, 1),
"idle_days": days,
"monthly_cost": estimate_monthly_cost(inst.instance_type),
"suggestion": "Recommended to release or downsize"
})
return idle_instances
def scan_idle_slb(client):
"""Scan SLB instances with no backend servers"""
idle_slbs = []
slbs = client.describe_load_balancers(
describe_load_balancers_request={}
).body.load_balancers.load_balancer
for slb in slbs:
# Get the backend server list
backends = client.describe_health_status(
describe_health_status_request={
"load_balancer_id": slb.load_balancer_id
}
).body.backend_servers.backend_server
if not backends:
idle_slbs.append({
"slb_id": slb.load_balancer_id,
"slb_name": slb.load_balancer_name,
"address": slb.address,
"monthly_cost": 30, # SLB base fee is about 30 RMB/month
"suggestion": "No backend servers, recommended to release"
})
return idle_slbs
def scan_unattached_disks(client):
"""Scan unattached cloud disks"""
idle_disks = []
disks = client.describe_disks(
describe_disks_request={"status": "Available"} # Available = unattached
).body.disks.disk
for disk in disks:
idle_disks.append({
"disk_id": disk.disk_id,
"disk_name": disk.disk_name,
"size_gb": disk.size,
"disk_type": disk.category,
"create_time": disk.creation_time,
"monthly_cost": estimate_disk_monthly(disk.size, disk.category),
"suggestion": "Unattached disk, recommended to release or attach"
})
return idle_disks
def generate_report(idle_ecs, idle_slbs, idle_disks):
"""Generate the inspection report"""
total_waste = sum(i["monthly_cost"] for i in idle_ecs) + \
sum(s["monthly_cost"] for s in idle_slbs) + \
sum(d["monthly_cost"] for d in idle_disks)
report = f"""
Idle resource inspection report ({datetime.date.today()})
Idle ECS: {len(idle_ecs)} instances, ~{sum(i['monthly_cost'] for i in idle_ecs)} RMB wasted per month
Idle SLB: {len(idle_slbs)} instances, ~{sum(s['monthly_cost'] for s in idle_slbs)} RMB wasted per month
Unattached disks: {len(idle_disks)} disks, ~{sum(d['monthly_cost'] for d in idle_disks)} RMB wasted per month
Total monthly waste: {total_waste} RMB
"""
if total_waste > 10000:
report += "\n⚠️ Monthly waste exceeds 10K RMB, recommended to handle immediately!"
return report
After running this inspection for 3 months at the ride-hailing project, we discovered and reclaimed:
- 23 idle ECS instances, saving 34K/month
- 8 SLBs without backends, saving 240 RMB/month
- 11 unattached disks, saving 1650 RMB/month
- 42 over-provisioned instances downsized, saving 63K/month
Total: 99K saved per month — nearly 300K over 3 months.
Summary
Runaway cloud cost is not a technical problem; it’s an engineering management problem. The core contradiction: the barrier to provisioning cloud resources is extremely low (an instance in 30 seconds via API), but the feedback loop for cost control is extremely long (the bill arrives at month end). The essence of FinOps is compressing that feedback loop from months to days, even to hours.
Priority and expected returns of the 6 decisions:
| Decision | Implementation difficulty | Time to effect | Monthly benefit (estimate) | Priority |
|---|---|---|---|---|
| Tagging strategy & attribution | Medium | 1-2 weeks | Foundational (no direct savings, but supports everything else) | P0 |
| K8s cost breakdown | Medium | 1 week | Uncovers waste | P1 |
| Resource right-sizing | Low | Immediate | Downsizing saves money directly | P1 |
| Committed discount layering | Low | Immediate | 20-25% cost reduction | P2 |
| Cost-as-code gates | High | 2-4 weeks | Preventive (avoids new waste) | P2 |
| Anomaly detection & alerting | Medium | 1 week | Preventive (avoids sustained waste) | P1 |
Key takeaways from practice:
- Tags are the foundation — without tag attribution, every downstream optimization is blind. Spend the first 2 weeks building the tag taxonomy; sharpening the axe doesn’t delay the chopping
- Right-sizing is the highest-ROI move — no architecture changes, no code changes, just spec adjustments with immediate results
- Committed discounts are “savings without engineering” — RI/SP need no technical changes, but you must predict load accurately to commit the right amount
- Cost-as-code is the endgame — embed cost control into the CI/CD pipeline to stop waste at the source, not remediate after the fact
- Anomaly detection is not optional — without real-time detection, idle resources quietly accumulate, and by the time you notice, tens of thousands have already leaked
FinOps is not a one-off optimization project; it’s continuous operations. Cloud resources change, load changes, prices change, and cost governance must change with them. Getting this system running, bringing the monthly bill from 350K down to 170K, is not the finish line — continuous optimization is the steady state of FinOps.
References & Acknowledgments
This article references the following materials during writing; thanks to the original authors for their contributions:
- FinOps 云成本优化架构实战:从资源浪费到成本可控 — CSDN, practical guide to the three FinOps elements and maturity model
- What Is Cloud Cost Optimization? Strategy & Best Practices — Oracle official, cloud cost optimization methodology and best practices
- FinOps Solutions – Cloud Cost Optimization — Microsoft Azure official, FinOps solution framework
- 云成本治理实战:AI 弹性伸缩与闲置资源回收,月费直降 38% — Tencent Cloud developer community, quantitative analysis of cloud waste and automated governance practice
- 云成本进化之路:从ITAM到FinOps,再到FOCUS — Zhihu, evolution of three generations of cost management models and the FOCUS unified billing spec
- OpenCost部署指南:Kubernetes环境中的无缝集成方案 — CSDN, OpenCost deployment and configuration practice in K8s
- Kubecost 开源 OpenCost:Kubernetes 成本监控的开源标准 — Tencent Cloud developer community, OpenCost design philosophy and the CNCF ecosystem
- FinOps Foundation 2025 Annual Report — FinOps Foundation, source of the 30-35% average cloud waste statistic