Salta al contenuto principale

Carbon Aware SDK: Spostare Workload nel Tempo e nello Spazio

Il settore ICT consuma circa 460-700 TWh di elettricità l'anno nei data center globali, una cifra destinata a raddoppiare entro il 2030 spinta…

Cosa ne pensi di questo articolo?

Carbon Aware SDK: Spostare Workload nel Tempo e nello Spazio
Green SoftwareFoto:SiddePexels

In Breve

Il settore ICT consuma circa 460-700 TWh di elettricità l'anno nei data center globali, una cifra destinata a raddoppiare entro il 2030 spinta…

Carbon Aware SDK: Spostare i Workload nel Tempo e nello Spazio

Il settore ICT consuma circa 460-700 TWh di elettricità l'anno nei data center globali, una cifra destinata a raddoppiare entro il 2030 spinta dall'esplosione dei workload AI. Ma non tutta questa energia è uguale: un kilowattora prodotto in un momento in cui la rete è alimentata al 90% da energie rinnovabili causa un decimo delle emissioni CO₂ rispetto a uno prodotto durante un picco di domanda coperto da centrali a gas. La stessa logica vale nello spazio: eseguire un job in una regione cloud europea con un mix energetico pulito può essere quattro volte più verde rispetto a una regione alimentata prevalentemente da carbone.

Questo concetto - eseguire il software quando e dove l'elettricità è più pulita - si chiama demand shifting o carbon-aware computing, ed è uno dei principi fondamentali del green software engineering. La Green Software Foundation ha costruito attorno a questo principio un toolkit open source di riferimento: il Carbon Aware SDK, disponibile su GitHub con licenza MIT e usato in produzione da aziende come UBS, Vestas e Microsoft stessa.

In questo articolo vedremo come funziona il Carbon Aware SDK dall'interno: architettura, sorgenti dati, API REST, integrazione con Python, Kubernetes e GitHub Actions. Costruiremo esempi concreti di time shifting (spostare un job di 6-8 ore quando l'intensità carbonica scende) e location shifting (instradare i workload verso la regione cloud più verde in tempo reale).

Cosa Imparerai

  • Come funziona il Carbon Aware SDK: architettura WebAPI, CLI e libreria client
  • Le sorgenti dati di carbon intensity: WattTime, ElectricityMaps, Ember Climate
  • Time shifting: schedulare batch jobs nelle finestre a bassa intensità carbonica
  • Location shifting: scegliere la region cloud con il mix energetico più pulito
  • Integrazione Python con il client del Carbon Aware SDK
  • Kubernetes + KEDA: carbon-aware autoscaling senza modifiche al codice applicativo
  • GitHub Actions: CI/CD pipeline carbon-aware con scheduling intelligente
  • Marginal vs average carbon intensity: quale segnale usare e perché
  • Limiti, trade-off e casi d'uso reali con riduzione misurabile di emissioni

Serie Green Software Engineering - Tutti gli Articoli

# Titolo Argomento
1 Principi del Green Software Engineering 8 principi GSF, SCI specification ISO/IEC 21031
2 Misurare le Emissioni con CodeCarbon Tracciamento CO₂ in Python, MLflow, dashboard
3 Climatiq API: Carbon Intensity nei Sistemi Cloud REST API, calcolo emissioni cloud e supply chain
4 Carbon Aware SDK (questo articolo) Time shifting, location shifting, Kubernetes, CI/CD
5 Scope 3 e Pipeline ESG Emissioni upstream/downstream, data pipeline CSRD
6 Modellazione Scope 1, 2, 3 Framework contabili GHG Protocol, SBTi
7 AI Carbon Footprint Training LLM, inferenza, ottimizzazione energetica
8 Sustainable Software Patterns Design pattern green, architetture efficienti
9 ESG e CSRD per il Software Compliance normativa, reporting obbligatorio EU
10 GreenOps: Operazioni Sostenibili FinOps+GreenOps, metriche operative, culture shift

Demand Shifting: Il Principio alla Base del Carbon-Aware Computing

L'intensità carbonica di un kilowattora di elettricità non è fissa: varia ogni ora in base a quanta energia rinnovabile è disponibile sulla rete. In una giornata soleggiata in Germania, l'intensità carbonica può scendere sotto i 100 gCO₂/kWh nelle ore centrali; la notte, con il solare spento e la domanda industriale in calo, risale sopra i 400 gCO₂/kWh. La stessa variabilità esiste nello spazio: la Svezia, alimentata quasi interamente da idroelettrico e nucleare, oscilla tra 15 e 40 gCO₂/kWh, mentre la Polonia, ancora dipendente dal carbone, supera spesso i 700 gCO₂/kWh.

Il demand shifting sfrutta questa variabilità spostando i workload flessibili - batch processing, training ML, backup, analisi dati, test CI/CD - verso i momenti e le regioni in cui l'elettricità è più pulita. Non si tratta di rinunciare al calcolo: si tratta di scegliere il momento migliore per eseguirlo.

Tipi di Demand Shifting

Tipo Descrizione Esempio pratico Riduzione CO₂ tipica
Time Shifting Spostare il workload in una finestra temporale a bassa carbon intensity nella stessa regione Eseguire il training ML alle 2:00 invece delle 14:00 20-45%
Location Shifting Eseguire il workload in una regione cloud con mix energetico più pulito Instradare il job da us-east-1 a eu-north-1 (Stoccolma) 30-70%
Demand Shaping Ridurre le funzionalità offerte quando la carbon intensity è alta Disabilitare la risoluzione video 4K durante picchi di emissioni 10-25%

Ovviamente non tutti i workload sono spostabili: una richiesta HTTP utente deve essere servita immediatamente, una transazione finanziaria non può aspettare. Ma una percentuale sorprendentemente alta dei carichi computazionali aziendali è time-flexible: training di modelli ML, pipeline ETL notturne, generazione report, backup, scansioni di sicurezza, pipeline CI/CD. Sono proprio questi i candidati ideali per il carbon-aware scheduling.

Carbon Aware SDK: Architettura e Componenti

Il Carbon Aware SDK è un progetto open source della Green Software Foundation, rilasciato sotto licenza MIT e attualmente in stato Graduated Project (il livello più alto di maturità nel portfolio GSF). Il repository è su GitHub alla voce Green-Software-Foundation/carbon-aware-sdk e viene sviluppato principalmente in C# con una WebAPI ASP.NET Core, ma espone interfacce utilizzabili da qualsiasi linguaggio.

L'architettura è modulare e ruota attorno a un concetto chiave: un'unica interfaccia normalizzata per accedere a dati di carbon intensity da provider diversi, con output sempre in gCO₂eq/kWh indipendentemente dalla sorgente. Questo è fondamentale perché ogni provider usa unità, granularità geografica e metodologie di calcolo diverse.

Componenti del Carbon Aware SDK

