Skip to main content
Source

This page is generated from skills/eks-cost-intelligence/references/idle-resources.md. Edit the source, not this page.

Idle Resources

Part of: eks-cost-intelligence Purpose: Checks for zero-replica Deployments idle for extended periods, LoadBalancer Services with no healthy endpoints, namespaces with no running workloads but allocated quotas, orphaned ConfigMaps/Secrets not referenced by running workloads, and per-hour cost of unused load balancers


Overview​

Idle resources is a mid-weight dimension (15 points max deduction). It detects resources that are provisioned and potentially incurring cost but serving no active workload — forgotten deployments scaled to zero, load balancers with no backends, empty namespaces with quotas, and orphaned configuration objects.

Unlike compute efficiency (which identifies under-utilized resources), this dimension targets resources that are doing nothing at all — pure waste with zero productive value.

Cost Reference: Unused Load Balancers​

Load Balancer TypeHourly Cost (idle)Monthly Cost (idle)
Network Load Balancer (NLB)~$0.0225/hr~$16.43/month
Application Load Balancer (ALB)~$0.0225/hr~$16.43/month
Classic Load Balancer (CLB)~$0.025/hr~$18.25/month

Prices are US East (N. Virginia) baseline. Actual costs vary by region but the idle hourly rate applies even with zero traffic.

Checks Summary​

#CheckDefault ThresholdSeverity Logic
1Zero-replica Deployments idle for extended periods0 replicas for > 7 daysBy count × resource allocation
2LoadBalancer Services with no healthy endpoints0 healthy endpointsHIGH (Req 10.5) — per LB hourly cost
3Namespaces with no running workloads but allocated quotas0 running pods + active quotaMEDIUM
4Orphaned ConfigMaps/Secrets not referenced by running workloadsNot mounted/envFrom by any running podLOW–MEDIUM by count

Pre-requisites​

These checks require:

  • kubectl access to the cluster (for Deployments, Services, Endpoints, Namespaces, ConfigMaps, Secrets)
  • AWS CLI access for elasticloadbalancing:DescribeLoadBalancers, elasticloadbalancing:DescribeTargetHealth (optional — enriches LB cost data)
  • No metrics-server required — all checks use Kubernetes API state inspection only

Check 1: Zero-Replica Deployments Idle for Extended Periods​

What it detects​

Deployments that have been scaled to zero replicas and appear to have been in this state for an extended period (> 7 days). These represent forgotten or abandoned workloads that may still have associated resources (PVCs, ConfigMaps, Secrets, ServiceAccounts) consuming cluster overhead.

Data collection​

Via kubectl:

# Find all Deployments with 0 replicas in non-system namespaces
kubectl get deployments --all-namespaces -o json | \
jq -r '
.items[] |
select(.metadata.namespace | test("^kube-|^amazon-|^aws-") | not) |
select(.spec.replicas == 0) |
{
namespace: .metadata.namespace,
name: .metadata.name,
replicas: .spec.replicas,
last_updated: (.metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"] // "unknown"),
generation: .metadata.generation,
conditions: [.status.conditions[]? | {type: .type, lastTransitionTime: .lastTransitionTime}]
}'
# Check when the Deployment was last scaled (using events or annotations)
kubectl get events --all-namespaces --field-selector reason=ScalingReplicaSet \
--sort-by='.lastTimestamp' -o json | \
jq -r '
.items[] |
select(.involvedObject.kind == "Deployment") |
select(.message | contains("Scaled down replica set") and contains(" to 0")) |
"\(.involvedObject.namespace)/\(.involvedObject.name) scaled-to-zero at \(.lastTimestamp)"
' | tail -50
# Alternative: check last transition time on Available condition
kubectl get deployments --all-namespaces -o json | \
jq -r '
.items[] |
select(.metadata.namespace | test("^kube-|^amazon-|^aws-") | not) |
select(.spec.replicas == 0) |
. as $dep |
($dep.status.conditions[]? | select(.type == "Available")) as $cond |
"\($dep.metadata.namespace)/\($dep.metadata.name) | lastTransition=\($cond.lastTransitionTime // "unknown")"
'

Via EKS MCP Server:

list_k8s_resources(
cluster_name="<cluster>",
kind="Deployment",
api_version="apps/v1"
)
# Filter results for spec.replicas == 0
# Check status.conditions[].lastTransitionTime for idle duration

Analysis logic​

For each Deployment in non-system namespaces:
If spec.replicas == 0:
idle_since = status.conditions["Available"].lastTransitionTime
idle_days = (now - idle_since).days

If idle_days > 7:
→ Generate finding

# Estimate residual cost (associated resources still consuming)
associated_pvcs = count PVCs referenced by the Deployment's pod template
associated_secrets = count Secrets referenced by the Deployment's pod template
residual_monthly_cost = sum(pvc_costs) + quota_overhead

Severity classification​

ConditionSeverity
Zero-replica Deployment idle > 30 days with associated PVCsHIGH
Zero-replica Deployment idle > 30 days, no PVCsMEDIUM
Zero-replica Deployment idle 7–30 daysLOW
Multiple (5+) zero-replica Deployments in same namespaceMEDIUM (governance gap)

Remediation​

# Review and delete abandoned Deployments
kubectl delete deployment <name> -n <namespace>

# Or scale back up if it should be running
kubectl scale deployment <name> -n <namespace> --replicas=1

# List all associated resources before cleanup
kubectl get all,pvc,configmap,secret -n <namespace> -l app=<deployment-label>
# Bulk identification: list all zero-replica deploys older than 30 days
kubectl get deployments --all-namespaces -o json | \
jq -r '
.items[] |
select(.metadata.namespace | test("^kube-|^amazon-|^aws-") | not) |
select(.spec.replicas == 0) |
select(.status.conditions[]? |
select(.type == "Available") |
(.lastTransitionTime | fromdateiso8601) < (now - 2592000)) |
"\(.metadata.namespace)/\(.metadata.name)"
'

Check 2: LoadBalancer Services with No Healthy Endpoints​

What it detects​

Kubernetes Services of type LoadBalancer that have no healthy backend endpoints. These services provision AWS load balancers (NLB or ALB) that incur hourly charges (~$0.0225/hr for NLB, ~$0.0225/hr for ALB) even with zero traffic and no healthy targets.

Severity: HIGH (per Requirement 10.5) — Each idle load balancer costs ~$16.43/month minimum regardless of traffic.

Data collection​

Via kubectl:

# Find all LoadBalancer Services
kubectl get services --all-namespaces -o json | \
jq -r '
.items[] |
select(.spec.type == "LoadBalancer") |
{
namespace: .metadata.namespace,
name: .metadata.name,
lb_type: (if .metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-type"] == "nlb"
or .metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-type"] == "external"
then "NLB" else "CLB/ALB" end),
external_ip: (.status.loadBalancer.ingress[0].hostname // "pending"),
selector: .spec.selector
}'
# Check endpoints health for each LoadBalancer Service
kubectl get endpoints --all-namespaces -o json | \
jq -r '
.items[] |
select(.subsets == null or .subsets == [] or
(.subsets[]?.addresses == null or .subsets[]?.addresses == [])) |
{
namespace: .metadata.namespace,
name: .metadata.name,
ready_addresses: 0,
not_ready: ([.subsets[]?.notReadyAddresses[]?] | length)
}'
# Combined: LoadBalancer Services with zero healthy endpoints
kubectl get services --all-namespaces -o json | \
jq -r '
.items[] |
select(.spec.type == "LoadBalancer") |
.metadata.namespace as $ns |
.metadata.name as $name |
"\($ns)/\($name)"
' | while read svc; do
ns=$(echo "$svc" | cut -d'/' -f1)
name=$(echo "$svc" | cut -d'/' -f2)
ready=$(kubectl get endpoints "$name" -n "$ns" -o json 2>/dev/null | \
jq '[.subsets[]?.addresses[]?] | length')
if [ "${ready:-0}" -eq 0 ]; then
echo "IDLE_LB: $svc (0 healthy endpoints)"
fi
done

Via AWS CLI (enriches with actual LB details):

# List ELBv2 load balancers and check target group health
aws elbv2 describe-load-balancers --query 'LoadBalancers[*].{
Name:LoadBalancerName,
ARN:LoadBalancerArn,
Type:Type,
State:State.Code,
DNSName:DNSName
}' --output table

# For each LB, check target group health
aws elbv2 describe-target-groups \
--load-balancer-arn <lb-arn> \
--query 'TargetGroups[*].TargetGroupArn' --output text | \
while read tg_arn; do
echo "Target Group: $tg_arn"
aws elbv2 describe-target-health \
--target-group-arn "$tg_arn" \
--query 'TargetHealthDescriptions[*].{Target:Target.Id,Health:TargetHealth.State}'
done
# List Classic Load Balancers and check instance health
aws elb describe-load-balancers --query 'LoadBalancerDescriptions[*].{
Name:LoadBalancerName,
Instances:Instances[*].InstanceId
}' --output json | \
jq '.[] | select(.Instances == null or (.Instances | length) == 0) | .Name'

Via EKS MCP Server:

list_k8s_resources(
cluster_name="<cluster>",
kind="Service",
api_version="v1"
)
# Filter for spec.type == "LoadBalancer"
# Then check endpoints for each:

list_k8s_resources(
cluster_name="<cluster>",
kind="Endpoints",
api_version="v1",
namespace="<namespace>"
)
# Check subsets[].addresses length for each matching endpoint

Analysis logic​

For each Service with spec.type == "LoadBalancer":
endpoints = get Endpoints object with same name/namespace
healthy_count = count(endpoints.subsets[].addresses[])

If healthy_count == 0:
# Determine LB type from annotations
lb_type = annotations["service.beta.kubernetes.io/aws-load-balancer-type"]

If lb_type in ["nlb", "external", "nlb-ip"]:
hourly_cost = 0.0225 # NLB idle cost
Else if lb_type == "alb" or ingress-controller managed:
hourly_cost = 0.0225 # ALB idle cost
Else:
hourly_cost = 0.025 # CLB idle cost (legacy)

monthly_cost = hourly_cost × 730
→ Generate HIGH severity finding (per Req 10.5)

Severity classification​

ConditionSeverity
Any LoadBalancer Service with 0 healthy endpointsHIGH
Multiple idle LoadBalancers (3+)HIGH (compounding waste)

Per Requirement 10.5: Idle LoadBalancer Services always generate HIGH severity findings because they represent direct, ongoing AWS billing with zero value.

Per-hour cost breakdown​

LB TypeHourly Idle CostMonthly Idle CostAnnual Idle Cost
NLB~$0.0225/hr~$16.43/month~$197.10/year
ALB~$0.0225/hr~$16.43/month~$197.10/year
CLB~$0.025/hr~$18.25/month~$219.00/year

These are base charges. LCU/NLCU charges are zero when there is no traffic, but the hourly fee applies regardless.

Remediation​

# Delete the idle LoadBalancer Service (removes AWS LB)
kubectl delete service <name> -n <namespace>

# Or switch to ClusterIP if the LB is no longer needed externally
kubectl patch service <name> -n <namespace> -p '{"spec": {"type": "ClusterIP"}}'

# Verify the AWS LB was actually deleted after Service removal
aws elbv2 describe-load-balancers --query 'LoadBalancers[*].LoadBalancerName'
# If the Service should have backends, check why pods are unhealthy
kubectl get pods -n <namespace> -l <service-selector-labels>
kubectl describe endpoints <service-name> -n <namespace>

Check 3: Namespaces with No Running Workloads but Allocated Quotas​

What it detects​

Namespaces that have ResourceQuotas or LimitRanges defined but contain no running pods. This indicates abandoned environments that still reserve cluster capacity through quotas or create governance overhead.

Data collection​

Via kubectl:

# Find namespaces with ResourceQuotas
kubectl get resourcequotas --all-namespaces -o json | \
jq -r '.items[] | "\(.metadata.namespace)"' | sort -u
# Find namespaces with zero running pods
kubectl get pods --all-namespaces --field-selector=status.phase=Running -o json | \
jq -r '[.items[].metadata.namespace] | unique[]' > /tmp/active_namespaces.txt

kubectl get namespaces -o json | \
jq -r '.items[].metadata.name' | \
grep -v -F -f /tmp/active_namespaces.txt | \
grep -v -E "^(kube-|amazon-|aws-|default$)"
# Combined: namespaces with quotas but no running workloads
kubectl get namespaces -o json | \
jq -r '
.items[] |
select(.metadata.name | test("^kube-|^amazon-|^aws-|^default$") | not) |
.metadata.name
' | while read ns; do
pod_count=$(kubectl get pods -n "$ns" --field-selector=status.phase=Running \
--no-headers 2>/dev/null | wc -l)
quota_count=$(kubectl get resourcequotas -n "$ns" --no-headers 2>/dev/null | wc -l)
limitrange_count=$(kubectl get limitranges -n "$ns" --no-headers 2>/dev/null | wc -l)

if [ "$pod_count" -eq 0 ] && [ "$((quota_count + limitrange_count))" -gt 0 ]; then
echo "EMPTY_NS_WITH_QUOTA: $ns (quotas=$quota_count, limitranges=$limitrange_count, running_pods=0)"
fi
done
# Get quota details for empty namespaces (shows reserved capacity)
kubectl get resourcequotas -n <namespace> -o json | \
jq '.items[] | {
name: .metadata.name,
hard: .spec.hard,
used: .status.used
}'

