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

Best Practice: Workload Time-Shifting und Carbon-aware Scheduling

Kontext

Batch-Jobs, Report-Generierung, ML-Training und ETL-Pipelines haben typischerweise flexible Ausführungszeitfenster — sie müssen bis zu einem Zeitpunkt fertig sein, aber der genaue Start-Zeitpunkt ist verhandelbar.

Genau diese Flexibilität ist ein Sustainability-Hebel: Stromnetze haben unterschiedliche Carbon Intensität zu verschiedenen Tageszeiten. Nachts (22:00–06:00 Uhr) ist in vielen Regionen die Carbon Intensity geringer als tagsüber, besonders in Regionen mit hohem Solar-Anteil. Time-Shifting kann 20–60% der Batch-Emissionen reduzieren ohne Änderungen am Code oder der Business-Logik.

Zugehörige Controls

Zielbild

  • Alle Batch-Workloads ohne Latenz-SLA auf 22:00–06:00 UTC geplant

  • EventBridge Flexible Windows für alle nicht-latenz-sensitiven Jobs

  • Carbon-Intensity-API für dynamisches Scheduling der wichtigsten Batch-Pipelines

  • Scheduling-Entscheidungen für alle Major-Batch-Jobs dokumentiert (Carbon-Rationale)

Technische Umsetzung

Schritt 1: Batch-Workloads inventarisieren und klassifizieren

# Alle EventBridge Schedules auflisten und Zeitplan prüfen
aws scheduler list-schedules \
  --query "Schedules[].[Name,ScheduleExpression,State]" \
  --output table

# Cron-Jobs in Lambda/EventBridge auf Peak-Zeiten prüfen
aws events list-rules \
  --query "Rules[?State=='ENABLED' && contains(ScheduleExpression,'cron')].[Name,ScheduleExpression,Description]" \
  --output table
# Klassifizierungsmatrix für Batch-Workloads
workloads:
  - name: "nightly-report-generation"
    current_schedule: "cron(0 8 * * ? *)"  # 08:00 UTC – Peak!
    latency_requirement: "before_business_start"
    recommended_window: "cron(0 22 * * ? *)"  # 22:00 UTC – Off-Peak
    carbon_saving_estimate: "~40%"

  - name: "weekly-analytics-aggregation"
    current_schedule: "cron(0 9 ? * MON *)"  # Montag 09:00 UTC
    latency_requirement: "available_by_monday_noon"
    recommended_window: "cron(0 2 ? * MON *)"  # Montag 02:00 UTC
    carbon_saving_estimate: "~50%"

  - name: "ml-model-training"
    current_schedule: "on-demand"
    latency_requirement: "within_24h"
    recommended_window: "carbon-aware-dynamic"
    carbon_saving_estimate: "~60% with API integration"

Schritt 2: EventBridge Flexible Windows implementieren

# Nightly Report: Off-Peak + Flexible Window
resource "aws_scheduler_schedule" "nightly_report" {
  name       = "nightly-report-generator"
  group_name = "sustainability"

  # Start um 22:00 UTC – Flexible Window erlaubt bis 00:00 UTC
  schedule_expression          = "cron(0 22 * * ? *)"
  schedule_expression_timezone = "UTC"

  flexible_time_window {
    mode                      = "FLEXIBLE"
    maximum_window_in_minutes = 120  # +/- 2 Stunden Fenster
  }

  target {
    arn      = aws_lambda_function.report_generator.arn
    role_arn = aws_iam_role.scheduler_exec.arn

    retry_policy {
      maximum_event_age_in_seconds = 86400  # 24h retry window
      maximum_retry_attempts       = 3
    }
  }

  tags = {
    purpose   = "sustainability-scheduling"
    control   = "WAF-SUS-060"
    workload  = "reporting"
  }
}

# Wöchentliche Analytics: Sonntag Nacht statt Montag Morgen
resource "aws_scheduler_schedule" "weekly_analytics" {
  name = "weekly-analytics-aggregation"

  schedule_expression          = "cron(0 1 ? * SUN *)"  # Sonntag 01:00 UTC
  schedule_expression_timezone = "UTC"

  flexible_time_window {
    mode                      = "FLEXIBLE"
    maximum_window_in_minutes = 180  # 3h Fenster
  }

  target {
    arn      = aws_sfn_state_machine.analytics_pipeline.arn
    role_arn = aws_iam_role.scheduler_exec.arn
  }
}

Schritt 3: AWS Batch auf Off-Peak mit Spot-Instanzen

# Batch Compute Environment: Graviton Spot-Instanzen
resource "aws_batch_compute_environment" "ml_training" {
  compute_environment_name = "ml-training-green"
  type                     = "MANAGED"
  state                    = "ENABLED"

  compute_resources {
    type                = "SPOT"
    allocation_strategy = "SPOT_CAPACITY_OPTIMIZED"

    min_vcpus     = 0
    max_vcpus     = 256
    desired_vcpus = 0

    instance_type = [
      "m7g",   # Graviton3 – alle Sizes
      "c7g",
      "r7g",
    ]

    spot_iam_fleet_role = aws_iam_role.spot_fleet.arn
    instance_role       = aws_iam_instance_profile.batch.arn
    subnets             = var.private_subnets
    security_group_ids  = [aws_security_group.batch.id]

    tags = {
      purpose     = "ml-training"
      environment = "production"
    }
  }

  tags = {
    sustainability = "spot-graviton"
    control        = "WAF-SUS-060"
  }
}

# Job Queue mit Time-Based Priority: Night = höhere Priorität
resource "aws_batch_job_queue" "night_priority" {
  name     = "night-priority-queue"
  state    = "ENABLED"
  priority = 100  # Höhere Priorität für Nacht-Submissions

  compute_environment_order {
    order               = 1
    compute_environment = aws_batch_compute_environment.ml_training.arn
  }
}

Schritt 4: Carbon-Intensity-API für dynamisches Scheduling

# carbon_aware_scheduler.py
# Integriert electricityMaps API für dynamisches Carbon Scheduling

import requests
import boto3
import json
from datetime import datetime, timedelta

class CarbonAwareScheduler:
    """
    Plant Batch-Jobs für den Zeitpunkt mit niedrigster Carbon Intensity
    innerhalb eines definierten Ausführungsfensters
    """

    def __init__(self, em_api_key: str, region_zone: str = "EU"):
        self.em_api_key = em_api_key
        self.region_zone = region_zone
        self.base_url = "https://api.electricitymap.org/v3"

    def get_carbon_forecast(self, hours_ahead: int = 24) -> list:
        """Holt Carbon Intensity Forecast für die nächsten N Stunden"""
        response = requests.get(
            f"{self.base_url}/carbon-intensity/forecast",
            params={"zone": self.region_zone},
            headers={"auth-token": self.em_api_key}
        )
        return response.json().get("forecast", [])

    def find_optimal_window(
        self,
        deadline: datetime,
        duration_minutes: int,
        not_before: datetime = None
    ) -> datetime:
        """
        Findet das optimale Ausführungsfenster basierend auf Carbon Intensity.

        Args:
            deadline: Spätester Fertigstellungszeitpunkt
            duration_minutes: Geschätzte Laufzeit des Jobs
            not_before: Frühestmöglicher Startzeit

        Returns:
            Optimaler Startzeitpunkt mit niedrigster Carbon Intensity
        """
        forecast = self.get_carbon_forecast(hours_ahead=48)
        now = datetime.utcnow()
        not_before = not_before or now

        best_time = None
        best_intensity = float('inf')

        for point in forecast:
            point_time = datetime.fromisoformat(point['datetime'].replace('Z', '+00:00'))
            point_time = point_time.replace(tzinfo=None)

            # Innerhalb des erlaubten Fensters?
            if point_time < not_before:
                continue
            if point_time + timedelta(minutes=duration_minutes) > deadline:
                break

            intensity = point['carbonIntensity']
            if intensity < best_intensity:
                best_intensity = intensity
                best_time = point_time

        print(f"Optimal execution time: {best_time} (intensity: {best_intensity:.0f} gCO2eq/kWh)")
        return best_time

    def schedule_job(self, job_name: str, optimal_time: datetime, job_input: dict):
        """Plant den Job auf den optimalen Zeitpunkt via EventBridge"""
        scheduler = boto3.client('scheduler')

        schedule_expression = (
            f"at({optimal_time.strftime('%Y-%m-%dT%H:%M:%S')})"
        )

        scheduler.create_schedule(
            Name=f"{job_name}-carbon-aware-{optimal_time.strftime('%Y%m%d%H%M')}",
            ScheduleExpression=schedule_expression,
            FlexibleTimeWindow={'Mode': 'FLEXIBLE', 'MaximumWindowInMinutes': 30},
            Target={
                'Arn': f"arn:aws:lambda:{boto3.session.Session().region_name}:...:function:{job_name}",
                'Input': json.dumps(job_input)
            }
        )

# Verwendung:
scheduler = CarbonAwareScheduler(
    em_api_key="your-electricity-maps-api-key",
    region_zone="SE"  # Schweden für eu-north-1
)
optimal = scheduler.find_optimal_window(
    deadline=datetime.utcnow() + timedelta(hours=20),
    duration_minutes=45,
)

Schritt 5: Kubernetes-Batch mit KEDA (Carbon-Aware)

# keda-scaledjob-carbon.yaml
# KEDA ScaledJob für Carbon-Aware Kubernetes Batch Processing
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
  name: analytics-batch-processor
  namespace: sustainability
spec:
  jobTargetRef:
    template:
      spec:
        containers:
        - name: processor
          image: acme/analytics-processor:latest
          # Graviton Node via Node Affinity
          resources:
            requests:
              cpu: "2"
              memory: "4Gi"
        nodeSelector:
          kubernetes.io/arch: arm64   # Graviton/ARM Node
        tolerations:
        - key: "sustainability.io/carbon-optimized"
          operator: "Equal"
          value: "true"
          effect: "NoSchedule"

  # Scaling basierend auf SQS Queue
  triggers:
  - type: aws-sqs-queue
    metadata:
      queueURL: https://sqs.eu-north-1.amazonaws.com/123456789/batch-jobs
      awsRegion: eu-north-1  # Green Region
      targetQueueLength: "5"  # Batch: 5 Messages per Job

  # Nur zwischen 22:00 und 06:00 UTC skalieren
  scalingModifiers:
    target: "1"
    formula: "isWorkingHours(time.Now()) ? 0 : 1"

Typische Fehlmuster

Fehlmuster Problem

"Report muss um 07:00 Uhr fertig sein → läuft um 06:45"

Report um 22:00 starten, bis 00:30 fertig, 6,5 Stunden Off-Peak → gleiche Verfügbarkeit, 40% weniger CO₂.

"Wir können keine Flex-Windows nutzen, weil Abhängigkeiten bestehen"

Abhängigkeiten zwischen Jobs werden durch Flex-Windows meist nicht verletzt — prüfen ob Abhängigkeit wirklich strikt zeitlich oder nur logisch ist.

"Carbon-Intensity-APIs sind zu komplex für uns"

Für die meisten Fälle reicht statisches Off-Peak-Scheduling. Carbon-API-Integration ist Level-4-Maßnahme, nicht Voraussetzung.

Metriken

  • Anteil Batch-Jobs auf Off-Peak (%): (Ziel: >80% aller nicht-latenz-sensitiven Jobs)

  • Flexible-Window-Aktivierung (%): Jobs mit Flexible Time Windows konfiguriert

  • Geschätzte CO₂-Einsparung durch Time-Shifting (tCO₂e): Quantifiziert gegen Baseline

Reifegrad

Level 1 – Batch läuft zu Entwickler-Convenience-Zeiten; keine Off-Peak-Strategie
Level 2 – Einzelne große Jobs manuell auf Nacht verschoben
Level 3 – Alle Flex-Jobs auf 22:00–06:00 UTC; EventBridge Flexible Windows
Level 4 – Carbon-Intensity-API für dynamisches Scheduling
Level 5 – Temporal + Spatial Shifting kombiniert; Carbon-Savings dokumentiert und in SCI