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-1difficilmente conviene eseguirlo ineu-north-1se 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-sdke 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