Via EKS MCP Server:

list_k8s_resources(
cluster_name="<cluster>",
kind="Namespace",
api_version="v1"
)

list_k8s_resources(
cluster_name="<cluster>",
kind="ResourceQuota",
api_version="v1",
namespace="<namespace>"
)

list_k8s_resources(
cluster_name="<cluster>",
kind="Pod",
api_version="v1",
namespace="<namespace>"
)
# Check for running pods; if zero, flag namespace with quotas

Analysis logic​

For each non-system namespace:
running_pods = count pods with status.phase == "Running"
has_quota = (ResourceQuotas exist in namespace)
has_limitrange = (LimitRanges exist in namespace)

If running_pods == 0 AND (has_quota OR has_limitrange):
# Calculate reserved capacity from quota spec
reserved_cpu = quota.spec.hard["requests.cpu"]
reserved_mem = quota.spec.hard["requests.memory"]

→ Generate finding
# Note: quotas don't directly consume resources, but indicate
# governance overhead and often mean the namespace was intended for workloads

Severity classification​

ConditionSeverity
Empty namespace with quota + no pods for > 30 daysMEDIUM
Empty namespace with quota + associated PVCs still boundHIGH
Empty namespace with quota only (no PVCs)LOW–MEDIUM
Multiple (3+) empty namespaces with quotasMEDIUM (governance gap)

Remediation​

# Review and delete empty namespaces (CAUTION: deletes all resources in namespace)
kubectl delete namespace <namespace>

# Or remove just the quota if the namespace may be reused
kubectl delete resourcequota <quota-name> -n <namespace>
kubectl delete limitrange <limitrange-name> -n <namespace>

# Before deleting, check for any non-running resources (PVCs, Secrets, etc.)
kubectl get all,pvc,configmap,secret,serviceaccount -n <namespace>

Check 4: Orphaned ConfigMaps and Secrets Not Referenced by Running Workloads​

What it detects​

ConfigMaps and Secrets that exist in non-system namespaces but are not referenced by any running pod — either as volume mounts, environment variable sources (envFrom/valueFrom), or projected volumes. These represent configuration debt and potential security exposure (orphaned secrets).

Data collection​

Via kubectl:

# Step 1: Get all ConfigMaps/Secrets referenced by running pods
kubectl get pods --all-namespaces -o json | \
jq -r '
.items[] |
select(.status.phase == "Running") |
select(.metadata.namespace | test("^kube-|^amazon-|^aws-") | not) |
.metadata.namespace as $ns |
(
# Volume mounts (configMap and secret)
([.spec.volumes[]? |
select(.configMap) | "\($ns)/configmap/\(.configMap.name)"] +
[.spec.volumes[]? |
select(.secret) | "\($ns)/secret/\(.secret.secretName)"] +
[.spec.volumes[]? |
select(.projected) | .projected.sources[]? |
(if .configMap then "\($ns)/configmap/\(.configMap.name)"
elif .secret then "\($ns)/secret/\(.secret.name)"
else empty end)] +
# EnvFrom references
[.spec.containers[]?.envFrom[]? |
(if .configMapRef then "\($ns)/configmap/\(.configMapRef.name)"
elif .secretRef then "\($ns)/secret/\(.secretRef.name)"
else empty end)] +
# Individual env valueFrom references
[.spec.containers[]?.env[]?.valueFrom? |
(if .configMapKeyRef then "\($ns)/configmap/\(.configMapKeyRef.name)"
elif .secretKeyRef then "\($ns)/secret/\(.secretKeyRef.name)"
else empty end)]
)[]
' | sort -u > /tmp/referenced_configs.txt
# Step 2: Get all ConfigMaps in non-system namespaces (excluding system ones)
kubectl get configmaps --all-namespaces -o json | \
jq -r '
.items[] |
select(.metadata.namespace | test("^kube-|^amazon-|^aws-") | not) |
select(.metadata.name | test("^kube-root-ca") | not) |
"\(.metadata.namespace)/configmap/\(.metadata.name)"
' | sort > /tmp/all_configmaps.txt

# Step 3: Get all Secrets in non-system namespaces (excluding service account tokens)
kubectl get secrets --all-namespaces -o json | \
jq -r '
.items[] |
select(.metadata.namespace | test("^kube-|^amazon-|^aws-") | not) |
select(.type != "kubernetes.io/service-account-token") |
select(.metadata.name | test("^default-token-") | not) |
"\(.metadata.namespace)/secret/\(.metadata.name)"
' | sort > /tmp/all_secrets.txt
# Step 4: Find orphaned ConfigMaps (not referenced by any running pod)
comm -23 /tmp/all_configmaps.txt /tmp/referenced_configs.txt

# Step 5: Find orphaned Secrets (not referenced by any running pod)
comm -23 /tmp/all_secrets.txt /tmp/referenced_configs.txt
# Simplified single-command approach (summary by namespace)
kubectl get pods --all-namespaces -o json | \
jq -r '
[.items[] |
select(.status.phase == "Running") |
select(.metadata.namespace | test("^kube-|^amazon-|^aws-") | not) |
.metadata.namespace as $ns |
(
[.spec.volumes[]?.configMap?.name // empty] +
[.spec.volumes[]?.secret?.secretName // empty] +
[.spec.containers[]?.envFrom[]?.configMapRef?.name // empty] +
[.spec.containers[]?.envFrom[]?.secretRef?.name // empty] +
[.spec.containers[]?.env[]?.valueFrom?.configMapKeyRef?.name // empty] +
[.spec.containers[]?.env[]?.valueFrom?.secretKeyRef?.name // empty]
)[] | {ns: $ns, ref: .}
] | group_by(.ns) | map({namespace: .[0].ns, referenced: [.[].ref] | unique})
'

Via EKS MCP Server:

# Get all pods to build reference map
list_k8s_resources(
cluster_name="<cluster>",
kind="Pod",
api_version="v1"
)
# Parse spec.volumes, spec.containers[].envFrom, spec.containers[].env[].valueFrom
# to build set of referenced ConfigMaps and Secrets

# Get all ConfigMaps
list_k8s_resources(
cluster_name="<cluster>",
kind="ConfigMap",
api_version="v1",
namespace="<namespace>"
)

# Get all Secrets
list_k8s_resources(
cluster_name="<cluster>",
kind="Secret",
api_version="v1",
namespace="<namespace>"
)
# Diff: all - referenced = orphaned

Exclusions​

Always exclude from orphan detection:

  • ConfigMaps named kube-root-ca.crt (system-managed)
  • Secrets of type kubernetes.io/service-account-token (auto-created)
  • Secrets matching ^default-token- pattern (legacy SA tokens)
  • ConfigMaps/Secrets in system namespaces (kube-*, amazon-*, aws-*)
  • Helm release secrets (type helm.sh/release.v1) — these track Helm state
  • ConfigMaps with label app.kubernetes.io/managed-by: Helm that match a deployed release

Analysis logic​

For each non-system namespace:
referenced_cms = set of ConfigMap names used by running pods (volumes, envFrom, valueFrom)
referenced_secrets = set of Secret names used by running pods (volumes, envFrom, valueFrom)

all_cms = set of all ConfigMaps in namespace (excluding system CMs)
all_secrets = set of all Secrets in namespace (excluding SA tokens, Helm releases)

orphaned_cms = all_cms - referenced_cms
orphaned_secrets = all_secrets - referenced_secrets

If len(orphaned_cms) + len(orphaned_secrets) > 0:
→ Generate finding
# Secrets carry more risk (potential credential exposure)
# ConfigMaps are lower risk (just configuration debt)

Severity classification​

ConditionSeverity
> 10 orphaned Secrets in a namespaceMEDIUM
Orphaned Secrets (any count)LOW
> 20 orphaned ConfigMaps in a namespaceMEDIUM
Orphaned ConfigMaps (any count)LOW
Total orphaned objects across cluster > 50MEDIUM (cluster hygiene)

Note: Orphaned ConfigMaps/Secrets have minimal direct cost impact but indicate configuration debt, potential security exposure (stale credentials in Secrets), and complicate cluster operations.

Remediation​

# Review orphaned ConfigMaps before deleting
kubectl get configmap <name> -n <namespace> -o yaml

# Delete confirmed orphaned ConfigMaps
kubectl delete configmap <name> -n <namespace>

# Delete confirmed orphaned Secrets (review first!)
kubectl get secret <name> -n <namespace> -o yaml | head -20 # Check type and annotations
kubectl delete secret <name> -n <namespace>
# Bulk cleanup: delete all orphaned ConfigMaps in a namespace (use with caution)
# First generate the list, review it, then delete
comm -23 /tmp/all_configmaps.txt /tmp/referenced_configs.txt | \
grep "^<namespace>/" | \
sed 's|.*/configmap/||' | \
xargs -I{} kubectl delete configmap {} -n <namespace> --dry-run=client

# Remove --dry-run=client after confirming the list is correct

Scoring Contribution​

The idle resources dimension has a maximum deduction of 15 points.

Deduction calculation​

deduction = 0

For each finding in this dimension:
If severity == CRITICAL: deduction += 15 × 0.6 = 9.0
If severity == HIGH: deduction += 15 × 0.3 = 4.5
If severity == MEDIUM: deduction += 15 × 0.15 = 2.25
If severity == LOW: deduction += 15 × 0.05 = 0.75

actual_deduction = min(deduction, 15) # Cap at maximum

Dimension status​

ConditionStatus
All checks completedASSESSED
Some checks partially completedASSESSED (with note)
No Kubernetes API accessCannot assess — skill halts at pre-flight

This dimension is never SKIPPED because all checks require only the Kubernetes API (no metrics sources needed).


Decision Tree​

START
│
├─ Check 1: Zero-replica Deployments
│ ├─ List all Deployments with spec.replicas == 0
│ ├─ Check idle duration via status.conditions[].lastTransitionTime
│ ├─ If idle > 7 days → Generate finding (severity by duration + PVCs)
│ └─ Check for associated PVCs still bound
│
├─ Check 2: Idle LoadBalancer Services
│ ├─ List all Services with spec.type == "LoadBalancer"
│ ├─ For each, check Endpoints object for healthy addresses
│ ├─ If 0 healthy endpoints → Generate HIGH severity finding
│ ├─ Calculate per-hour cost: NLB ~$0.0225/hr, ALB ~$0.0225/hr
│ └─ Optionally verify via AWS ELBv2 API for target group health
│
├─ Check 3: Empty Namespaces with Quotas
│ ├─ List namespaces with ResourceQuotas or LimitRanges
│ ├─ For each, count running pods
│ ├─ If 0 running pods + quota exists → Generate finding
│ └─ Check for bound PVCs in empty namespace (escalates severity)
│
├─ Check 4: Orphaned ConfigMaps/Secrets
│ ├─ Build reference map from all running pods
│ ├─ List all ConfigMaps/Secrets (excluding system objects)
│ ├─ Diff: all - referenced = orphaned
│ ├─ Exclude Helm releases, SA tokens, system CMs
│ └─ Generate finding if orphan count exceeds threshold
│
└─ Aggregate findings → Calculate dimension deduction (max 15 pts)

Examples​

Example Finding: Idle Load Balancer​

finding:
id: "idle-lb-staging-api-gateway"
dimension: "idle"
severity: "HIGH"
affected_resource: "staging/api-gateway (Service type=LoadBalancer)"
current_state: "NLB provisioned with 0 healthy endpoints for 14 days"
monthly_cost: 16.43
monthly_waste: 16.43
monthly_savings: 16.43
effort: "Low"
fix_summary: "Delete unused LoadBalancer Service or fix backend pods"
remediation: "kubectl delete service api-gateway -n staging"
confidence: "high"
data_sources: ["kubernetes_api", "aws_elb_api"]

Example Finding: Zero-Replica Deployment​

finding:
id: "idle-deploy-dev-old-backend"
dimension: "idle"
severity: "MEDIUM"
affected_resource: "dev/old-backend (Deployment, 0 replicas since 2024-08-15)"
current_state: "Scaled to 0 replicas for 45 days, 2 PVCs still bound (50Gi total)"
monthly_cost: 5.00
monthly_waste: 5.00
monthly_savings: 5.00
effort: "Low"
fix_summary: "Delete abandoned deployment and associated PVCs"
remediation: "kubectl delete deployment old-backend -n dev && kubectl delete pvc old-backend-data-0 old-backend-data-1 -n dev"
confidence: "medium"
data_sources: ["kubernetes_api"]

Example Finding: Orphaned Secrets​

finding:
id: "idle-orphaned-secrets-production"
dimension: "idle"
severity: "LOW"
affected_resource: "production namespace (8 orphaned Secrets)"
current_state: "8 Secrets not referenced by any running pod"
monthly_cost: 0
monthly_waste: 0
monthly_savings: 0
effort: "Low"
fix_summary: "Review and delete orphaned Secrets to reduce configuration debt"
remediation: "Review each Secret and delete if no longer needed"
confidence: "medium"
data_sources: ["kubernetes_api"]