Take control of cloud spending with FinOps. Covers tagging strategy, cost allocation, budget alerts, rightsizing, reserved capacity, and organizational alignment.
Cloud financial management, or FinOps, is the practice of aligning cloud spending with business outcomes through continuous visibility, accountability, and control. It matters when cloud costs grow faster than application growth, when teams over-provision resources, and when budgeting is reactive rather than strategic. This guide details how to implement FinOps in a cloud-native engineering environment using real tools and real workflows—no invented abstractions.
Start by collecting cloud spending data across services, accounts, regions, and projects. Use a combination of native provider APIs and standard tooling to gather cost data from infrastructure, PaaS, and SaaS layers.
Use curl to fetch billing data from cloud providers, with standard HTTP authentication and pagination.
curl \
-H "Authorization: Bearer ${AWS_ACCESS_TOKEN}" \
-H "Accept: application/json" \
"https://billing.aws.amazon.com/v1/billing?from=2023-10-01&to=2023-10-31"
Responses are JSON with nextPageToken for pagination. Use jq to extract line items:
curl -H "Authorization: Bearer ${GCP_PROJECT_TOKEN}" \
"https://billing.googleapis.com/v1/billingAccounts/123456789/billingPeriods?pageSize=100" \
| jq -r '.billingPeriods[] | select(.billingPeriod == "2023-10") | .lineItems[] |
.description + " | " + .cost | .cost'
Use kubectl to orchestrate a pipeline that fetches, normalizes, and loads cost data into a central database.
apiVersion: batch/v1
kind: Job
metadata:
name: fetch-cost-data
spec:
template:
spec:
containers:
- name: cost-fetcher
image: alpine:latest
command:
- sh
- -c
- |
# Fetch AWS costs
curl -s -H "Authorization: Bearer $AWS_TOKEN" \
"https://billing.aws.amazon.com/v1/billing?from=$START_DATE&to=$END_DATE" \
| jq -c '.results[] | { service: .service, cost: .cost, account: .account, region: .region }' \
| tee /data/aws-costs.json
# Fetch GCP costs
curl -s -H "Authorization: Bearer $GCP_TOKEN" \
"https://billing.googleapis.com/v1/billingAccounts/$BILLING_ID/billingPeriods?pageSize=100" \
| jq -c '.billingPeriods[] | select(.billingPeriod == "$PERIOD") | .lineItems[] |
{ service: .description, cost: .cost, account: .account, region: .region }' \
| tee /data/gcp-costs.json
# Merge and write to database
cat /data/aws-costs.json /data/gcp-costs.json \
| jq -c 'reduce .[] as $item ([]; . + [$item])' \
| psql -c "COPY cost_data FROM STDIN WITH (FORMAT JSON)"
env:
- name: AWS_TOKEN
valueFrom:
secretKeyRef:
name: cloud-auth-secrets
key: aws-billing-token
- name: GCP_TOKEN
valueFrom:
secretKeyRef:
name: cloud-auth-secrets
key: gcp-billing-token
- name: START_DATE
value: "2023-10-01"
- name: END_DATE
value: "2023-10-31"
- name: BILLING_ID
value: "123456789"
- name: PERIOD
value: "2023-10"
volumeMounts:
- name: cost-data
mountPath: /data
volumes:
- name: cost-data
emptyDir: {}
restartPolicy: OnFailure
This pipeline runs daily and outputs a cost_data table with columns: service, cost, account, region, timestamp.
Assign costs to teams, projects, and applications using tags, labels, and metadata. Tag all cloud resources with a consistent schema:
{
"project": "user-auth",
"team": "identity",
"environment": "production",
"owner": "[email protected]"
}
Use aws ec2 describe-instances with --filters to extract tags:
aws ec2 describe-instances \
--filters "Name=tag:project,Values=user-auth" \
--query 'Reservations[].Instances[].{InstanceId: InstanceId, Tags: Tags}' \
--output json
Then use jq and psql to enrich cost data:
psql -c "
WITH instance_tags AS (
SELECT
i.instance_id,
jsonb_object_agg(t.key, t.value) AS tags
FROM instances i
JOIN instance_tags t ON i.id = t.instance_id
GROUP BY i.instance_id
)
INSERT INTO cost_data (service, cost, account, region, project, team, environment)
SELECT
cd.service,
cd.cost,
cd.account,
cd.region,
COALESCE(it.tags->>'project', 'unknown'),
COALESCE(it.tags->>'team', 'unknown'),
COALESCE(it.tags->>'environment', 'unknown')
FROM cost_data cd
JOIN instance_tags it ON cd.instance_id = it.instance_id
ON CONFLICT (timestamp, service, account, region) DO UPDATE
SET cost = EXCLUDED.cost, project = EXCLUDED.project, team = EXCLUDED.team;
"
Assign ownership of cloud spend to teams and projects, and set up regular cost reviews.
Use pgAdmin, Grafana, or Superset to build a real-time dashboard. Query cost data with psql:
SELECT
team,
SUM(cost) AS total_cost,
COUNT(*) AS resource_count,
AVG(cost) AS avg_cost_per_resource
FROM cost_data
WHERE timestamp >= NOW() - INTERVAL '30 days'
GROUP BY team
ORDER BY total_cost DESC;
Use the same query to generate a monthly cost report:
SELECT
DATE_TRUNC('month', timestamp) AS month,
team,
SUM(cost) AS monthly_cost,
LAG(SUM(cost)) OVER (PARTITION BY team ORDER BY DATE_TRUNC('month', timestamp)) AS previous_month_cost
FROM cost_data
GROUP BY month, team
ORDER BY month DESC, team;
Set up alerts when team spend exceeds budget:
SELECT
team,
SUM(cost) AS current_month_cost,
budget,
(SUM(cost) - budget) AS variance
FROM cost_data
JOIN cost_budgets ON cost_data.team = cost_budgets.team
WHERE timestamp >= DATE_TRUNC('month', NOW())
AND timestamp < DATE_TRUNC('month', NOW()) + INTERVAL '1 month'
GROUP BY team, budget
HAVING SUM(cost) > budget;
Use curl with a monitoring system (like Prometheus or Datadog) to trigger alerts.
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"title": "Team Cloud Budget Exceeded",
"message": "Team marketing exceeded their $12k budget with $13.4k in spend.",
"tags": ["finops", "budget", "alert"],
"priority": "high"
}' \
https://api.datadog.com/v1/alerts
Define cost allocation rules using a declarative configuration format, independent of any platform.
rules:
- match:
service: "EC2"
environment: "production"
allocate:
to: "team"
from: "tags"
key: "owner"
weight: 0.7
- match:
service: "RDS"
region: "us-west-2"
allocate:
to: "project"
from: "tags"
key: "project"
weight: 0.3
- match:
service: "Lambda"
allocate:
to: "team"
from: "metadata"
key: "team"
weight: 0.5
Apply rules in a cost-allocation service that runs daily:
import json
import psycopg2
from collections import defaultdict
def allocate_costs():
conn = psycopg2.connect("dbname=finops user=finops password=secret")
cursor = conn.cursor()
cursor.execute("SELECT * FROM cost_data")
cost_records = cursor.fetchall()
rules = load_rules("rules.yaml")
allocations = defaultdict(float)
for record in cost_records:
for rule in rules:
if match(record, rule["match"]):
key = lookup_value(record, rule["allocate"])
allocations[key] += record["cost"] * rule["weight"]
for team, cost in allocations.items():
cursor.execute(
"INSERT INTO team_costs (team, cost, month) VALUES (%s, %s, %s)",
(team, cost, date.today().strftime("%Y-%m"))
)
conn.commit()
conn.close()
Proactively reduce cloud spend with automation and feedback.
Use kubectl to manage horizontal pod autoscaling based on cost.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: requests_per_second
target:
type: AverageValue
averageValue: 1000
behavior:
scaleUp:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 600
policies:
- type: Pods
value: 2
periodSeconds: 120
- type: Percent
value: 10
periodSeconds: 300
Use systemctl to run cost optimization scripts on a schedule.
[Unit]
Description=Run cost optimization tasks
After=network.target
[Service]
Type=oneshot
User=finops
WorkingDirectory=/opt/finops
ExecStart=/opt/finops/optimization.py
Environment=AWS_PROFILE=prod
Environment=DATABASE_URL=postgresql://finops:secret@db:5432/finops
[Install]
WantedBy=multi-user.target
# /opt/finops/optimization.py
import boto3
from botocore.exceptions import ClientError
def rightsize_ec2_instances():
ec2 = boto3.client("ec2", region_name="us-east-1")
response = ec2.describe_instances()
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
instance_id = instance["InstanceId"]
current_type = instance["InstanceType"]
current_cpu = instance["CPUUtilization"]
current_memory = instance["MemoryUtilization"]
if current_cpu < 30 and current_memory < 40:
new_type = get_suitable_instance_type(current_type, "compute")
ec2.modify_instance_attribute(
InstanceId=instance_id,
InstanceType={"Value": new_type}
)
log(f"Reduced {instance_id} from {current_type} to {new_type}")
def terminate_idle_resources():
ec2 = boto3.client("ec2")
response = ec2.describe_instances(
Filters=[{"Name": "tag:cost-center", "Values": ["finops"}]
)
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
instance_id = instance["InstanceId"]
idle = is_instance_idle(instance_id)
if idle:
ec2.terminate_instances(InstanceIds=[instance_id])
log(f"Terminated idle instance {instance_id}")
Use psql to detect cost spikes and unusual patterns.
-- Identify cost anomalies using z-score
WITH monthly_costs AS (
SELECT
team,
DATE_TRUNC('month', timestamp) AS month,
SUM(cost) AS total_cost
FROM cost_data
GROUP BY team, month
),
z_scores AS (
SELECT
team,
month,
total_cost,
(total_cost - AVG(total_cost) OVER (PARTITION BY team)) /
STDDEV(total_cost) OVER (PARTITION BY team) AS z_score
FROM monthly_costs
)
SELECT
team,
month,
total_cost,
z_score,
CASE
WHEN z_score > 2 THEN 'High'
WHEN z_score < -2 THEN 'Low'
ELSE 'Normal'
END AS anomaly
FROM z_scores
WHERE ABS(z_score) > 1.5;
FinOps is not a one-time setup but a continuous practice.
Use cron to run a monthly review script.
# /etc/crontab
0 9 1 * * root /opt/finops/run-finops-review.sh
#!/bin/bash
# /opt/finops/run-finops-review.sh
set -e
DATE=$(date +%Y-%m)
# Generate report
psql -c "
COPY (
SELECT
team,
SUM(cost) AS total_cost,
LAG(SUM(cost)) OVER (PARTITION BY team ORDER BY DATE_TRUNC('month', timestamp)) AS prev_cost,
(SUM(cost) - LAG(SUM(cost)) OVER (PARTITION BY team ORDER BY DATE_TRUNC('month', timestamp))) AS delta
FROM cost_data
WHERE timestamp >= DATE_TRUNC('month', '$DATE') - INTERVAL '1 month'
AND timestamp < DATE_TRUNC('month', '$DATE') + INTERVAL '1 month'
GROUP BY team, DATE_TRUNC('month', timestamp)
ORDER BY team
) TO '/reports/finops-$DATE.csv' WITH (FORMAT CSV, HEADER TRUE);
"
# Send email
mutt -s "Monthly FinOps Report - $DATE" \
-a "/reports/finops-$DATE.csv" \
-- [email protected] < /reports/summary-$DATE.txt
Use git to version cost policies and budgets.
# Version cost rules
git add rules.yaml
git commit -m "Increase RDS cost weight for core projects"
git push origin main
Set up a CI pipeline that validates cost rules:
# .github/workflows/validate-cost-rules.yml
name: Validate Cost Rules
on: [push]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate YAML
run: |
for file in rules/*.yaml; do
echo "Validating $file"
yq e '.rules[] | .match | .service, .environment' "$file"
done
- name: Run cost allocation
run: |
python3 -m cost_allocation --config rules.yaml
psql -c "SELECT * FROM team_costs LIMIT 5"
- name: Deploy
run: |
kubectl apply -f k8s/cost-dashboard.yaml
systemctl restart cost-optimization.service
psql to identify gaps: SELECT region, COUNT(*) as count
FROM cost_data
WHERE timestamp >= NOW() - INTERVAL '7 days'
GROUP BY region
HAVING COUNT(*) < 10;
SELECT
service,
region,
COUNT(*) as count,
ARRAY_AGG(DISTINCT project) as projects
FROM cost_data
GROUP BY service, region
HAVING COUNT(*) > 100 AND ARRAY_LENGTH(ARRAY_AGG(DISTINCT project), 1) > 3;
systemctl status and journalctl to monitor budgeting service: journalctl -u cost-budgeting.service -f
Look for ERROR entries with code=500 and message="Failed to update budget for team marketing".
kubectl describe hpa and kubectl get events for scaling triggers. kubectl describe hpa user-service-hpa
kubectl get events --field-selector="involvedObject.name=user-service-hpa"
FinOps succeeds when cost data is consistent, ownership is clear, and optimization is continuous. The key is not just collecting numbers, but using them to drive decisions, and the tools to do so are already in every cloud engineer’s toolkit.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy