Skip to content

08 · FinOps & Enterprise Cost Optimization

Level 3's cost module covered budgets and tagging for one account. FinOps at enterprise scale is the ongoing practice built on top of that visibility: reserved capacity decisions, anomaly detection, showback/chargeback across teams, and rightsizing as a recurring process rather than a one-time task.

Enterprise-wide cost visibility

ibmcloud enterprise account-group-usage-report \
  --account-group-id $(ibmcloud enterprise account-group product-teams --output json | jq -r .id) \
  --billing-month 2026-08
Account                Charges (USD)
orders-prod             4,812.33
orders-dev                642.10
catalog-prod             2,109.87

This is the enterprise-level view that Module 01's account-group structure makes possible — one report spans every account under product-teams without needing per-account login.

Showback and chargeback with cost tags

ibmcloud billing account-usage --output json | \
  jq -r '.resources[] | select(.tags[]? | startswith("team:")) |
    "\(.tags[] | select(startswith("team:")))\t\(.charges)"' | \
  awk -F'\t' '{sum[$1]+=$2} END {for (t in sum) print t, sum[t]}'
team:orders 6,891.20
team:catalog 2,109.87

Showback (reporting spend per team without billing them internally) is the easy first step; chargeback (actually debiting a team's internal budget) needs the tag discipline from Level 3, Module 07 enforced without exception — one untagged large resource breaks the whole report's accuracy silently.

Detect cost anomalies before the monthly bill does

ibmcloud billing account-usage --output json > usage-current.json

python3 - <<'EOF'
import json
with open("usage-current.json") as f:
    data = json.load(f)
for r in data["resources"]:
    if r["charges"] > r.get("last_month_charges", 0) * 1.5 and r["charges"] > 50:
        print(f"ANOMALY: {r['resource_name']} jumped to ${r['charges']:.2f}")
EOF
ANOMALY: Databases for PostgreSQL jumped to $312.40

A 50%+ month-over-month jump on a specific service is usually one of a few causes: a forgotten dev resource left running at production size, an autoscaler's max-size set too high and actually being hit, or a genuine traffic increase — the anomaly check's job is to surface it quickly, not diagnose which cause it is.

Reserved capacity and committed-use discounts

ibmcloud billing offering-list --output json | jq '.[] | select(.type=="subscription")'

For steady-state workloads whose sizing has stabilized (the opposite of Module 05's autoscaled traffic-following workloads), a committed-use agreement with IBM trades flexibility for a lower rate — check actual utilization trends over at least a full quarter before committing, since a mis-sized reservation costs more than paying list price for variable usage.

Rightsizing as a recurring job, not a one-time audit

oc adm top pods -A --no-headers | awk '{print $1, $2, $3}' | sort -k3 -n
ibmcloud is instances --output json | \
  jq -r '.[] | select(.profile.name | test("bx2-8x32|bx2-16x64")) | .name'

Cross-reference instances provisioned at large profiles against their actual ibmcloud ob monitoring CPU/memory utilization over the trailing 30 days (Level 4, Module 05's baseline habit, applied as a recurring report instead of a one-off check) — a bx2-16x64 instance sitting at 8% average CPU for a month is a rightsizing candidate, not a capacity decision that was necessarily wrong when originally made.

ibmcloud is instance-update prod-batch-worker --profile bx2-4x16
Updating instance profile requires a reboot. Continue? [y/N]

Idle and orphaned resource cleanup

ibmcloud is floating-ips --output json | jq '.[] | select(.target == null) | .address'
203.0.113.44

An unattached floating IP still bills — a common, easy-to-miss leftover from a deleted instance whose floating IP wasn't explicitly released. Schedule a monthly sweep (a scripted check, not a manual click-through) for unattached floating IPs, unattached block storage volumes, and snapshots past their intended retention.

FinOps dashboard: pull it together

resource "ibm_resource_instance" "cost_dashboard_functions" {
  name              = "finops-anomaly-check"
  service           = "functions"
  plan              = "lite"
  location          = "us-south"
  resource_group_id = data.ibm_resource_group.mastery_path.id
}

A scheduled Code Engine job or Cloud Function (from Level 1's serverless module) running the anomaly-detection script monthly and posting results to a chat channel turns this module's manual commands into an actual recurring FinOps practice rather than something remembered only when the bill is surprising.

terraform validate
# Success! The configuration is valid.

Gotchas

  • Showback numbers are only as good as tag coverage — a single large, untagged resource silently understates every team's real spend in a showback report; audit tag coverage percentage as its own metric.
  • Committed-use discounts lock in a minimum spend — committing based on a temporary traffic spike (rather than sustained baseline) can leave an account paying for capacity it no longer uses once the spike passes.
  • Instance profile changes need a reboot — rightsizing a running production instance is a planned-maintenance-window activity, not something to script as a silent background change.
  • Free-tier and lite-plan resources still show as $0 in billing but count against account-level quotas — a cost review focused only on billed dollars can miss quota exhaustion building up on the free side.

How It Actually Works

  • The enterprise account-group usage report is possible only because billing data is rolled up along the same account-group hierarchy tree Module 01 built, not recomputed separately — every account's metering records already carry their account ID, and the enterprise's billing system aggregates by walking the account-group parentage from Module 01 the way an org chart aggregates headcount. That's why the account-group structure decision made at provisioning time directly determines what cost views are possible later without custom tooling.
  • An unattached floating IP keeps billing because a floating IP is a reserved, routable public address allocated against the account's address pool independent of whatever instance it's currently bound to — detaching it from a deleted instance doesn't release the allocation itself, it just clears the target field; the IP is still held in reserve (so it can be immediately re-attached to a replacement instance) and billed for exactly that reservation, not for traffic or compute. Only an explicit release call actually frees it back to IBM's pool and stops the charge.
  • A month-over-month anomaly script works because metering records accumulate into per-service-instance monthly totals that the billing API exposes as a queryable time series, not just a final invoice number. Comparing charges against last_month_charges per resource is directly reading two adjacent points on that time series — the same underlying data source Level 3's budget alerts poll, just compared resource-by-resource instead of against one aggregate threshold, which is why this script catches a single service's spike even when total account spend still looks unremarkable.
  • Instance profile resizing needs a reboot because vCPU and memory allocation are properties of the underlying VSI's hypervisor placement, not something the guest OS can renegotiate live — changing profile requires the VPC infrastructure layer to deallocate the instance's current compute resources and reschedule it onto a host slot matching the new profile's shape, which is a stop/reallocate/start cycle from the hypervisor's perspective regardless of how briefly the CLI command appears to run.

Cheat sheet

Task Command
Enterprise usage report ibmcloud enterprise account-group-usage-report --account-group-id <id> --billing-month <YYYY-MM>
List unattached floating IPs ibmcloud is floating-ips --output json \| jq '.[] \| select(.target == null)'
Resize an instance profile ibmcloud is instance-update <name> --profile <profile>
List subscription/reserved offerings ibmcloud billing offering-list
View pod resource usage cluster-wide oc adm top pods -A

Exercise

  1. Write a script (Python or jq) that flags any resource whose current month charge exceeds 150% of the prior month.
  2. Compute a showback total per team: tag from a sample billing usage JSON export.
  3. Find (or simulate) an unattached floating IP and describe the cleanup steps and their cost impact.
  4. Given a hypothetical instance sitting at 8% average CPU for a month, propose a rightsized profile and describe the maintenance-window process for applying it safely.