Componente Tecnologia Uso Quando usarlo
WebAPI ASP.NET Core (C#), Docker REST API con Swagger/OpenAPI, deployabile come microservizio Integrazione con qualsiasi stack, architetture a microservizi
CLI dotnet tool, cross-platform Interrogazione da terminale, scripting bash/PowerShell Script di deployment, automazione DevOps, test rapidi
SDK Library NuGet package (C#) Integrazione diretta nel codice .NET applicativo Applicazioni .NET che vogliono logica carbon-aware embedded
Python Client openapi-generator, Python 3.8+ Client auto-generato dalla spec OpenAPI del WebAPI Pipeline ML, script data engineering, Airflow DAG

Il flusso tipico è il seguente: si fa girare il WebAPI come container Docker, configurato con le credenziali del proprio provider di carbon intensity, e si interrogano gli endpoint REST dall'applicazione di scheduling. Il WebAPI si occupa di normalizzare i dati, gestire le chiamate ai provider, e restituire le finestre temporali o le regioni ottimali.

Sorgenti Dati: WattTime, ElectricityMaps e Ember Climate

Il Carbon Aware SDK supporta più provider di dati di carbon intensity, ognuno con caratteristiche e copertura geografica diverse. La scelta del provider ha implicazioni importanti sia metodologiche (marginal vs average) sia pratiche (copertura, costo, frequenza aggiornamento).

Confronto Provider Carbon Intensity

Provider Tipo Segnale Copertura Frequenza Forecast Costo
WattTime Marginal (MOER) USA completo, 50+ paesi 5 minuti 24-72 ore Piano free limitato, piani commerciali
ElectricityMaps Average (LCA) 85+ paesi, EU completa Oraria/sub-oraria 24 ore Piano developer free, commerciale
Ember Climate Average storico Globale (50+ paesi) Giornaliero/storico No (solo storico) Open data, gratuito
JSON statico Configurabile Custom Manuale No Gratuito (sviluppo/test)

Marginal vs Average Carbon Intensity: Una Distinzione Cruciale

La scelta tra segnale marginale e medio è la differenza metodologica più importante del carbon-aware computing e ha implicazioni concrete sui risultati ottenibili.

Il segnale marginale (MOER - Marginal Operating Emissions Rate), fornito da WattTime, risponde alla domanda: "Se in questo momento aumentassi il mio consumo di 1 kWh, quale centrale entrasse in funzione per coprire quella domanda aggiuntiva?". La risposta è quasi sempre la centrale a gas più flessibile (il cosiddetto "marginal generator"), non le rinnovabili che sono già accese al massimo. Questo segnale è il più rilevante per le decisioni in tempo reale di chi vuole ridurre le emissioni causate dal proprio consumo incrementale.

Il segnale medio (LCA - Life Cycle Average), fornito da ElectricityMaps, risponde invece alla domanda: "Qual è la media delle emissioni di tutta l'elettricità prodotta sulla rete in questo momento?". In una rete con 50% solare e 50% gas, il segnale medio è ~250 gCO₂/kWh, indipendentemente da cosa accade al margine. Questo segnale è più adatto per il reporting ESG e la contabilità Scope 2 market-based.

Importante: Cambio di Approccio di ElectricityMaps nel 2025

Nel 2025, ElectricityMaps ha discontinuato il segnale marginale dalla propria API citando preoccupazioni sulla verificabilità dei dati e allineamento con le normative EU e USA che vietano l'uso di fattori marginali nella contabilità Scope 2. ElectricityMaps ora offre esclusivamente segnali medi basati su metodologia LCA (Life Cycle Assessment). Se il tuo use case richiede segnale marginale, WattTime rimane l'unico provider mainstream che lo supporta. Il Carbon Aware SDK gestisce questa differenza in modo trasparente attraverso la configurazione del provider.

Quando Usare Quale Segnale

Use Case Segnale Consigliato Provider Perché
Time shifting batch jobs Marginale WattTime Massimizza la riduzione reale delle emissioni causate
Location shifting multi-region Medio o marginale ElectricityMaps o WattTime Entrambi utili; medio più stabile cross-region
Reporting ESG / Scope 2 Medio ElectricityMaps Richiesto da GHG Protocol e normative EU/USA
Sviluppo e test JSON statico File locale Nessun costo, dati deterministici per test

Setup e Configurazione del Carbon Aware SDK

Il modo più rapido per avviare il Carbon Aware SDK in locale è con Docker Compose. Vediamo la configurazione completa con ElectricityMaps come provider (il più facile da configurare grazie all'API key gratuita per sviluppatori).

# docker-compose.yml - Carbon Aware SDK WebAPI
version: '3.8'

services:
  carbon-aware-api:
    image: ghcr.io/green-software-foundation/carbon-aware-sdk:latest
    ports:
      - "8080:80"
    environment:
      # Provider: ElectricityMaps (segnale medio LCA)
      CarbonAwareVars__CarbonIntensityDataSource: "ElectricityMaps"
      CarbonAwareVars__ElectricityMapsClient__APITokenHeader: "auth-token"
      CarbonAwareVars__ElectricityMapsClient__APIToken: "${ELECTRICITY_MAPS_TOKEN}"
      CarbonAwareVars__ElectricityMapsClient__BaseURL: "https://api.electricitymap.org/v3/"

      # Oppure WattTime (segnale marginale MOER)
      # CarbonAwareVars__CarbonIntensityDataSource: "WattTime"
      # CarbonAwareVars__WattTimeClient__Username: "${WATTTIME_USER}"
      # CarbonAwareVars__WattTimeClient__Password: "${WATTTIME_PASS}"
      # CarbonAwareVars__WattTimeClient__BaseURL: "https://api2.watttime.org/v2/"

    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Per l'ambiente di sviluppo e test, si può usare la sorgente dati JSON statica che non richiede API key. Il SDK include dataset di esempio già precaricati.

# appsettings.json - Configurazione con JSON statico (dev/test)
{
  "CarbonAwareVars": {
    "CarbonIntensityDataSource": "Json",
    "JsonDataFileLocation": "./data/test-data.json",
    "Proxy": {
      "UseProxy": false
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Una volta avviato il container, la documentazione Swagger è disponibile su http://localhost:8080/swagger. Verifichiamo che tutto funzioni con una chiamata di test alla CLI:

# Installazione Carbon Aware CLI (dotnet tool)
dotnet tool install -g GSF.CarbonAware.Cli

# Verifica emissioni attuali in una location
carbon-aware emissions location --location "westeurope" \
  --config ./appsettings.json

# Output esempio:
# Location: westeurope
# Time: 2025-09-15T14:30:00Z
# Rating: 180.5 gCO2eq/kWh
# Duration: PT1H

API Endpoints: Il Cuore del Carbon Aware SDK

Il WebAPI espone un set di endpoint REST documentati con OpenAPI/Swagger. Vediamo i principali con esempi di chiamate reali e interpretazione delle risposte.

GET /emissions/bylocations - Emissioni Correnti per Location

Restituisce i dati di carbon intensity per una o più location in un intervallo temporale. Usato per confrontare la carbon intensity attuale tra regioni diverse (location shifting).

# Confronto carbon intensity tra regioni cloud (location shifting)
GET /emissions/bylocations?locations=westeurope&locations=eastus&locations=northeurope
    &time=2025-09-15T14:00:00Z
    &toTime=2025-09-15T15:00:00Z

# Risposta JSON:
[
  {
    "location": "westeurope",
    "timestamp": "2025-09-15T14:00:00Z",
    "duration": 60,
    "rating": 185.3
  },
  {
    "location": "eastus",
    "timestamp": "2025-09-15T14:00:00Z",
    "duration": 60,
    "rating": 312.7
  },
  {
    "location": "northeurope",
    "timestamp": "2025-09-15T14:00:00Z",
    "duration": 60,
    "rating": 32.1
  }
]
# North Europe (Irlanda/Stoccolma) quasi 10x più verde di East US!

GET /emissions/bylocation/best - Migliore Location

Restituisce direttamente la location con la più bassa carbon intensity nell'intervallo specificato. Ideale per il routing automatico dei workload.

GET /emissions/bylocation/best?locations=westeurope&locations=eastus&locations=northeurope
    &time=2025-09-15T14:00:00Z
    &toTime=2025-09-15T15:00:00Z

# Risposta:
{
  "location": "northeurope",
  "timestamp": "2025-09-15T14:00:00Z",
  "duration": 60,
  "rating": 32.1
}

GET /emissions/forecasts/current - Forecast Carbon Intensity

Questo è l'endpoint più potente per il time shifting: restituisce il forecast delle prossime 24-72 ore con la finestra temporale ottimale in cui eseguire il workload. Permette di specificare la durata del job per trovare la finestra migliore di quella lunghezza.

GET /emissions/forecasts/current?locations=westeurope
    &dataStartAt=2025-09-15T16:00:00Z
    &dataEndAt=2025-09-16T16:00:00Z
    &windowSize=60

# Risposta con finestra ottimale:
[
  {
    "generatedAt": "2025-09-15T14:30:00Z",
    "location": "westeurope",
    "dataStartAt": "2025-09-15T16:00:00Z",
    "dataEndAt": "2025-09-16T16:00:00Z",
    "windowSize": 60,
    "optimalDataPoints": [
      {
        "location": "westeurope",
        "timestamp": "2025-09-16T02:00:00Z",
        "duration": 60,
        "rating": 95.2
      }
    ],
    "forecastData": [
      { "timestamp": "2025-09-15T16:00:00Z", "rating": 195.8 },
      { "timestamp": "2025-09-15T17:00:00Z", "rating": 210.3 },
      { "timestamp": "2025-09-15T18:00:00Z", "rating": 230.1 },
      { "timestamp": "2025-09-15T22:00:00Z", "rating": 155.4 },
      { "timestamp": "2025-09-16T01:00:00Z", "rating": 105.7 },
      { "timestamp": "2025-09-16T02:00:00Z", "rating": 95.2 },  // OTTIMALE
      { "timestamp": "2025-09-16T03:00:00Z", "rating": 98.6 },
      { "timestamp": "2025-09-16T06:00:00Z", "rating": 120.3 }
    ]
  }
]
# Il job da 1 ora dovrebbe partire alle 02:00 UTC (risparmio ~51% CO2 vs ora corrente)

POST /emissions/forecasts/batch - Forecast Multipli

Permette di inviare un batch di richieste di forecast per pianificare più job contemporaneamente, utile per lo scheduling di pipeline complesse con dipendenze.

POST /emissions/forecasts/batch
Content-Type: application/json

[
  {
    "requestedAt": "2025-09-15T14:00:00Z",
    "location": "westeurope",
    "dataStartAt": "2025-09-15T20:00:00Z",
    "dataEndAt": "2025-09-16T08:00:00Z",
    "windowSize": 120
  },
  {
    "requestedAt": "2025-09-15T14:00:00Z",
    "location": "westeurope",
    "dataStartAt": "2025-09-15T20:00:00Z",
    "dataEndAt": "2025-09-16T08:00:00Z",
    "windowSize": 30
  }
]

Integrazione Python: CarbonAwareClient e Time Shifting

Per pipeline Python - tipicamente data engineering, ML training o batch analytics - il Carbon Aware SDK offre un client Python generato automaticamente dalla specifica OpenAPI. Vediamo come costruire un sistema completo di time shifting per un job di training ML.

Installazione e Configurazione Client Python

# requirements.txt
carbon-aware-sdk-client>=1.0.0   # Client auto-generato da OpenAPI
requests>=2.31.0
python-dateutil>=2.8.2
apscheduler>=3.10.4              # Scheduling job
pytz>=2023.3

# Installazione da PyPI (se disponibile) oppure da sorgente:
# pip install openapi-python-client
# openapi-python-client generate \
#   --url http://localhost:8080/swagger/v1/swagger.json

Client Carbon Aware in Python

"""
carbon_aware_client.py
Client Python per il Carbon Aware SDK WebAPI
Implementa time shifting e location shifting
"""
import requests
from datetime import datetime, timedelta, timezone
from typing import Optional
import logging

logger = logging.getLogger(__name__)


class CarbonAwareClient:
    """
    Client per il Carbon Aware SDK WebAPI.
    Incapsula le chiamate REST e fornisce metodi ad alto livello
    per time shifting e location shifting.
    """

    def __init__(self, base_url: str = "http://localhost:8080"):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({
            "Content-Type": "application/json",
            "Accept": "application/json"
        })

    def get_current_emissions(
        self,
        locations: list[str],
        time: Optional[datetime] = None,
        to_time: Optional[datetime] = None
    ) -> list[dict]:
        """
        Recupera la carbon intensity corrente per una o più location.

        Args:
            locations: Lista di location name (es. ["westeurope", "eastus"])
            time: Inizio intervallo (default: ora corrente)
            to_time: Fine intervallo (default: time + 1 ora)

        Returns:
            Lista di dict con location, timestamp, rating (gCO2eq/kWh)
        """
        now = datetime.now(timezone.utc)
        time = time or now
        to_time = to_time or (time + timedelta(hours=1))

        params = {
            "locations": locations,
            "time": time.isoformat(),
            "toTime": to_time.isoformat()
        }

        response = self.session.get(
            f"{self.base_url}/emissions/bylocations",
            params=params
        )
        response.raise_for_status()
        return response.json()

    def get_best_location(
        self,
        locations: list[str],
        time: Optional[datetime] = None,
        to_time: Optional[datetime] = None
    ) -> dict:
        """
        Trova la location con la più bassa carbon intensity.

        Returns:
            Dict con location, timestamp, rating della migliore location
        """
        now = datetime.now(timezone.utc)
        time = time or now
        to_time = to_time or (time + timedelta(hours=1))

        params = {
            "locations": locations,
            "time": time.isoformat(),
            "toTime": to_time.isoformat()
        }

        response = self.session.get(
            f"{self.base_url}/emissions/bylocation/best",
            params=params
        )
        response.raise_for_status()
        return response.json()

    def get_optimal_window(
        self,
        location: str,
        window_size_minutes: int,
        search_start: Optional[datetime] = None,
        search_end: Optional[datetime] = None
    ) -> dict:
        """
        Trova la finestra temporale ottimale (più bassa carbon intensity)
        per eseguire un job di durata window_size_minutes nella location indicata.

        Args:
            location: Location name (es. "westeurope")
            window_size_minutes: Durata del job in minuti
            search_start: Inizio finestra di ricerca (default: ora corrente)
            search_end: Fine finestra di ricerca (default: search_start + 24 ore)

        Returns:
            Dict con optimalDataPoints (timestamp ottimale) e forecastData
        """
        now = datetime.now(timezone.utc)
        search_start = search_start or now
        search_end = search_end or (search_start + timedelta(hours=24))

        params = {
            "locations": [location],
            "dataStartAt": search_start.isoformat(),
            "dataEndAt": search_end.isoformat(),
            "windowSize": window_size_minutes
        }

        response = self.session.get(
            f"{self.base_url}/emissions/forecasts/current",
            params=params
        )
        response.raise_for_status()

        forecasts = response.json()
        if not forecasts:
            raise ValueError(f"Nessun forecast disponibile per {location}")

        return forecasts[0]

    def calculate_carbon_savings(
        self,
        current_rating: float,
        optimal_rating: float,
        duration_hours: float,
        power_kw: float
    ) -> dict:
        """
        Calcola il risparmio di CO2 del time shifting.

        Args:
            current_rating: Carbon intensity attuale (gCO2/kWh)
            optimal_rating: Carbon intensity ottimale (gCO2/kWh)
            duration_hours: Durata del job in ore
            power_kw: Potenza media del job in kW

        Returns:
            Dict con emissioni attuali, ottimali e risparmio
        """
        energy_kwh = duration_hours * power_kw
        current_emissions_g = current_rating * energy_kwh
        optimal_emissions_g = optimal_rating * energy_kwh
        savings_g = current_emissions_g - optimal_emissions_g
        savings_pct = (savings_g / current_emissions_g) * 100 if current_emissions_g > 0 else 0

        return {
            "energy_kwh": energy_kwh,
            "current_emissions_gco2": round(current_emissions_g, 2),
            "optimal_emissions_gco2": round(optimal_emissions_g, 2),
            "savings_gco2": round(savings_g, 2),
            "savings_percentage": round(savings_pct, 1)
        }

Time Shifting: Scheduler per ML Training

"""
ml_training_scheduler.py
Scheduler carbon-aware per job di training ML.
Calcola la finestra ottimale nelle prossime 12 ore
e pianifica l'avvio del training.
"""
from datetime import datetime, timedelta, timezone
from apscheduler.schedulers.blocking import BlockingScheduler
from carbon_aware_client import CarbonAwareClient
import subprocess
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class MLTrainingScheduler:
    """
    Scheduler carbon-aware per training ML.
    Trova la finestra a più bassa carbon intensity
    nelle prossime 12 ore e schedula il training.
    """

    def __init__(
        self,
        location: str = "westeurope",
        training_duration_minutes: int = 120,
        max_wait_hours: int = 12,
        carbon_aware_url: str = "http://localhost:8080"
    ):
        self.location = location
        self.training_duration_minutes = training_duration_minutes
        self.max_wait_hours = max_wait_hours
        self.client = CarbonAwareClient(base_url=carbon_aware_url)
        self.scheduler = BlockingScheduler(timezone="UTC")

    def find_optimal_start(self) -> datetime:
        """
        Interroga il Carbon Aware SDK per trovare
        il momento ottimale nelle prossime max_wait_hours.
        """
        now = datetime.now(timezone.utc)
        search_end = now + timedelta(hours=self.max_wait_hours)

        logger.info(
            f"Ricerca finestra ottimale in {self.location} "
            f"per job da {self.training_duration_minutes} minuti..."
        )

        forecast = self.client.get_optimal_window(
            location=self.location,
            window_size_minutes=self.training_duration_minutes,
            search_start=now,
            search_end=search_end
        )

        optimal_points = forecast.get("optimalDataPoints", [])
        if not optimal_points:
            logger.warning("Nessuna finestra ottimale trovata, uso ora corrente")
            return now

        optimal_timestamp_str = optimal_points[0]["timestamp"]
        optimal_timestamp = datetime.fromisoformat(
            optimal_timestamp_str.replace("Z", "+00:00")
        )
        optimal_rating = optimal_points[0]["rating"]

        # Recupera rating corrente per calcolo risparmio
        current_data = self.client.get_current_emissions([self.location])
        current_rating = current_data[0]["rating"] if current_data else 300.0

        # Calcola risparmio
        savings = self.client.calculate_carbon_savings(
            current_rating=current_rating,
            optimal_rating=optimal_rating,
            duration_hours=self.training_duration_minutes / 60,
            power_kw=150  # GPU server: ~150kW stima
        )

        wait_minutes = (optimal_timestamp - now).total_seconds() / 60

        logger.info(
            f"Finestra ottimale trovata:\n"
            f"  Inizio: {optimal_timestamp.strftime('%Y-%m-%d %H:%M UTC')}\n"
            f"  Carbon intensity: {optimal_rating:.1f} gCO2/kWh\n"
            f"  Carbon intensity attuale: {current_rating:.1f} gCO2/kWh\n"
            f"  Attesa: {wait_minutes:.0f} minuti\n"
            f"  Risparmio CO2: {savings['savings_gco2']:.0f}g ({savings['savings_percentage']}%)"
        )

        return optimal_timestamp

    def run_training(self):
        """Esegue il job di training ML."""
        logger.info("Avvio training ML carbon-optimized...")
        result = subprocess.run(
            ["python", "train_model.py", "--config", "config.yaml"],
            capture_output=True,
            text=True
        )
        if result.returncode == 0:
            logger.info("Training completato con successo")
        else:
            logger.error(f"Training fallito: {result.stderr}")

    def schedule_and_run(self):
        """Trova la finestra ottimale e schedula il training."""
        optimal_start = self.find_optimal_start()
        now = datetime.now(timezone.utc)

        if optimal_start <= now + timedelta(minutes=5):
            # Avvio immediato se il momento ottimale è imminente
            logger.info("Avvio immediato (finestra ottimale = ora)")
            self.run_training()
        else:
            # Schedula l'avvio
            self.scheduler.add_job(
                self.run_training,
                "date",
                run_date=optimal_start,
                id="ml_training"
            )
            logger.info(f"Training schedulato per {optimal_start}")
            self.scheduler.start()


# Utilizzo:
if __name__ == "__main__":
    scheduler = MLTrainingScheduler(
        location="westeurope",
        training_duration_minutes=120,   # Job da 2 ore
        max_wait_hours=12                # Aspetta massimo 12 ore
    )
    scheduler.schedule_and_run()

Location Shifting: Multi-Region Carbon-Aware Routing

Il location shifting è spesso più efficace del time shifting, perché la differenza di carbon intensity tra regioni cloud può essere molto maggiore della variazione temporale all'interno di una singola regione. Un job eseguito in europe-north1 (Finlandia, ~9 gCO₂/kWh) al posto di asia-east1 (Taiwan, ~550 gCO₂/kWh) riduce le emissioni di oltre il 98%.

Carbon Intensity Media per Regione Cloud (2025)

Cloud Provider Regione Location Carbon Intensity Media (gCO₂/kWh) Fonte Principale
Google Cloud europe-north1 Finlandia 9 Idroelettrico, nucleare
Azure swedencentral Svezia 18 Idroelettrico, nucleare
Azure northeurope Irlanda 280 Eolico, gas
AWS eu-west-1 Irlanda 320 Eolico, gas
AWS us-east-1 Virginia 380 Gas, nucleare, carbone
Google Cloud asia-east1 Taiwan 545 Carbone, gas
AWS ap-southeast-1 Singapore 408 Gas naturale

Python: Location Shifting Automatico per Batch Job

"""
location_shifter.py
Location shifting carbon-aware per batch job multi-cloud.
Seleziona automaticamente la regione più verde
per eseguire un job Kubernetes.
"""
from carbon_aware_client import CarbonAwareClient
from datetime import datetime, timezone
import subprocess
import json
import logging

logger = logging.getLogger(__name__)

# Mapping location Carbon Aware SDK -> region cloud reale
LOCATION_TO_CLOUD_REGION = {
    "northeurope": {
        "aws": "eu-west-1",
        "azure": "northeurope",
        "gcp": "europe-west1"
    },
    "swedencentral": {
        "azure": "swedencentral",
        "gcp": "europe-north1"
    },
    "westeurope": {
        "aws": "eu-central-1",
        "azure": "westeurope",
        "gcp": "europe-west4"
    },
    "eastus": {
        "aws": "us-east-1",
        "azure": "eastus",
        "gcp": "us-east1"
    }
}

CANDIDATE_LOCATIONS = ["swedencentral", "northeurope", "westeurope", "eastus"]
TARGET_CLOUD = "azure"  # Cloud provider di destinazione


class LocationShifter:
    def __init__(self, carbon_aware_url: str = "http://localhost:8080"):
        self.client = CarbonAwareClient(base_url=carbon_aware_url)

    def select_greenest_region(self) -> tuple[str, str, float]:
        """
        Seleziona la regione cloud con la più bassa carbon intensity
        tra le candidate.

        Returns:
            Tuple (location_name, cloud_region, carbon_intensity_rating)
        """
        best = self.client.get_best_location(
            locations=CANDIDATE_LOCATIONS
        )

        best_location = best["location"]
        best_rating = best["rating"]

        # Mappa a regione cloud reale
        cloud_regions = LOCATION_TO_CLOUD_REGION.get(best_location, {})
        cloud_region = cloud_regions.get(TARGET_CLOUD, "westeurope")

        logger.info(
            f"Regione selezionata: {best_location} -> {TARGET_CLOUD}:{cloud_region}\n"
            f"Carbon intensity: {best_rating:.1f} gCO2/kWh"
        )

        # Log emissioni di tutte le candidate per confronto
        all_emissions = self.client.get_current_emissions(CANDIDATE_LOCATIONS)
        logger.info("Confronto regioni:")
        for em in sorted(all_emissions, key=lambda x: x["rating"]):
            marker = " <- SELEZIONATA" if em["location"] == best_location else ""
            logger.info(f"  {em['location']:20s} {em['rating']:6.1f} gCO2/kWh{marker}")

        return best_location, cloud_region, best_rating

    def deploy_job_to_region(self, cloud_region: str, job_config: dict):
        """
        Deploya un batch job Kubernetes nella regione selezionata.
        Usa kubectl con il context della regione target.
        """
        # Sostituisce la regione nel manifest Kubernetes
        manifest = job_config.copy()
        manifest["metadata"]["annotations"]["target-region"] = cloud_region

        manifest_json = json.dumps(manifest)

        logger.info(f"Deploy job in regione {cloud_region}...")
        result = subprocess.run(
            ["kubectl", "apply", "-f", "-", "--context", f"aks-{cloud_region}"],
            input=manifest_json,
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            logger.info(f"Job deployato con successo in {cloud_region}")
        else:
            raise RuntimeError(f"Deploy fallito: {result.stderr}")

    def run_carbon_aware_job(self, job_config: dict):
        """Workflow completo: seleziona regione e deploya."""
        location, cloud_region, rating = self.select_greenest_region()
        logger.info(f"Avvio job carbon-aware in {cloud_region} ({rating:.1f} gCO2/kWh)")
        self.deploy_job_to_region(cloud_region, job_config)


# Utilizzo
if __name__ == "__main__":
    shifter = LocationShifter()

    # Configurazione job Kubernetes
    batch_job = {
        "apiVersion": "batch/v1",
        "kind": "Job",
        "metadata": {
            "name": "data-processing-carbon-aware",
            "annotations": {
                "green-software.io/carbon-aware": "true",
                "target-region": ""  # Verrà popolato dal location shifter
            }
        },
        "spec": {
            "template": {
                "spec": {
                    "containers": [{
                        "name": "processor",
                        "image": "myapp/data-processor:latest",
                        "resources": {
                            "requests": {"cpu": "2", "memory": "4Gi"},
                            "limits": {"cpu": "4", "memory": "8Gi"}
                        }
                    }],
                    "restartPolicy": "Never"
                }
            }
        }
    }

    shifter.run_carbon_aware_job(batch_job)

Kubernetes e KEDA: Carbon-Aware Autoscaling

Per ambienti Kubernetes in produzione, Microsoft ha rilasciato il Carbon Aware KEDA Operator (open source su Azure GitHub), che integra il Carbon Aware SDK direttamente nel layer di scaling di KEDA. Il principio è semplice: quando la carbon intensity è bassa (energia pulita disponibile), KEDA può scalare fino al massimo numero di repliche; quando è alta, il numero massimo di repliche viene ridotto automaticamente.

L'architettura si compone di tre elementi: il Kubernetes Carbon Intensity Exporter (recupera i dati dal Carbon Aware SDK e li espone come ConfigMap nel cluster), il Carbon Aware KEDA Operator (legge la ConfigMap e aggiorna i limiti di scaling di KEDA), e la risorsa custom CarbonAwareKedaScaler che definisce le soglie di scaling.

Installazione Carbon Intensity Exporter

# Installa il Carbon Intensity Exporter con Helm
helm repo add azure-carbon https://azure.github.io/carbon-aware-keda-operator
helm repo update

# Crea il secret con le credenziali WattTime
kubectl create secret generic watttime-credentials \
  --from-literal=username=MY_WATTTIME_USER \
  --from-literal=password=MY_WATTTIME_PASS \
  --namespace kube-system

# Installa l'exporter
helm install carbon-intensity-exporter azure-carbon/carbon-intensity-exporter \
  --namespace kube-system \
  --set carbonDataProvider=WattTime \
  --set watttime.username=MY_WATTTIME_USER \
  --set watttime.password=MY_WATTTIME_PASS \
  --set location=westus \
  --set forecastIntervalHours=12

ConfigMap Generata dall'Exporter

apiVersion: v1
kind: ConfigMap
metadata:
  name: carbon-intensity
  namespace: kube-system
data:
  # Recuperata ogni 12 ore dall'exporter
  lastUpdated: "2025-09-15T14:00:00Z"
  forecastDateTime: "2025-09-15T14:00:00Z"
  message: "Carbon intensity data for westus"

  # Array JSON con forecast delle prossime 24 ore
  # Format: ISO timestamp + gCO2/kWh
  forecastData: |
    [
      { "timestamp": "2025-09-15T14:00:00Z", "intensity": 312.5 },
      { "timestamp": "2025-09-15T15:00:00Z", "intensity": 298.3 },
      { "timestamp": "2025-09-15T16:00:00Z", "intensity": 285.1 },
      { "timestamp": "2025-09-15T22:00:00Z", "intensity": 195.7 },
      { "timestamp": "2025-09-16T02:00:00Z", "intensity": 145.2 },
      { "timestamp": "2025-09-16T03:00:00Z", "intensity": 138.9 }
    ]

  # Intensità corrente
  currentIntensity: "312.5"
  currentIntensityUnit: "gCO2/kWh"

CarbonAwareKedaScaler: Definizione della Risorsa Custom

apiVersion: carbonaware.azure.com/v1alpha1
kind: CarbonAwareKedaScaler
metadata:
  name: batch-processor-carbon-scaler
  namespace: default
spec:
  # Riferimento al KEDA ScaledJob da controllare
  kedaTargetRef:
    apiVersion: keda.sh/v1alpha1
    kind: ScaledJob
    name: batch-processor-scaledjob

  # Sorgente dati carbon intensity (ConfigMap dell'exporter)
  carbonIntensityForecastDataSource:
    localConfigMap:
      name: carbon-intensity
      namespace: kube-system
      key: forecastData
    mockCarbonForecast: false

  # Soglie: definiscono maxReplicas in base all'intensità carbonica
  # Ordinate per intensità crescente
  # - Se intensità <= 150: max 20 repliche (energia molto pulita)
  # - Se intensità <= 300: max 10 repliche (energia moderatamente pulita)
  # - Se intensità <= 500: max 4 repliche (energia poco pulita)
  # - Se intensità > 500: max 1 replica (energia sporca)
  maxReplicasByCarbonIntensity:
    - carbonIntensityThreshold: 150
      maxReplicas: 20
    - carbonIntensityThreshold: 300
      maxReplicas: 10
    - carbonIntensityThreshold: 500
      maxReplicas: 4

ScaledJob KEDA per Batch Processing

apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
  name: batch-processor-scaledjob
  namespace: default
  labels:
    app: batch-processor
    green-software.io/carbon-aware: "true"
spec:
  # Sorgente trigger: RabbitMQ, Kafka, Azure Queue, ecc.
  jobTargetRef:
    parallelism: 1
    completions: 1
    activeDeadlineSeconds: 3600
    backoffLimit: 2
    template:
      metadata:
        labels:
          app: batch-processor
      spec:
        containers:
          - name: processor
            image: myapp/batch-processor:v2.1.0
            resources:
              requests:
                cpu: "500m"
                memory: "512Mi"
              limits:
                cpu: "2"
                memory: "2Gi"
            env:
              - name: BATCH_SIZE
                value: "1000"
              - name: OUTPUT_BUCKET
                value: "gs://my-processed-data"
        restartPolicy: Never

  # Trigger: coda Azure Service Bus
  triggers:
    - type: azure-servicebus
      metadata:
        queueName: batch-jobs
        namespace: my-servicebus-namespace
        messageCount: "50"

  # Scaling: min 0, max viene controllato dal CarbonAwareKedaScaler
  pollingInterval: 60      # Controlla la coda ogni 60 secondi
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  minReplicaCount: 0
  maxReplicaCount: 20      # Overridato dal CarbonAwareKedaScaler

Come Funziona il Carbon Aware KEDA Operator in Pratica

Ogni ora, il Carbon Aware KEDA Operator legge la ConfigMap aggiornata dall'exporter e aggiorna il campo maxReplicaCount dello ScaledJob KEDA in base alla soglia corrispondente all'intensità carbonica corrente. Se ci sono 200 job in coda e la carbon intensity è 312 gCO₂/kWh (soglia 300-500), KEDA può scalare fino a un massimo di 10 worker invece di 20. Non appena l'intensità scende sotto 150 (es. alle 2:00 di notte con forte vento), il limite sale automaticamente a 20 e i job vengono processati più rapidamente. Nessuna modifica al codice applicativo è necessaria.

Kubernetes CronJob Carbon-Aware con Init Container

Per job schedulati periodicamente, si può usare un pattern diverso: un init container che interroga il Carbon Aware SDK prima dell'avvio del job e decide se eseguire o rimandare. Questo approccio non richiede KEDA e funziona con i CronJob standard di Kubernetes.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-etl-pipeline
  namespace: default
  annotations:
    green-software.io/carbon-aware: "true"
    green-software.io/max-intensity: "200"
spec:
  # Schedulato per le 00:00 UTC ogni notte
  # L'init container deciderà se eseguire o saltare
  schedule: "0 0 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 7
  failedJobsHistoryLimit: 3

  jobTemplate:
    spec:
      template:
        spec:
          initContainers:
            # Init container che controlla la carbon intensity
            - name: carbon-aware-check
              image: curlimages/curl:8.5.0
              env:
                - name: CARBON_AWARE_API
                  value: "http://carbon-aware-api.monitoring.svc.cluster.local:8080"
                - name: LOCATION
                  value: "westeurope"
                - name: MAX_INTENSITY
                  value: "200"
              command:
                - sh
                - -c
                - |
                  set -e
                  echo "Controllo carbon intensity per ${LOCATION}..."

                  # Interroga il Carbon Aware SDK
                  RESPONSE=$(curl -sf "${CARBON_AWARE_API}/emissions/bylocations?locations=${LOCATION}")
                  INTENSITY=$(echo "$RESPONSE" | grep -o '"rating":[0-9.]*' | head -1 | cut -d: -f2)

                  echo "Carbon intensity corrente: ${INTENSITY} gCO2/kWh (massimo: ${MAX_INTENSITY})"

                  if [ $(echo "${INTENSITY} > ${MAX_INTENSITY}" | bc -l) -eq 1 ]; then
                    echo "ATTENZIONE: Intensità carbonica troppo alta (${INTENSITY} > ${MAX_INTENSITY})"
                    echo "Il job verrà rimandato al prossimo ciclo di scheduling"
                    exit 1  # Fallisce l'init container, il job non parte
                  fi

                  echo "OK: Intensità carbonica accettabile. Avvio job ETL..."
                  exit 0

          containers:
            - name: etl-pipeline
              image: myapp/etl-pipeline:v1.5.0
              command: ["python", "run_etl.py"]
              env:
                - name: PIPELINE_DATE
                  value: "$(date +%Y-%m-%d)"
              resources:
                requests:
                  cpu: "1"
                  memory: "2Gi"
                limits:
                  cpu: "4"
                  memory: "8Gi"
          restartPolicy: Never

GitHub Actions: CI/CD Pipeline Carbon-Aware

I test CI/CD e le pipeline di build sono tra i workload più facili da rendere carbon-aware, perché molti di essi sono effettivamente time-flexible: un test suite di integrazione può aspettare 2-4 ore senza impatto sul developer workflow, specialmente per pipeline notturne o scheduled.

Uno studio del 2024 ha stimato che il carbon footprint delle GitHub Actions per tutto l'ecosistema è nell'ordine di 450-1000 MTCO₂eq/anno. Con il carbon-aware scheduling anche solo dei workflow più pesanti (build con compilazione, test suite completi, training di modelli), la riduzione potrebbe essere significativa.

Workflow GitHub Actions con Carbon-Aware Scheduling

# .github/workflows/carbon-aware-build.yml
# Pipeline CI/CD carbon-aware con scheduling intelligente

name: Carbon-Aware Build and Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  # Scheduled: ogni 6 ore per trovare la finestra migliore
  schedule:
    - cron: '0 */6 * * *'
  workflow_dispatch:
    inputs:
      force_run:
        description: 'Forza esecuzione indipendentemente dalla carbon intensity'
        required: false
        default: 'false'

env:
  CARBON_AWARE_API: ${{ secrets.CARBON_AWARE_API_URL }}
  MAX_INTENSITY: "250"
  LOCATION: "westeurope"

jobs:
  # Job 1: Controlla carbon intensity e decide se procedere
  carbon-check:
    name: Carbon Intensity Check
    runs-on: ubuntu-latest
    outputs:
      should_run: ${{ steps.check.outputs.should_run }}
      intensity: ${{ steps.check.outputs.intensity }}
      optimal_time: ${{ steps.check.outputs.optimal_time }}

    steps:
      - name: Check Carbon Intensity
        id: check
        run: |
          # Per PR e push diretti: esegui sempre (developer experience)
          if [[ "${{ github.event_name }}" == "push" || \
                "${{ github.event_name }}" == "pull_request" || \
                "${{ github.event.inputs.force_run }}" == "true" ]]; then
            echo "Event: ${{ github.event_name }} - Esecuzione forzata"
            echo "should_run=true" >> $GITHUB_OUTPUT
            echo "intensity=0" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Per scheduled e manual: controlla carbon intensity
          RESPONSE=$(curl -sf \
            "${CARBON_AWARE_API}/emissions/bylocations?locations=${LOCATION}" \
            --max-time 30) || {
            echo "WARN: Carbon Aware API non raggiungibile, esecuzione forzata"
            echo "should_run=true" >> $GITHUB_OUTPUT
            echo "intensity=-1" >> $GITHUB_OUTPUT
            exit 0
          }

          INTENSITY=$(echo "$RESPONSE" | python3 -c "
          import json, sys
          data = json.load(sys.stdin)
          print(data[0]['rating'] if data else 999)
          ")

          echo "Carbon intensity corrente: ${INTENSITY} gCO2/kWh"
          echo "Soglia massima: ${MAX_INTENSITY} gCO2/kWh"
          echo "intensity=${INTENSITY}" >> $GITHUB_OUTPUT

          if python3 -c "exit(0 if float('$INTENSITY') <= float('$MAX_INTENSITY') else 1)"; then
            echo "Intensità accettabile - Avvio build"
            echo "should_run=true" >> $GITHUB_OUTPUT
          else
            echo "Intensità troppo alta - Skip build"
            echo "should_run=false" >> $GITHUB_OUTPUT

            # Recupera finestra ottimale per info
            FORECAST=$(curl -sf \
              "${CARBON_AWARE_API}/emissions/forecasts/current?locations=${LOCATION}&windowSize=60")
            OPTIMAL=$(echo "$FORECAST" | python3 -c "
          import json, sys
          data = json.load(sys.stdin)
          pts = data[0].get('optimalDataPoints', []) if data else []
          print(pts[0]['timestamp'] if pts else 'N/A')
            ")
            echo "optimal_time=$OPTIMAL" >> $GITHUB_OUTPUT
            echo "Finestra ottimale: $OPTIMAL"
          fi

      - name: Summary Carbon Check
        run: |
          echo "### Carbon Intensity Check" >> $GITHUB_STEP_SUMMARY
          echo "| Parametro | Valore |" >> $GITHUB_STEP_SUMMARY
          echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY
          echo "| Location | ${LOCATION} |" >> $GITHUB_STEP_SUMMARY
          echo "| Intensity | ${{ steps.check.outputs.intensity }} gCO2/kWh |" >> $GITHUB_STEP_SUMMARY
          echo "| Max Threshold | ${MAX_INTENSITY} gCO2/kWh |" >> $GITHUB_STEP_SUMMARY
          echo "| Will Run | ${{ steps.check.outputs.should_run }} |" >> $GITHUB_STEP_SUMMARY

  # Job 2: Build e test (condizionale alla carbon intensity)
  build-and-test:
    name: Build and Test
    runs-on: ubuntu-latest
    needs: carbon-check
    if: needs.carbon-check.outputs.should_run == 'true'

    steps:
      - uses: actions/checkout@v4

      - name: Carbon Intensity Badge
        run: |
          echo "Esecuzione con carbon intensity: ${{ needs.carbon-check.outputs.intensity }} gCO2/kWh"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Test
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4

Case Study: Training ML con Riduzione del 35% di CO₂

Presentiamo un case study realistico basato su pattern documentati in letteratura accademica (Springer Nature, 2024) e da implementazioni aziendali con il Carbon Aware SDK.

Scenario: Pipeline di Training Modello di Raccomandazione E-Commerce

Parametro Valore
Azienda E-commerce mid-size (500K clienti attivi)
Workload Training weekly del modello di recommendation
Durata job 3-4 ore (dipende dalla dimensione dataset)
Hardware 4x NVIDIA A100 (cloud VM), ~300W totali
Consumo energia per run ~1.2 kWh
Regione cloud Azure westeurope (Amsterdam)
Frequenza 1 volta a settimana (domenica notte)
Flessibilità temporale 12 ore (da domenica 20:00 a lunedì 08:00)

Risultati Prima e Dopo l'Adozione del Carbon Aware SDK

Confronto Emissioni: Prima vs Dopo (su base annuale)

Metrica Prima (scheduling fisso domenica 22:00) Dopo (carbon-aware time shifting) Variazione
Carbon intensity media al momento dell'esecuzione 265 gCO₂/kWh 172 gCO₂/kWh -35%
Emissioni per singolo run 318 gCO₂ 206 gCO₂ -112g (-35%)
Emissioni annuali (52 run) 16.5 kgCO₂ 10.7 kgCO₂ -5.8 kg (-35%)
Spostamento temporale medio 0 ore 6.2 ore N/A
Job eseguiti entro la finestra di 12 ore 100% 100% Invariato
Costo computazionale Invariato Invariato 0%

Il risultato di -35% di emissioni CO₂ è stato ottenuto semplicemente spostando il momento di esecuzione all'interno di una finestra di 12 ore già disponibile. Nessuna modifica al codice di training, nessun costo aggiuntivo, nessuna riduzione delle funzionalità. Questo è il punto di forza del carbon-aware computing: i benefici ambientali hanno un costo operativo prossimo a zero per i workload time-flexible.

Case Study 2: Multi-Region Deployment Carbon-Aware

Un secondo scenario riguarda il location shifting per pipeline di data processing che vengono eseguite in batch settimanali su un dataset distribuito globalmente.

Confronto Location Shifting: Emissioni per Region (job da 4 ore, 500W)

Regione Carbon Intensity Media Emissioni per Run (gCO₂) vs Best
asia-east1 (Taiwan) 545 gCO₂/kWh 1090 gCO₂ +11850% (peggior caso)
us-east-1 (Virginia) 380 gCO₂/kWh 760 gCO₂ +8160%
eu-west-1 (Irlanda) 280 gCO₂/kWh 560 gCO₂ +5933%
swedencentral (Azure) 18 gCO₂/kWh 36 gCO₂ +289%
europe-north1 (Finlandia) 9 gCO₂/kWh 9.2 gCO₂ BEST (-99% vs Taiwan)

Il location shifting verso europe-north1 al posto di asia-east1 riduce le emissioni di oltre il 99% per lo stesso identico workload. Ovviamente la fattibilità dipende da dove risiedono i dati (data gravity), dai requisiti di latenza e dalle normative sulla residenza dei dati (GDPR, ecc.). Ma per workload analytics su dataset trasferibili, questa è la leva più potente disponibile.

Confronto con le API dei Grid Operators Nazionali

Oltre ai provider supportati nativamente dal Carbon Aware SDK, esistono API pubbliche degli operatori di rete nazionali che forniscono dati di carbon intensity o mix energetico in tempo reale. Questi sono utili per chi vuole dati iper-localizzati o non vuole dipendere da provider commerciali.

API Pubbliche Grid Operators per Carbon Intensity

Operatore Paese API Dati Disponibili Note
Terna Italia transparency.terna.it Mix energetico RT, previsioni D+1 Gratuita, REST JSON
RTE Francia data.rte-france.com Mix energetico, emissioni CO₂, prezzi OAuth2, gratuita
CAISO California (USA) caiso.com/awe Renewable pct, carbon intensity Gratuita, XML/JSON
National Grid ESO UK carbonintensity.org.uk Carbon intensity regionale UK Gratuita, REST JSON, forecast
ENTSO-E Europa transparency.entsoe.eu Mix energetico 40+ paesi EU Registrazione, SFTP/API

Integrazione con l'API Carbon Intensity UK (National Grid ESO)

Il National Grid ESO del UK offre un'API pubblica gratuita particolarmente ben documentata, con dati regionali, previsioni e mix energetico per area. È un ottimo punto di partenza per chi vuole sperimentare senza costi.

"""
uk_carbon_intensity.py
Integrazione con API National Grid ESO (UK) per carbon intensity.
Alternativa gratuita per workload in regioni UK.
"""
import requests
from datetime import datetime, timezone


class UKCarbonIntensityClient:
    """
    Client per l'API pubblica National Grid ESO.
    Documentazione: https://carbon-intensity.github.io/api-definitions/
    """
    BASE_URL = "https://api.carbonintensity.org.uk"

    def get_current_intensity(self) -> dict:
        """Intensità carbonica attuale per UK nazionale."""
        response = requests.get(f"{self.BASE_URL}/intensity")
        response.raise_for_status()
        return response.json()["data"][0]

    def get_regional_intensity(self, region_id: int) -> dict:
        """
        Intensità per regione UK specifica.
        Region IDs: 1=North Scotland, 6=Yorkshire, 13=South East, ecc.
        """
        response = requests.get(f"{self.BASE_URL}/regional/regionid/{region_id}")
        response.raise_for_status()
        return response.json()["data"][0]

    def get_forecast_48h(self) -> list[dict]:
        """Forecast 48 ore per UK nazionale."""
        response = requests.get(f"{self.BASE_URL}/intensity/date")
        response.raise_for_status()
        return response.json()["data"]

    def find_optimal_window(self, duration_hours: int = 2) -> dict:
        """
        Trova la finestra a più bassa carbon intensity
        nelle prossime 48 ore.
        """
        forecast = self.get_forecast_48h()

        # Calcola media su finestra scorrevole
        best_window_start = None
        best_avg_intensity = float("inf")

        slots_per_window = duration_hours * 2  # Dati ogni 30 min

        for i in range(len(forecast) - slots_per_window + 1):
            window = forecast[i : i + slots_per_window]
            intensities = [
                slot["intensity"]["actual"] or slot["intensity"]["forecast"]
                for slot in window
                if (slot["intensity"]["actual"] or slot["intensity"]["forecast"]) is not None
            ]
            if not intensities:
                continue

            avg_intensity = sum(intensities) / len(intensities)

            if avg_intensity < best_avg_intensity:
                best_avg_intensity = avg_intensity
                best_window_start = window[0]["from"]

        return {
            "optimal_start": best_window_start,
            "avg_intensity_gco2_kwh": round(best_avg_intensity, 1),
            "duration_hours": duration_hours
        }


# Utilizzo:
client = UKCarbonIntensityClient()

# Intensità corrente
current = client.get_current_intensity()
print(f"Carbon intensity UK ora: {current['intensity']['actual']} gCO2/kWh")
print(f"Indice: {current['intensity']['index']}")  # very low / low / moderate / high / very high

# Finestra ottimale per job da 2 ore
optimal = client.find_optimal_window(duration_hours=2)
print(f"Finestra ottimale: {optimal['optimal_start']}")
print(f"Intensità media: {optimal['avg_intensity_gco2_kwh']} gCO2/kWh")

Limiti e Trade-off: Latenza vs Sostenibilità

Il carbon-aware computing non è una soluzione senza compromessi. È fondamentale capire i limiti pratici e i trade-off per non creare più problemi di quanti ne risolva.

Limiti da Considerare Prima dell'Adozione

  • Data gravity: Spostare il compute è facile, ma i dati potrebbero essere troppo voluminosi per essere trasferiti. Un job che accede a 10TB di dati in us-east-1 difficilmente conviene eseguirlo in eu-north-1 se il trasferimento emette più CO₂ del risparmio ottenuto.
  • Accuratezza delle previsioni: Le previsioni di carbon intensity a 24-72 ore hanno un margine di errore che aumenta con l'orizzonte temporale. Le condizioni meteorologiche (vento, sole) sono difficilmente prevedibili oltre le 6-12 ore con alta accuratezza.
  • Latency vs sustainability: Il time shifting introduce latenza intrinseca nei risultati. Se un team di data science aspetta i risultati di un training job, ritardarlo di 8 ore ha un costo produttivo reale.
  • Marginal vs average paradox: Come evidenziato da ricerche recenti, ottimizzare per segnale marginale e segnale medio può portare a decisioni opposte. Scegliere il segnale sbagliato per il proprio use case può persino aumentare le emissioni.
  • Rebound effect: Se il carbon-aware computing incentiva a consumare più compute durante le ore "verdi", l'effetto netto potrebbe essere zero o negativo (paradosso di Jevons applicato al software).
  • Copertura geografica: WattTime e ElectricityMaps hanno copertura eccellente in USA e EU, ma limitata in molti paesi emergenti dove i data center stanno crescendo rapidamente.

Matrice di Fattibilità per Tipo di Workload

Applicabilità del Carbon-Aware Computing per Tipo di Workload

Tipo Workload Time Shifting Location Shifting Motivazione
Training modelli ML Ottimo Ottimo Alta flessibilità, alta intensità energetica
Pipeline ETL batch Ottimo Buono Scheduler già presente, dipende da data locality
Backup e replica dati Ottimo Limitato Time-flexible, ma backup multi-region già pianificato
CI/CD pipeline non bloccanti Buono Buono Dipende dalla soglia di attesa accettata dal team
Generazione report Ottimo Limitato Alta flessibilità temporale, dipende da latenza SLA
API serving (real-time) Non applicabile Limitato SLA latenza non ammette spostamento temporale
Database transazionale Non applicabile Limitato Consistenza e latenza impediscono spostamento
Streaming real-time (Kafka) Non applicabile Non applicabile Continuous, latency-sensitive, stateful

Best Practices per l'Adozione del Carbon Aware SDK

Checklist Implementazione Carbon-Aware Computing

  • Identifica i workload time-flexible prima di tutto: cataloga i job batch con la loro finestra di flessibilità accettabile. Inizia con i job a più alto consumo energetico e massima flessibilità temporale.
  • Scegli il provider giusto per il tuo use case: WattTime per ottimizzazione delle emissioni marginali reali; ElectricityMaps per reporting ESG e Scope 2 market-based.
  • Definisci SLA chiari: specifica la finestra massima di attesa accettabile per ogni tipo di job. Un training ML può aspettare 12 ore; un report executive deve essere pronto prima delle 9:00.
  • Implementa fallback graceful: se il Carbon Aware SDK non è raggiungibile o il forecast non è disponibile, esegui il job normalmente. Non bloccare mai la produzione per un'ottimizzazione ambientale.
  • Misura e reporta: integra le metriche di carbon intensity nelle dashboard operativi. Mostra il risparmio accumulato per motivare il team e dimostrare il ROI ESG.
  • Combina time e location shifting: per massimizzare il risparmio, trova la combinazione ottimale di momento e regione, non ottimizzare le due dimensioni separatamente.
  • Attenzione al data transfer: considera le emissioni del trasferimento dati nel calcolo totale del location shifting. A volte la migliore regione "green" non è conveniente per via del costo di trasporto dei dati.
  • Deploy il Carbon Aware SDK come sidecar o servizio condiviso: non ogni team deve configurare il proprio deployment. Un'istanza centralizzata con caching riduce i costi e semplifica la gestione.

TypeScript: Utility Carbon-Aware per Next.js e Node.js

// carbon-aware.utils.ts
// Utility TypeScript per integrazione Carbon Aware SDK
// Usabile in Next.js (SSR/cron), Node.js, Deno

interface EmissionsData {
  location: string;
  timestamp: string;
  duration: number;
  rating: number;
}

interface OptimalWindow {
  optimalTimestamp: string;
  rating: number;
  currentRating: number;
  savingsPercentage: number;
}

export class CarbonAwareUtils {
  private readonly baseUrl: string;

  constructor(baseUrl: string = process.env.CARBON_AWARE_API_URL ?? 'http://localhost:8080') {
    this.baseUrl = baseUrl;
  }

  /**
   * Controlla se la carbon intensity corrente è sotto la soglia.
   * Usabile come guard per job schedulati.
   */
  async isCarbonIntensityAcceptable(
    location: string,
    maxIntensityGco2: number
  ): Promise<{ acceptable: boolean; current: number }> {
    try {
      const url = new URL(`${this.baseUrl}/emissions/bylocations`);
      url.searchParams.append('locations', location);

      const response = await fetch(url.toString(), {
        signal: AbortSignal.timeout(10_000)
      });

      if (!response.ok) {
        // Fail open: se l'API è down, procedi comunque
        console.warn(`Carbon Aware API error: ${response.status}. Proceeding anyway.`);
        return { acceptable: true, current: -1 };
      }

      const data: EmissionsData[] = await response.json();
      const current = data[0]?.rating ?? 0;

      return {
        acceptable: current <= maxIntensityGco2,
        current
      };
    } catch (error) {
      // Timeout o rete non disponibile: fail open
      console.warn('Carbon Aware API unreachable. Proceeding with job.', error);
      return { acceptable: true, current: -1 };
    }
  }

  /**
   * Trova la migliore regione cloud per eseguire un job ora.
   */
  async getBestRegion(locations: string[]): Promise<EmissionsData> {
    const url = new URL(`${this.baseUrl}/emissions/bylocation/best`);
    locations.forEach(loc => url.searchParams.append('locations', loc));

    const response = await fetch(url.toString());
    if (!response.ok) {
      throw new Error(`Carbon Aware API error: ${response.status}`);
    }

    return response.json() as Promise<EmissionsData>;
  }

  /**
   * Trova la finestra temporale ottimale per le prossime N ore.
   */
  async getOptimalWindow(
    location: string,
    jobDurationMinutes: number,
    searchHours: number = 24
  ): Promise<OptimalWindow | null> {
    const now = new Date();
    const searchEnd = new Date(now.getTime() + searchHours * 60 * 60 * 1000);

    const url = new URL(`${this.baseUrl}/emissions/forecasts/current`);
    url.searchParams.append('locations', location);
    url.searchParams.append('dataStartAt', now.toISOString());
    url.searchParams.append('dataEndAt', searchEnd.toISOString());
    url.searchParams.append('windowSize', jobDurationMinutes.toString());

    const response = await fetch(url.toString());
    if (!response.ok) return null;

    const forecasts = await response.json() as Array<{
      optimalDataPoints: Array<{ timestamp: string; rating: number }>;
      forecastData: Array<{ timestamp: string; rating: number }>;
    }>;

    const forecast = forecasts[0];
    if (!forecast?.optimalDataPoints?.length) return null;

    const optimal = forecast.optimalDataPoints[0];
    const currentRating = forecast.forecastData[0]?.rating ?? 0;
    const savingsPercentage = currentRating > 0
      ? Math.round(((currentRating - optimal.rating) / currentRating) * 100)
      : 0;

    return {
      optimalTimestamp: optimal.timestamp,
      rating: optimal.rating,
      currentRating,
      savingsPercentage
    };
  }
}

// Decorator per funzioni che supportano carbon-aware execution
export function carbonAware(
  location: string,
  maxIntensity: number = 300
) {
  return function (
    _target: object,
    _propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: unknown[]) {
      const client = new CarbonAwareUtils();
      const { acceptable, current } = await client.isCarbonIntensityAcceptable(location, maxIntensity);

      if (!acceptable) {
        console.log(`Job skipped: carbon intensity ${current} gCO2/kWh > threshold ${maxIntensity}`);
        return null;
      }

      console.log(`Job starting: carbon intensity ${current} gCO2/kWh (threshold: ${maxIntensity})`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

Conclusioni: Il Carbon-Aware Computing Come Standard Operativo

Il Carbon Aware SDK rappresenta oggi lo strumento più maturo e meglio supportato per implementare il demand shifting nei sistemi software aziendali. Con una API REST standard, supporto per i principali provider di dati (WattTime, ElectricityMaps) e integrazioni pronte per Kubernetes/KEDA e GitHub Actions, abbassa significativamente la barriera tecnica all'adozione del green software engineering.

I risultati sono concreti e documentati: riduzione del 20-45% delle emissioni per i workload batch con time shifting, fino al 99% con location shifting verso regioni europee alimentate da idroelettrico e nucleare. E il punto fondamentale è che questi risparmi hanno un costo operativo vicino a zero: non richiedono hardware diverso, non riducono le funzionalità dell'applicazione, non aumentano il costo del cloud.

La direzione è chiara: i principali cloud provider stanno integrando metriche di carbon intensity nelle loro console (AWS Customer Carbon Footprint Tool, Google Cloud Carbon Footprint, Azure Emissions Impact Dashboard). Il carbon-aware computing, oggi un differenziatore avanzato, diventerà nei prossimi anni un requisito standard per le organizzazioni soggette a normative ESG e CSRD. Iniziare ora significa acquisire competenze e infrastruttura con anticipo.

Prossimi Passi

  • Esplora il repository ufficiale su github.com/Green-Software-Foundation/carbon-aware-sdk e avvia il container Docker in locale con la configurazione JSON per fare i primi esperimenti senza API key.
  • Leggi l'articolo precedente della serie: Climatiq API: Carbon Intensity nei Sistemi Cloud, per capire come misurare le emissioni prima di ottimizzarle.
  • Prosegui con Scope 3 e Pipeline ESG per integrare i dati di carbon intensity nel reporting di sostenibilità aziendale richiesto dalla CSRD.
  • Se lavori con modelli ML, approfondisci AI Carbon Footprint dove vediamo come ottimizzare l'intero ciclo training + inferenza attraverso tecniche combinate di carbon-aware scheduling e model efficiency.
  • Iscriviti alla Green Software Foundation come supporter gratuito per accedere alla community, ai webinar tecnici e al programma di certificazione "Green Software Practitioner".

Risorse e Riferimenti

  • Carbon Aware SDK - Green Software Foundation: carbon-aware-sdk.greensoftware.foundation
  • GitHub Repository: github.com/Green-Software-Foundation/carbon-aware-sdk
  • Azure Carbon Aware KEDA Operator: github.com/Azure/carbon-aware-keda-operator
  • WattTime API Documentation: docs.watttime.org
  • ElectricityMaps API: portal.electricitymaps.com/developer-hub/api
  • UK Carbon Intensity API: carbonintensity.org.uk
  • Springer 2024: "Timeshifting strategies for carbon-efficient LLM training"
  • MDPI Sustainability 2025: "Carbon-Aware Spatio-Temporal Workload Shifting in Edge-Cloud"
  • Green Software Foundation SCI Specification: ISO/IEC 21031:2024

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti