WAF++ WAF++
Back to WAF++ Homepage

Best Practice: Idle-Ressourcen identifizieren und eliminieren

Kontext

Studien zeigen konsistent: 30–45% aller Cloud-Compute-Ressourcen laufen mit weniger als 10% CPU-Auslastung. Eine EC2-Instanz, die bei 3% CPU läuft, verbraucht nahezu dieselbe Energie wie eine bei 80% — aber liefert nur einen Bruchteil des Nutzens.

Non-Production-Umgebungen (Dev, Test, Staging) verschärfen das Problem: Sie laufen in vielen Organisationen 24/7, obwohl ihr tatsächlicher Nutzungszeitraum nur 30–40% der Kalenderzeit ausmacht (Geschäftszeiten an Werktagen). Das bedeutet, 60–70% der Non-Prod-Energie wird für wartende, ungenutzte Infrastruktur verbraucht.

Zugehörige Controls

  • WAF-SUS-040 – Idle & Underutilized Resource Elimination

  • WAF-SUS-060 – Workload Scheduling & Time-Shifting

Zielbild

  • Keine EC2-Instanz läuft > 14 Tage mit < 5% CPU-Auslastung ohne Review

  • Non-Production-Umgebungen stoppen außerhalb der Geschäftszeiten automatisch

  • Autoscaling mit Scale-to-Zero für alle zustandslosen Workloads

  • Quarterly Zombie-Resource-Hunt: detached EBS, stale Snapshots, leere ASGs

  • Spot-Instanzen für >50% der Non-Production- und Batch-Compute-Last

Technische Umsetzung

Schritt 1: Idle-Ressourcen identifizieren

# AWS Compute Optimizer Recommendations auflisten
aws compute-optimizer get-ec2-instance-recommendations \
  --filters "name=Finding,values=Underprovisioned,Overprovisioned,Optimized,NotOptimized" \
  --query "instanceRecommendations[?finding=='Underprovisioned' || finding=='Overprovisioned'].[
    instanceArn,
    finding,
    utilizationMetrics[?name=='CPU'].value|[0],
    recommendationOptions[0].instanceType
  ]" \
  --output table

# CloudWatch: Alle EC2-Instanzen mit CPU < 5% in den letzten 14 Tagen
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-XXXXX \
  --start-time $(date -d "14 days ago" -u +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 86400 \
  --statistics Average \
  --query 'Datapoints[].Average'

# Terraform: CloudWatch Alarm für Idle-Detection
resource "aws_cloudwatch_metric_alarm" "idle_instance" {
  for_each = toset(var.monitored_instance_ids)

  alarm_name          = "idle-detection-${each.value}"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 14   # 14 Tage
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 86400 # 1 Tag
  statistic           = "Average"
  threshold           = 5     # < 5% CPU
  alarm_description   = "WAF-SUS-040: Instance may be idle. Review for stop/terminate."

  dimensions = {
    InstanceId = each.value
  }

  alarm_actions = [aws_sns_topic.sustainability_alerts.arn]

  tags = {
    purpose = "sustainability-idle-detection"
    control = "WAF-SUS-040"
  }
}

Schritt 2: Non-Production Scheduled Shutdown

# AWS Systems Manager Maintenance Window für Scheduled Shutdown
resource "aws_ssm_maintenance_window" "non_prod_shutdown" {
  name              = "non-prod-shutdown"
  schedule          = "cron(0 19 ? * MON-FRI *)"  # 19:00 Uhr Mo-Fr
  duration          = 1
  cutoff            = 0
  allow_unassociated_targets = false

  tags = {
    purpose = "non-prod-scheduled-shutdown"
    control = "WAF-SUS-040"
  }
}

# Oder einfachere Variante mit EventBridge + Lambda
resource "aws_cloudwatch_event_rule" "stop_non_prod" {
  name                = "stop-non-prod-instances"
  description         = "Stop all non-production EC2 instances evenings and weekends"
  schedule_expression = "cron(0 18 ? * MON-FRI *)"  # 18:00 UTC Montag-Freitag
}

resource "aws_cloudwatch_event_rule" "start_non_prod" {
  name                = "start-non-prod-instances"
  description         = "Start non-production EC2 instances on weekday mornings"
  schedule_expression = "cron(0 7 ? * MON-FRI *)"   # 07:00 UTC Montag-Freitag
}

resource "aws_lambda_function" "instance_scheduler" {
  function_name = "non-prod-instance-scheduler"
  runtime       = "python3.12"
  architectures = ["arm64"]
  handler       = "scheduler.handler"
  role          = aws_iam_role.instance_scheduler.arn
  filename      = "scheduler.zip"

  environment {
    variables = {
      TAG_KEY   = "environment"
      TAG_VALUE = "development,staging"  # Stop diese Umgebungen
    }
  }
}
# scheduler.py – Lambda für Scheduled Stop/Start
import boto3
import os

def handler(event, context):
    ec2 = boto3.client('ec2')
    action = event.get('action', 'stop')  # 'stop' oder 'start'

    tag_key = os.environ['TAG_KEY']
    tag_values = os.environ['TAG_VALUE'].split(',')

    # Alle Instanzen mit dem Tag finden
    response = ec2.describe_instances(
        Filters=[
            {
                'Name': f'tag:{tag_key}',
                'Values': tag_values
            },
            {
                'Name': 'instance-state-name',
                'Values': ['running'] if action == 'stop' else ['stopped']
            }
        ]
    )

    instance_ids = [
        instance['InstanceId']
        for reservation in response['Reservations']
        for instance in reservation['Instances']
    ]

    if not instance_ids:
        print(f"No instances to {action}")
        return

    if action == 'stop':
        ec2.stop_instances(InstanceIds=instance_ids)
        print(f"Stopped {len(instance_ids)} non-production instances")
    else:
        ec2.start_instances(InstanceIds=instance_ids)
        print(f"Started {len(instance_ids)} non-production instances")

Schritt 3: Zombie-Ressourcen aufspüren

#!/bin/bash
# zombie-hunt.sh – Findet und reportiert ungenutzte Ressourcen

echo "=== Zombie Resource Hunt ==="

# 1. Unattached EBS Volumes
echo "--- Unattached EBS Volumes ---"
aws ec2 describe-volumes \
  --filters "Name=status,Values=available" \
  --query "Volumes[].[VolumeId,Size,CreateTime,AvailabilityZone]" \
  --output table

# 2. Unassigned Elastic IPs
echo "--- Unassigned Elastic IPs ---"
aws ec2 describe-addresses \
  --query "Addresses[?AssociationId==null].[AllocationId,PublicIp]" \
  --output table

# 3. Leere Load Balancer (keine Targets)
echo "--- Load Balancers with no registered targets ---"
aws elbv2 describe-load-balancers \
  --query "LoadBalancers[].[LoadBalancerArn,LoadBalancerName,CreatedTime]" \
  --output json | jq -r '.[] | @tsv' | while read ARN NAME CREATED; do
    TARGETS=$(aws elbv2 describe-target-groups --load-balancer-arn "$ARN" \
      --query "length(TargetGroups)" --output text)
    if [ "$TARGETS" == "0" ]; then
      echo "  $NAME (no target groups): $ARN"
    fi
  done

# 4. Alte EBS Snapshots (> 365 Tage ohne zugehöriges aktives AMI)
echo "--- EBS Snapshots older than 365 days ---"
aws ec2 describe-snapshots \
  --owner-ids self \
  --query "Snapshots[?StartTime<='$(date -d '365 days ago' +%Y-%m-%d)'].[SnapshotId,VolumeId,StartTime,Description]" \
  --output table

Schritt 4: Autoscaling mit Scale-to-Zero

# Autoscaling mit Target Tracking für stateless Services
resource "aws_autoscaling_group" "api_servers" {
  name                = "api-servers"
  vpc_zone_identifier = var.private_subnets
  min_size            = 1     # Production: minimum 1 für HA
  max_size            = 20
  desired_capacity    = 2

  mixed_instances_policy {
    instances_distribution {
      on_demand_base_capacity                  = 1
      on_demand_percentage_above_base_capacity = 30
      spot_allocation_strategy                 = "capacity-optimized"
    }
    launch_template {
      launch_template_specification {
        launch_template_id = aws_launch_template.api.id
        version            = "$Latest"
      }
      override { instance_type = "m7g.medium" }
      override { instance_type = "m7g.large" }
      override { instance_type = "c7g.medium" }
    }
  }

  lifecycle {
    ignore_changes = [desired_capacity]
  }
}

# Target Tracking: CPU-basiertes Autoscaling
resource "aws_autoscaling_policy" "cpu_target" {
  name                   = "cpu-target-tracking"
  autoscaling_group_name = aws_autoscaling_group.api_servers.name
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ASGAverageCPUUtilization"
    }
    target_value = 65.0  # 65% CPU als Zielauslastung
  }
}

Typische Fehlmuster

Fehlmuster Problem

"Non-Prod muss immer laufen, weil Entwickler in verschiedenen Zeitzonen arbeiten"

Lösung: Zeitzone-basiertes Scheduling (Team in Berlin → 07:00–20:00 CET); oder Self-Service-Start durch Developer.

"Autoscaling ist zu komplex für unsere Anwendung"

Managed Services (Lambda, Fargate, Cloud Run) sind Zero-Config Scale-to-Zero. Kein eigenes Autoscaling erforderlich.

"Wir wissen nicht, wem die ungenutzten Ressourcen gehören"

Fehlende Tags sind Sustainability-Governance-Problem. Zombie-Hunt macht den Handlungsbedarf sichtbar.

"EBS Volumes brauchen wir vielleicht noch"

Detached Volumes werden nie mehr genutzt — Backup-Snapshot erstellen, dann löschen.

Metriken

  • Idle-Instance-Anteil (%): EC2-Instanzen mit < 10% CPU im 14-Tage-Schnitt

  • Non-Prod-Betriebszeiten-Reduktion (%): Stunden gespart durch Scheduled Shutdown

  • Zombie-Resource-Count: unattached EBS Volumes + unused EIPs + leere ASGs (Ziel: 0)

  • Autoscaling-Coverage (%): Anteil stateless Compute mit konfiguriertem Autoscaling

Reifegrad

Level 1 – Kein Monitoring; Non-Prod läuft 24/7; kein Autoscaling
Level 2 – Manuelle Cleanup-Kampagnen; punktuelles Autoscaling
Level 3 – Idle-Detection-Alerts; Scheduled Shutdown; Autoscaling überall
Level 4 – >50% Spot für Non-Prod; Scale-to-Zero; Quarterly Zombie-Hunt
Level 5 – Zero idle Compute; alle Ressourcen demand-driven; Emissionsreduktion documentiert