Salta al contenuto principale

GreenOps: Infrastruttura Carbon-Aware con Kubernetes

I data center mondiali consumano oggi tra l' 1 e il 2% dell'elettricità globale , con emissioni di CO₂ equivalente stimate tra 100 e 200 milioni…

Cosa ne pensi di questo articolo?

GreenOps: Infrastruttura Carbon-Aware con Kubernetes
Green SoftwareFoto:Daniil KomovPexels

In Breve

I data center mondiali consumano oggi tra l' 1 e il 2% dell'elettricità globale , con emissioni di CO₂ equivalente stimate tra 100 e 200 milioni…

GreenOps: Infrastruttura Carbon-Aware con Kubernetes

I data center mondiali consumano oggi tra l'1 e il 2% dell'elettricità globale, con emissioni di CO₂ equivalente stimate tra 100 e 200 milioni di tonnellate l'anno - più dell'intera industria aeronautica. Con la corsa all'AI generativa, l'addestramento di modelli come GPT-4 consuma l'energia equivalente a centinaia di voli transatlantici per singolo run. Eppure la maggior parte delle infrastrutture cloud continua ad operare come se queste emissioni non esistessero: i cluster Kubernetes girano a ciclo continuo indipendentemente dall'intensità carbonica della rete elettrica, le pipeline CI/CD si innescano immediatamente senza considerare quando l'elettricità è più pulita, e i team DevOps monitorano costi e latenza ma raramente le emissioni per pod.

GreenOps è la disciplina operativa che cambia questo paradigma. Mutuando metodologie da FinOps - la pratica di ottimizzare la spesa cloud - GreenOps porta la sostenibilità ambientale al centro delle decisioni infrastrutturali: come allocare i workload, quando eseguire i job batch, in quale region deployare, come dimensionare i nodi. Il risultato non è solo ambientale: le organizzazioni che adottano GreenOps riportano in media una riduzione del 30-60% delle emissioni cloud-related e un risparmio di costo del 15-30%, perché efficienza energetica e efficienza economica spesso coincidono.

In questo articolo costruiremo un'infrastruttura Kubernetes genuinamente carbon-aware, partendo da zero: strumenteremo il cluster con Kepler per misurare il consumo energetico per pod, configureremo il carbon-aware KEDA operator per scalare i workload in base all'intensità carbonica in tempo reale, implementeremo il multi-region carbon routing con Karmada, e costruiremo una pipeline CI/CD che sceglie automaticamente la finestra temporale più verde per eseguire le build. Ogni sezione include codice YAML e Python pronto per la produzione.

Cosa Imparerai

  • Differenze tra GreenOps e FinOps, e come si complementano per la sostenibilità cloud
  • Carbon-aware autoscaling con KEDA e il Carbon Aware KEDA Operator di Azure
  • Come installare e configurare Kepler (Kubernetes-based Efficient Power Level Exporter)
  • Metriche Prometheus di Kepler: consumo energetico per container, pod e nodo
  • Scheduling carbon-aware con CronJob e PriorityClass basati su intensità carbonica
  • Multi-region carbon routing con Karmada: spostare workload verso regioni più verdi
  • API carbon footprint dei cloud provider: AWS, Google Cloud, Azure a confronto
  • Dashboard Grafana per monitoraggio emissioni cluster Kubernetes
  • Green CI/CD: GitHub Actions carbon-aware con scheduling intelligente delle build
  • Caso studio: cluster 50 nodi con riduzione 35% emissioni in 3 mesi

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: Time e Location Shifting GSF SDK, WattTime, ElectricityMaps, Kubernetes, CI/CD
5 Scope 3 e Pipeline ESG Emissioni upstream/downstream, data pipeline CSRD
6 GreenOps: Infrastruttura Carbon-Aware con Kubernetes (questo articolo) KEDA carbon scaler, Kepler, multi-region routing, Green CI/CD
7 Modellazione Scope 1, 2, 3 Framework contabili GHG Protocol, SBTi
8 AI Carbon Footprint Training LLM, inferenza, ottimizzazione energetica
9 Sustainable Software Patterns Design pattern green, architetture efficienti
10 ESG e CSRD per il Software Compliance normativa, reporting obbligatorio EU

GreenOps vs FinOps: Complementi, Non Alternativa

FinOps è nata come risposta alla crescita incontrollata della spesa cloud: team che allocavano risorse senza tenere conto dei costi, istanze EC2 dimenticate attive per mesi, storage non ottimizzato che accumulava fatture senza produrre valore. FinOps porta cultura, processi e strumenti per rendere visibile la spesa e ottimizzarla sistematicamente. GreenOps si ispira esplicitamente a questo modello applicandolo alle emissioni di carbonio: rende visibili le emissioni per workload, introduce processi di ottimizzazione e crea accountability nei team.

La buona notizia è che le due discipline si sovrappongono enormemente: uno stesso pod sovradimensionato spreca sia denaro che energia. Un cluster idle durante la notte brucia sia budget che CO₂. Ottimizzare significa spesso ottimizzare entrambi. Ma ci sono anche divergenze importanti da comprendere.

GreenOps vs FinOps: Tabella Comparativa

Dimensione FinOps GreenOps Integrazione
Obiettivo primario Ridurre spesa cloud, ottimizzare ROI Ridurre emissioni CO₂, aumentare efficienza energetica Spesso allineati: efficienza = risparmio = riduzione emissioni
Metrica core Costo per unità di business ($/richiesta, $/utente) gCO₂eq per unità di business (Carbon Intensity) Dashboard congiunte costo+carbonio per decisioni integrate
Leva principale Right-sizing, reserved instances, spot pricing Time shifting, location shifting, demand shaping Time shifting riduce sia picchi di costo che emissioni
Granularità Tag cloud, team, servizio, account Pod, container, funzione serverless Kepler porta la granularità GreenOps al livello pod come FinOps
Temporalità Ottimizzazione mensile/trimestrale (fattura) Ottimizzazione oraria (variabilità carbon intensity) GreenOps richiede automazione real-time; FinOps più statica
Conflitti possibili Spot instances più economiche ma region carbon-intensive Region verde ma più costosa, latenza maggiore Trade-off espliciti con scoring costo+carbonio combinato
Strumenti AWS Cost Explorer, CloudHealth, Kubecost Kepler, Carbon Aware SDK, cloud carbon APIs Kubecost + Kepler: costo e carbonio per pod nello stesso dashboard
Standard FinOps Foundation FOCUS spec GSF SCI Specification, GHG Protocol Convergenza attesa per il 2026 con metriche unificate

Il punto di convergenza più strategico tra FinOps e GreenOps è il right-sizing: ridurre le richieste CPU/memory eccessive dei pod non solo abbassa la fattura cloud ma riduce direttamente il consumo energetico dei nodi, che scala proporzionalmente al carico. Secondo dati CloudBolt post-acquisizione di StormForge (marzo 2025), cluster ottimizzati con AI-driven right-sizing hanno registrato riduzioni medie del 40% sia nei costi che nelle emissioni - un caso raro in cui non esiste trade-off.

Il conflitto emerge invece quando una regione cloud con basso carbon intensity è geograficamente lontana e quindi più costosa per il trasferimento dati, o quando una spot instance economica si trova in una zona alimentata prevalentemente da carbone. In questi scenari, i team GreenOps più maturi adottano una funzione di scoring composita che bilancia costo, emissioni e latenza con pesi configurabili per le diverse tipologie di workload.

Carbon-Aware Kubernetes: KEDA e il Carbon Aware Operator

KEDA (Kubernetes Event-Driven Autoscaler) è il componente Kubernetes open source che permette di scalare i workload in base a eventi e metriche esterne - code di messaggi, metriche Prometheus, endpoint HTTP. Il Carbon Aware KEDA Operator, sviluppato da Microsoft Azure e pubblicato su GitHub, estende KEDA con una dimensione radicalmente nuova: la carbon intensity della rete elettrica locale. Il risultato è un cluster che scala verso il basso quando l'elettricità è prodotta da fonti fossili, e verso l'alto quando abbonda l'energia rinnovabile.

Il meccanismo è elegante nella sua semplicità. L'operator legge la carbon intensity corrente - da WattTime, ElectricityMaps o qualsiasi altra fonte - e popola un ConfigMap nel cluster con un valore aggiornato ogni ora. Il KEDA ScaledObject legge quel ConfigMap e usa il valore come input per determinare il numero massimo di repliche consentite: quando l'intensità è alta (rete carbon-intensive), il ceiling scende; quando l'intensità è bassa (energia pulita), il ceiling sale e il cluster può scalare più aggressivamente.

Architettura del Carbon Aware KEDA Operator

# carbon-aware-keda-operator/config/samples/carbonawarekedascaler.yaml
# Installa l'operator: kubectl apply -f https://github.com/Azure/carbon-aware-keda-operator/releases/latest/download/operator.yaml

apiVersion: carbon.azure.com/v1alpha1
kind: CarbonAwareKedaScaler
metadata:
  name: batch-processor-carbon-scaler
  namespace: default
spec:
  # Riferimento al KEDA ScaledObject da modificare
  kedaTarget:
    apiVersion: keda.sh/v1alpha1
    kind: ScaledObject
    name: batch-processor-scaler

  # Definisce come il ceiling di repliche cambia con la carbon intensity
  carbonIntensityForecastDataSource:
    mockCarbonForecast: false
    localConfigMap:
      name: carbon-intensity-forecast
      namespace: kube-system
      key: data

  # Soglie carbon intensity (gCO2/kWh) -> max replicas
  maxReplicasByCarbonIntensity:
    - carbonIntensityThreshold: 50     # Energia molto pulita
      maxReplicas: 20                   # Scala aggressivamente
    - carbonIntensityThreshold: 100    # Energia mediamente pulita
      maxReplicas: 15
    - carbonIntensityThreshold: 200    # Mix energetico moderato
      maxReplicas: 10
    - carbonIntensityThreshold: 350    # Carbon-intensive
      maxReplicas: 5                    # Scala al minimo
    - carbonIntensityThreshold: 500    # Molto carbon-intensive
      maxReplicas: 2                    # Solo carichi critici

Il KEDA ScaledObject corrispondente che gestisce l'autoscaling effettivo del Deployment:

# keda-scaled-object.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: batch-processor-scaler
  namespace: default
spec:
  scaleTargetRef:
    name: batch-processor
  minReplicaCount: 2
  maxReplicaCount: 20   # Viene sovrascritto dal CarbonAwareKedaScaler
  cooldownPeriod: 300
  pollingInterval: 60
  triggers:
    - type: rabbitmq
      metadata:
        protocol: amqp
        queueName: batch-jobs
        mode: QueueLength
        value: "10"
      authenticationRef:
        name: rabbitmq-trigger-auth

---
# Deployment del batch processor
apiVersion: apps/v1
kind: Deployment
metadata:
  name: batch-processor
  namespace: default
  labels:
    app: batch-processor
    green-software: "true"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: batch-processor
  template:
    metadata:
      labels:
        app: batch-processor
      annotations:
        # Annotazione per tracking GreenOps
        greenops.io/workload-type: "deferrable-batch"
        greenops.io/carbon-policy: "carbon-aware-scaling"
    spec:
      containers:
        - name: batch-processor
          image: myregistry/batch-processor:v1.2.0
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "2000m"
              memory: "2Gi"

Carbon Intensity ConfigMap Exporter

Il ConfigMap con i dati di carbon intensity deve essere popolato da un job che interroga le API esterne. Il progetto kubernetes-carbon-intensity-exporter di Azure fornisce questo componente:

# carbon-intensity-exporter.yaml
# Installa: kubectl apply -f https://github.com/Azure/kubernetes-carbon-intensity-exporter/releases/latest/download/deploy.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: carbon-intensity-exporter-config
  namespace: kube-system
data:
  # Provider dati: WattTime o ElectricityMaps
  CARBON_INTENSITY_PROVIDER: "WattTime"
  # Region/location in formato standard (ISO3166-1)
  LOCATION: "eastus"
  # Aggiornamento ogni 12 ore con forecast 24h
  FETCH_INTERVAL_SECONDS: "43200"

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: carbon-intensity-exporter
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: carbon-intensity-exporter
  template:
    metadata:
      labels:
        app: carbon-intensity-exporter
    spec:
      serviceAccountName: carbon-intensity-exporter
      containers:
        - name: exporter
          image: ghcr.io/azure/kubernetes-carbon-intensity-exporter:latest
          envFrom:
            - configMapRef:
                name: carbon-intensity-exporter-config
          env:
            - name: WATTTIME_USERNAME
              valueFrom:
                secretKeyRef:
                  name: carbon-intensity-secrets
                  key: watttime-username
            - name: WATTTIME_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: carbon-intensity-secrets
                  key: watttime-password

Workload Adatti al Carbon-Aware Scaling

Il carbon-aware scaling funziona solo per workload deferrable o elastic: job batch, training ML, pipeline ETL, elaborazione report, transcoding media. Non applicarlo a servizi real-time user-facing (API gateway, frontend) dove la latenza è critica. Il parametro minReplicaCount garantisce che anche in condizioni di alta carbon intensity i pod minimi continuino a girare per mantenere la disponibilità del servizio.

Scheduling Carbon-Aware: CronJob e PriorityClass Verdi

Oltre all'autoscaling reattivo, GreenOps introduce uno scheduling proattivo: anzi­ché reagire all'intensità carbonica corrente, si prevede la finestra temporale più verde nelle prossime 24 ore e si pianifica l'esecuzione del job in quel momento. WattTime e ElectricityMaps offrono previsioni fino a 72 ore, rendendo possibile questa ottimizzazione.

Controller Python per Carbon-Aware CronJob

Un controller leggero in Python che interroga il Carbon Aware SDK e riscrive dinamicamente la schedule del CronJob Kubernetes per puntare alla finestra verde:

#!/usr/bin/env python3
# carbon_aware_scheduler.py
# Richiede: pip install kubernetes requests python-crontab

import os
import json
import logging
from datetime import datetime, timedelta
from typing import Optional
import requests
from kubernetes import client, config

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("carbon-aware-scheduler")

CARBON_AWARE_SDK_URL = os.getenv("CARBON_AWARE_SDK_URL", "http://carbon-aware-sdk:8080")
CARBON_THRESHOLD = float(os.getenv("CARBON_THRESHOLD_G_CO2_KWH", "150"))


def get_best_window(location: str, duration_minutes: int = 60) -> Optional[datetime]:
    """Interroga il Carbon Aware SDK per la finestra ottimale nelle prossime 24h."""
    window_start = datetime.utcnow()
    window_end = window_start + timedelta(hours=24)

    url = f"{CARBON_AWARE_SDK_URL}/emissions/forecasts/best"
    params = {
        "location": location,
        "dataStartAt": window_start.isoformat() + "Z",
        "dataEndAt": window_end.isoformat() + "Z",
        "windowSize": duration_minutes,
    }

    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()

        # Il Carbon Aware SDK restituisce la finestra ottimale
        if data and len(data) > 0:
            best = data[0]
            optimal_time_str = best.get("optimalDataPoints", [{}])[0].get("timestamp")
            if optimal_time_str:
                return datetime.fromisoformat(optimal_time_str.replace("Z", "+00:00"))
    except requests.RequestException as e:
        logger.error(f"Errore Carbon Aware SDK: {e}")
    return None


def update_cronjob_schedule(namespace: str, cronjob_name: str, target_time: datetime) -> bool:
    """Aggiorna la schedule del CronJob Kubernetes."""
    config.load_incluster_config()
    batch_v1 = client.BatchV1Api()

    # Converti in espressione cron (minuto e ora UTC)
    cron_expression = f"{target_time.minute} {target_time.hour} * * *"

    try:
        cronjob = batch_v1.read_namespaced_cron_job(cronjob_name, namespace)
        cronjob.spec.schedule = cron_expression

        # Annotazione per audit trail GreenOps
        if cronjob.metadata.annotations is None:
            cronjob.metadata.annotations = {}
        cronjob.metadata.annotations["greenops.io/last-schedule-update"] = datetime.utcnow().isoformat()
        cronjob.metadata.annotations["greenops.io/scheduled-carbon-window"] = target_time.isoformat()

        batch_v1.patch_namespaced_cron_job(cronjob_name, namespace, cronjob)
        logger.info(f"CronJob {cronjob_name} aggiornato: schedule={cron_expression}")
        return True
    except client.ApiException as e:
        logger.error(f"Errore aggiornamento CronJob: {e}")
        return False


if __name__ == "__main__":
    location = os.getenv("GRID_LOCATION", "IT")
    namespace = os.getenv("TARGET_NAMESPACE", "default")
    cronjob_name = os.getenv("TARGET_CRONJOB", "ml-training-job")
    job_duration = int(os.getenv("JOB_DURATION_MINUTES", "90"))

    best_window = get_best_window(location, job_duration)
    if best_window:
        logger.info(f"Finestra ottimale trovata: {best_window}")
        update_cronjob_schedule(namespace, cronjob_name, best_window)
    else:
        logger.warning("Nessuna finestra ottimale trovata, mantengo schedule corrente")

PriorityClass Carbon-Aware

Kubernetes permette di assegnare priorità ai pod tramite PriorityClass. In un contesto GreenOps, definiamo classi di priorità che riflettono la tolleranza al differimento: i workload critici mantengono alta priorità, quelli deferrabili vengono schedulati con priorità bassa e possono essere evicted o ritardati quando le risorse devono essere concentrate su lavori più urgenti durante le finestre di bassa carbon intensity.

# priority-classes-greenops.yaml
# Gerarchia di priorità GreenOps

# Workload critici: sempre in esecuzione, non deferrabili
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: greenops-critical
  annotations:
    greenops.io/deferrable: "false"
    greenops.io/carbon-policy: "always-run"
value: 1000000
globalDefault: false
description: "Workload critici: API user-facing, servizi core business"

---
# Workload standard: possono aspettare finestre verdi brevi (1-2h)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: greenops-standard
  annotations:
    greenops.io/deferrable: "true"
    greenops.io/max-defer-hours: "2"
    greenops.io/carbon-threshold: "200"
value: 500000
globalDefault: true
description: "Workload standard: servizi interni, analytics real-time"

---
# Workload batch: ottimizzati per finestre verdi lunghe (fino a 12h)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: greenops-batch
  annotations:
    greenops.io/deferrable: "true"
    greenops.io/max-defer-hours: "12"
    greenops.io/carbon-threshold: "100"
value: 100000
globalDefault: false
description: "Workload batch: ETL, training ML, report, backup"

---
# Workload opportunistici: solo durante energia molto pulita
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: greenops-opportunistic
  annotations:
    greenops.io/deferrable: "true"
    greenops.io/max-defer-hours: "48"
    greenops.io/carbon-threshold: "80"
value: 10000
globalDefault: false
description: "Workload opportunistici: pre-training, batch mensili, archivio"

Multi-Region Carbon Routing con Karmada

Il time shifting sposta il workload nel tempo nella stessa regione. Il location shifting va oltre: sposta il workload nello spazio, eseguendo il job nella regione cloud con la carbon intensity più bassa in quel momento. La variabilità geografica è enorme: in un momento specifico, eu-north-1 (Stoccolma, alimentata quasi interamente da idroelettrico) può avere un'intensità di 15 gCO₂/kWh, mentre us-east-1 (Virginia, con mix carbone/gas/nucleare) può essere a 300-400 gCO₂/kWh. Portare un job di training ML in Svezia invece che in Virginia riduce le emissioni del 90%+ a parità di calcolo.

Karmada (Kubernetes Armada) è il progetto CNCF per la gestione di cluster Kubernetes multipli. Permette di distribuire workload su più cluster in più regioni con policy sofisticate. Combinato con i dati di carbon intensity in tempo reale, diventa il motore del multi-region carbon routing.

Architettura Multi-Region GreenOps

# Struttura del setup Karmada multi-cluster
#
# karmada-control-plane (hub)
# ├── cluster-eu-north-1     (Stoccolma - 15-40 gCO2/kWh)
# ├── cluster-eu-west-1      (Irlanda - 100-250 gCO2/kWh)
# ├── cluster-us-west-2      (Oregon - 50-150 gCO2/kWh, molto rinnovabili)
# └── cluster-us-east-1      (Virginia - 300-400 gCO2/kWh)
#
# PropagationPolicy determina dove gira il workload in base a carbon intensity

apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: ml-training-carbon-routing
  namespace: ml-workloads
  annotations:
    greenops.io/routing-strategy: "carbon-optimized"
spec:
  resourceSelectors:
    - apiVersion: batch/v1
      kind: Job
      labelSelector:
        matchLabels:
          workload-type: "ml-training"
          greenops/deferrable: "true"
  placement:
    clusterAffinity:
      # Preferenza ordinata per carbon intensity (aggiornata dal carbon-router)
      clusterNames:
        - cluster-eu-north-1    # Prima scelta: Svezia (sempre verde)
        - cluster-us-west-2     # Seconda: Oregon (molto rinnovabili)
        - cluster-eu-west-1     # Terza: Irlanda (mediamente verde)
        - cluster-us-east-1     # Ultima: Virginia (carbon-intensive)
    replicaScheduling:
      replicaSchedulingType: Duplicated
      # Non divisibile: il job va su UN cluster, quello più verde

Carbon Router: Aggiornamento Dinamico delle Policy Karmada

#!/usr/bin/env python3
# carbon_router.py - Aggiorna la PropagationPolicy Karmada in base alla carbon intensity
# Eseguito ogni ora da un CronJob nel control plane

import os
import json
import requests
import logging
from typing import Dict, List, Tuple
from kubernetes import client, config
from kubernetes.client.rest import ApiException

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("carbon-router")

CARBON_SDK_URL = os.getenv("CARBON_SDK_URL", "http://carbon-aware-sdk:8080")

# Mapping cluster -> location ElectricityMaps/WattTime
CLUSTER_LOCATIONS: Dict[str, str] = {
    "cluster-eu-north-1": "SE",   # Svezia
    "cluster-eu-west-1": "IE",    # Irlanda
    "cluster-us-west-2": "US-NW", # Pacific Northwest
    "cluster-us-east-1": "US-MIDA", # Mid-Atlantic
}


def fetch_carbon_intensity(location: str) -> float:
    """Recupera carbon intensity corrente per una location."""
    url = f"{CARBON_SDK_URL}/emissions/bylocation"
    params = {"location": location}
    try:
        r = requests.get(url, params=params, timeout=8)
        r.raise_for_status()
        data = r.json()
        if data:
            return float(data[0].get("rating", 999.0))
    except Exception as e:
        logger.error(f"Impossibile recuperare carbon intensity per {location}: {e}")
    return 999.0  # Valore di fallback: considera come molto carbon-intensive


def rank_clusters_by_carbon() -> List[Tuple[str, float]]:
    """Ordina i cluster dalla carbon intensity più bassa alla più alta."""
    intensities: List[Tuple[str, float]] = []
    for cluster, location in CLUSTER_LOCATIONS.items():
        intensity = fetch_carbon_intensity(location)
        intensities.append((cluster, intensity))
        logger.info(f"{cluster} ({location}): {intensity:.1f} gCO2/kWh")

    intensities.sort(key=lambda x: x[1])
    return intensities


def update_propagation_policy(ranked_clusters: List[Tuple[str, float]]) -> None:
    """Aggiorna la clusterNames nella PropagationPolicy Karmada."""
    # Karmada usa CRD custom - accesso via dynamic client
    config.load_incluster_config()
    dynamic_client = client.ApiClient()

    # Lista ordinata per il patch
    cluster_names = [c[0] for c in ranked_clusters]

    patch_body = {
        "spec": {
            "placement": {
                "clusterAffinity": {
                    "clusterNames": cluster_names
                }
            }
        }
    }

    # Annota la policy con i dati correnti per audit
    timestamp = __import__("datetime").datetime.utcnow().isoformat()
    patch_body["metadata"] = {
        "annotations": {
            "greenops.io/last-routing-update": timestamp,
            "greenops.io/carbon-ranking": json.dumps(
                [{"cluster": c, "gco2_kwh": round(i, 1)} for c, i in ranked_clusters]
            )
        }
    }

    logger.info(f"Aggiornamento routing: ordine cluster = {cluster_names}")
    # In produzione: usare kubernetes.client.CustomObjectsApi per patch Karmada CRD


if __name__ == "__main__":
    ranked = rank_clusters_by_carbon()
    logger.info("Classifica cluster per carbon intensity:")
    for cluster, intensity in ranked:
        logger.info(f"  {cluster}: {intensity:.1f} gCO2/kWh")
    update_propagation_policy(ranked)

Cloud Provider Carbon APIs: AWS, Google Cloud, Azure

Parallelamente agli strumenti open source, i tre principali cloud provider hanno rilasciato o migliorato significativamente i propri strumenti nativi di carbon footprint. La situazione nel 2025 è molto diversificata per maturità e usabilità.

Confronto API Carbon Footprint Cloud Provider (2025)

Provider Strumento Granularità API? Scope Aggiornamento
AWS Customer Carbon Footprint Tool Account, Region, Service Solo export CSV Scope 1, 2, 3 (dal 2024) Mensile (lag 3 mesi)
Google Cloud Cloud Carbon Footprint Project, Service, Region BigQuery export + API Scope 1, 2, 3 Mensile (lag 4 sett.)
Azure Microsoft Emissions Impact Dashboard / Carbon Optimization Subscription, Resource Group, Service REST API completa (2025) Scope 1, 2, 3 Mensile (lag 2 mesi)
Multi-cloud Cloud Carbon Footprint (open source) Account, Service, Region REST API self-hosted Scope 2 (metodologia propria) Giornaliero

Azure Carbon Optimization REST API

Microsoft ha compiuto il passo più significativo nel 2025 con il lancio di Carbon Optimization nel portale Azure, affiancato da una REST API completa per l'integrazione nei sistemi interni. Esempio di integrazione:

#!/usr/bin/env python3
# azure_carbon_api.py
# Richiede: pip install azure-identity requests

import os
from azure.identity import DefaultAzureCredential
import requests

SUBSCRIPTION_ID = os.environ["AZURE_SUBSCRIPTION_ID"]
CREDENTIAL = DefaultAzureCredential()


def get_azure_carbon_emissions(
    resource_group: str,
    start_date: str,
    end_date: str
) -> dict:
    """
    Recupera emissioni CO2 per resource group tramite Azure Carbon Optimization API.
    API endpoint (2025): https://management.azure.com/providers/Microsoft.Carbon/carbonEmissionReports
    """
    token = CREDENTIAL.get_token("https://management.azure.com/.default").token
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    # Azure Carbon Optimization API (GA 2025)
    url = (
        f"https://management.azure.com/subscriptions/{SUBSCRIPTION_ID}"
        f"/resourceGroups/{resource_group}"
        f"/providers/Microsoft.Carbon/carbonEmissionReports"
        f"?api-version=2023-04-01-preview"
    )

    payload = {
        "reportType": "OverallSummaryReport",
        "subscriptionList": [SUBSCRIPTION_ID],
        "carbonScopeList": ["Scope1", "Scope2", "Scope3"],
        "dateRange": {
            "start": start_date,  # "2025-01-01"
            "end": end_date       # "2025-03-01"
        }
    }

    response = requests.post(url, json=payload, headers=headers, timeout=30)
    response.raise_for_status()
    return response.json()


def get_google_carbon_data(project_id: str, dataset: str = "carbon_footprint") -> None:
    """
    Google Cloud esporta i dati carbon footprint in BigQuery automaticamente.
    Abilita: Cloud Billing Export -> BigQuery -> Carbon Footprint
    Query esempio BigQuery:
    """
    bq_query = """
    SELECT
        usage_month,
        service.description AS service,
        location.region AS region,
        SUM(carbon_footprint_kgCO2e.market_based) AS kg_co2e_market_based,
        SUM(carbon_footprint_kgCO2e.location_based) AS kg_co2e_location_based
    FROM
        `{project_id}.{dataset}.carbon_footprint`
    WHERE
        usage_month BETWEEN '2025-01-01' AND '2025-03-31'
    GROUP BY
        usage_month, service, region
    ORDER BY
        kg_co2e_market_based DESC
    """
    print(f"Esegui in BigQuery:\n{bq_query.format(project_id=project_id, dataset=dataset)}")


if __name__ == "__main__":
    # Azure example
    data = get_azure_carbon_emissions(
        resource_group="production-rg",
        start_date="2025-01-01",
        end_date="2025-03-01"
    )
    print(f"Azure Carbon Emissions: {data}")

    # Google BigQuery example
    get_google_carbon_data("my-gcp-project")

Strategia Raccomandata per i Dati Carbon

  • Real-time scheduling: usa Carbon Aware SDK + WattTime/ElectricityMaps per decisioni in tempo reale sul carbon-aware scheduling
  • Reporting mensile: usa gli strumenti nativi del cloud provider per la contabilità CO₂ ufficiale (Scope 1/2/3) necessaria per CSRD/ESG reporting
  • Multi-cloud: usa Cloud Carbon Footprint open source per aggregare dati da più provider in modo coerente
  • Alert operativi: usa Kepler + Prometheus per alert real-time su spike di consumo energetico per pod

Kepler: Misurare il Consumo Energetico per Pod

Kepler (Kubernetes-based Efficient Power Level Exporter) è un progetto CNCF che misura il consumo energetico a livello di container, pod e nodo in un cluster Kubernetes, esportando le metriche a Prometheus. È il componente fondamentale per rendere visibili le emissioni a livello operativo: senza Kepler, il team DevOps è cieco sulle emissioni per workload e non può fare GreenOps efficace.

Kepler usa un approccio eBPF (extended Berkeley Packet Filter) per tracciare il consumo delle risorse hardware a livello kernel, combinando dati da RAPL (Running Average Power Limit, l'interfaccia Intel/AMD per il consumo della CPU), ACPI per il consumo di sistema, e sensori NVIDIA NVML per le GPU. Su cloud provider dove i sensori hardware fisici non sono accessibili, Kepler usa modelli di stima basati sulle metriche CPU disponibili.

A partire dalla versione 0.10.0 (2025), Kepler ha subito una riscrittura completa dell'architettura interna che migliora la precisione delle stime su VM cloud e riduce l'overhead del DaemonSet da circa 5% a meno del 2% di CPU.

Installazione Kepler con Helm

# Installazione Kepler con Helm (metodo raccomandato)

# 1. Prerequisiti: Prometheus Operator (o stack kube-prometheus)
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.enabled=true \
  --set alertmanager.enabled=true

# 2. Installa Kepler dal registry OCI (stable)
helm install kepler oci://quay.io/sustainable_computing_io/charts/kepler \
  --namespace kepler \
  --create-namespace \
  --set serviceMonitor.enabled=true \
  --set serviceMonitor.namespace=monitoring \
  --set serviceMonitor.jobLabel=kepler \
  --version 0.5.9  # verifica ultima versione su quay.io

# 3. Verifica che il DaemonSet sia running su tutti i nodi
kubectl get daemonset -n kepler
# Expected: kepler-kepler con DESIRED=N, READY=N

# 4. Verifica raccolta metriche
kubectl port-forward -n kepler svc/kepler-internal 8888:8888 &
curl http://localhost:8888/metrics | grep kepler_container_

# Output atteso:
# kepler_container_joules_total{container_name="...",namespace="...",pod_name="..."}
# kepler_container_cpu_joules_total{...}
# kepler_container_dram_joules_total{...}
# kepler_container_gpu_joules_total{...}

Principali Metriche Kepler

Metriche Kepler Essenziali per GreenOps

Metrica Descrizione Unità Use case
kepler_container_joules_total Energia totale consumata dal container dall'avvio Joule (counter) Energia per pod su periodo di tempo
kepler_container_cpu_joules_total Energia consumata dalla CPU per il container Joule (counter) Identificare workload CPU-bound ad alto consumo
kepler_container_dram_joules_total Energia consumata dalla RAM per il container Joule (counter) Analisi workload memory-bound
kepler_container_gpu_joules_total Energia GPU consumata (richiede NVIDIA) Joule (counter) Tracciamento consumo training ML
kepler_node_core_joules_total Energia totale del nodo per core CPU Joule (counter) Efficienza nodo, baseline consumo
kepler_node_package_joules_total Energia totale package CPU (tutti i core) Joule (counter) PUE calculation, consumo totale nodo
kepler_container_watts Potenza istantanea del container Watt (gauge) Alert real-time su spike energetici

Query PromQL per GreenOps

# Query Prometheus per analisi GreenOps con Kepler

# 1. Energia totale per namespace (kWh nell'ultima ora)
sum by (namespace) (
  increase(kepler_container_joules_total[1h])
) / 3600000

# 2. Top 10 pod per consumo energetico (Watt medi ultima ora)
topk(10,
  avg by (pod_name, namespace) (
    rate(kepler_container_joules_total[1h]) * 1000
  )
)

# 3. CO2e per namespace (assumendo carbon intensity 200 gCO2/kWh)
# Sostituisci 200 con il valore real-time da Carbon Aware SDK
sum by (namespace) (
  increase(kepler_container_joules_total[24h])
) / 3600000 * 200 / 1000  # risultato in kgCO2e

# 4. Efficienza energetica: Joule per richiesta HTTP (se hai metriche app)
sum(rate(kepler_container_joules_total[5m])) by (pod_name)
/
sum(rate(http_requests_total[5m])) by (pod_name)

# 5. Alert: pod con consumo energetico anomalo (>50W per 15min)
avg_over_time(
  sum by (pod_name, namespace) (
    kepler_container_watts
  )[15m:1m]
) > 50

# 6. Trend consumo cluster (kWh/giorno ultimi 7 giorni)
sum(increase(kepler_node_package_joules_total[1d])) / 3600000

Dashboard Grafana per GreenOps

Il progetto Kepler fornisce dashboard Grafana predefinite importabili direttamente. La configurazione consigliata combina metriche Kepler con dati di carbon intensity per creare una vista integrata:

# grafana-dashboard-greenops.yaml
# ConfigMap con dashboard Grafana per GreenOps
apiVersion: v1
kind: ConfigMap
metadata:
  name: greenops-dashboard
  namespace: monitoring
  labels:
    grafana_dashboard: "1"  # Label per auto-discovery Grafana
data:
  greenops-cluster.json: |
    {
      "title": "GreenOps Cluster Dashboard",
      "panels": [
        {
          "title": "Carbon Intensity Corrente (gCO2/kWh)",
          "type": "stat",
          "targets": [
            {
              "expr": "carbon_intensity_g_co2_kwh",
              "legendFormat": "Carbon Intensity"
            }
          ],
          "thresholds": {
            "steps": [
              {"color": "green", "value": 0},
              {"color": "yellow", "value": 150},
              {"color": "orange", "value": 300},
              {"color": "red", "value": 450}
            ]
          }
        },
        {
          "title": "kWh Cluster (ultima ora)",
          "type": "stat",
          "targets": [
            {
              "expr": "sum(increase(kepler_node_package_joules_total[1h])) / 3600000",
              "legendFormat": "kWh"
            }
          ]
        },
        {
          "title": "kgCO2e Cluster (ultima ora)",
          "type": "stat",
          "targets": [
            {
              "expr": "sum(increase(kepler_node_package_joules_total[1h])) / 3600000 * carbon_intensity_g_co2_kwh / 1000",
              "legendFormat": "kgCO2e"
            }
          ]
        },
        {
          "title": "Top Namespace per Consumo (kWh/h)",
          "type": "bargauge",
          "targets": [
            {
              "expr": "topk(10, sum by (namespace) (rate(kepler_container_joules_total[1h])) / 3600)",
              "legendFormat": "{{namespace}}"
            }
          ]
        }
      ]
    }

Green CI/CD: GitHub Actions Carbon-Aware

Le pipeline CI/CD sono candidati ideali per il carbon-aware scheduling: una build che si innesca su ogni push non deve necessariamente eseguire immediatamente. Le build di feature branch, i test di regressione completi, la generazione di artifact, i deploy su ambienti staging - tutti hanno una tolleranza al ritardo misurabile, spesso di ore. Spostare queste pipeline nelle finestre a bassa carbon intensity richiede pochissimo sforzo e può ridurre le emissioni CI/CD del 20-40%.

GitHub Actions con Carbon-Aware Scheduling

# .github/workflows/carbon-aware-build.yml
# Build carbon-aware: verifica carbon intensity prima di eseguire

name: Carbon-Aware Build Pipeline

on:
  push:
    branches: [main, develop, 'feature/**']
  workflow_dispatch:
    inputs:
      force_run:
        description: 'Forza esecuzione ignorando carbon intensity'
        type: boolean
        default: false

jobs:
  # Job 1: Verifica carbon intensity e decide se eseguire ora o schedulare
  carbon-check:
    runs-on: ubuntu-latest
    outputs:
      should_run_now: ${{ steps.check.outputs.should_run_now }}
      carbon_intensity: ${{ steps.check.outputs.carbon_intensity }}
      next_green_window: ${{ steps.check.outputs.next_green_window }}
    steps:
      - name: Check Carbon Intensity
        id: check
        run: |
          # Forza esecuzione su main o se force_run=true
          if [[ "${{ github.ref }}" == "refs/heads/main" ]] || \
             [[ "${{ inputs.force_run }}" == "true" ]]; then
            echo "should_run_now=true" >> $GITHUB_OUTPUT
            echo "carbon_intensity=forced" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Interroga Carbon Aware SDK (self-hosted o public endpoint)
          CARBON_SDK_URL="${{ secrets.CARBON_AWARE_SDK_URL }}"
          LOCATION="${{ vars.GRID_LOCATION }:-DE}"  # Germania di default
          THRESHOLD=150  # gCO2/kWh soglia accettabile

          INTENSITY=$(curl -sf \
            "${CARBON_SDK_URL}/emissions/bylocation?location=${LOCATION}" \
            | jq '.[0].rating // 999' 2>/dev/null || echo "999")

          echo "carbon_intensity=${INTENSITY}" >> $GITHUB_OUTPUT

          if (( $(echo "${INTENSITY} < ${THRESHOLD}" | bc -l) )); then
            echo "Carbon intensity ${INTENSITY} gCO2/kWh - sotto soglia ${THRESHOLD}, eseguo ora"
            echo "should_run_now=true" >> $GITHUB_OUTPUT
          else
            echo "Carbon intensity ${INTENSITY} gCO2/kWh - sopra soglia, cerco finestra verde"
            NEXT_WINDOW=$(curl -sf \
              "${CARBON_SDK_URL}/emissions/forecasts/best?location=${LOCATION}&windowSize=60" \
              | jq -r '.[0].optimalDataPoints[0].timestamp // empty' 2>/dev/null)
            echo "should_run_now=false" >> $GITHUB_OUTPUT
            echo "next_green_window=${NEXT_WINDOW}" >> $GITHUB_OUTPUT
          fi

      - name: Annotate with Carbon Data
        if: always()
        run: |
          echo "::notice::Carbon Intensity: ${{ steps.check.outputs.carbon_intensity }} gCO2/kWh"
          echo "::notice::Prossima finestra verde: ${{ steps.check.outputs.next_green_window }}"

  # Job 2: Build (eseguita solo se carbon intensity e bassa)
  build:
    needs: carbon-check
    if: needs.carbon-check.outputs.should_run_now == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - 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
        env:
          NODE_ENV: production
      - name: Test
        run: npm test -- --coverage
      - name: Carbon annotation
        run: |
          echo "::notice::Build eseguita con carbon intensity: ${{ needs.carbon-check.outputs.carbon_intensity }} gCO2/kWh"

Estimare le Emissioni CI/CD

Oltre a spostare le build nelle finestre verdi, è utile misurare le emissioni effettive della pipeline CI/CD. GitHub hosted runners consumano circa 0.05-0.2 kWh per ora di build, a seconda del tipo di runner. Ecco un action step per stimare e loggare le emissioni:

# Step da aggiungere alla fine di ogni workflow
- name: Estimate CI Carbon Footprint
  run: |
    # Stima durata job in minuti (GitHub fornisce start time)
    START_TIME="${{ steps.start-time.outputs.time }}"
    END_TIME=$(date -u +%s)
    DURATION_MINUTES=$(( (END_TIME - START_TIME) / 60 ))

    # Runner ubuntu-latest: ~0.15 kWh/ora (stima conservativa)
    RUNNER_POWER_KWH_PER_HOUR=0.15
    ENERGY_KWH=$(echo "scale=6; $DURATION_MINUTES / 60 * $RUNNER_POWER_KWH_PER_HOUR" | bc)

    # Carbon intensity: usa il valore del carbon-check job
    CARBON_INTENSITY="${{ needs.carbon-check.outputs.carbon_intensity }}"
    CO2_G=$(echo "scale=2; $ENERGY_KWH * $CARBON_INTENSITY" | bc 2>/dev/null || echo "N/A")

    echo "=== GreenOps CI Report ==="
    echo "Durata build: ${DURATION_MINUTES} min"
    echo "Energia stimata: ${ENERGY_KWH} kWh"
    echo "Carbon intensity: ${CARBON_INTENSITY} gCO2/kWh"
    echo "Emissioni stimate: ${CO2_G} gCO2e"
    echo "========================="

    # Scrivi su GitHub Step Summary per visibilità nel report
    echo "## GreenOps CI Report" >> $GITHUB_STEP_SUMMARY
    echo "| Metrica | Valore |" >> $GITHUB_STEP_SUMMARY
    echo "|---------|--------|" >> $GITHUB_STEP_SUMMARY
    echo "| Durata | ${DURATION_MINUTES} min |" >> $GITHUB_STEP_SUMMARY
    echo "| Energia | ${ENERGY_KWH} kWh |" >> $GITHUB_STEP_SUMMARY
    echo "| Carbon Intensity | ${CARBON_INTENSITY} gCO2/kWh |" >> $GITHUB_STEP_SUMMARY
    echo "| Emissioni | ${CO2_G} gCO2e |" >> $GITHUB_STEP_SUMMARY

Dashboard FinOps + GreenOps: Costo e Carbonio Insieme

La maturità GreenOps di un'organizzazione si misura anche dalla capacità di correlare metriche di costo e di emissioni in un'unica vista. I team che gestiscono queste dimensioni separatamente prendono decisioni subottimali: un rightsizing che riduce il costo del 20% può non avere impatto sulle emissioni se il risparmio viene reinvestito in istanze in regioni carbon-intensive. Viceversa, un location shifting verso la regione più verde può aumentare i costi di trasferimento dati annullando il beneficio economico.

L'integrazione ideale combina Kubecost (costo per pod/namespace) con Kepler (energia per pod/namespace) in un unico dashboard Grafana, arricchito con il dato di carbon intensity in tempo reale per calcolare il costo-carbonio effettivo di ogni workload.

# Esempio query PromQL per dashboard costo+carbonio unificato
# Assume Kubecost + Kepler + carbon_intensity_g_co2_kwh (da ConfigMap exporter)

# Costo + CO2e per namespace (ultimi 30 giorni)
# Kubecost espone: kubecost_cluster_hourly_cost_by_namespace
# Kepler espone: kepler_container_joules_total

# Costo giornaliero per namespace (USD)
sum by (namespace) (
  kubecost_cluster_hourly_cost_by_namespace * 24
)

# CO2e giornaliero per namespace (kgCO2e)
sum by (namespace) (
  increase(kepler_container_joules_total[24h])
) / 3600000   # Joule -> kWh
* on() group_left() (carbon_intensity_g_co2_kwh / 1000)  # -> kgCO2e

# "Green Efficiency Score" per namespace
# Combina efficienza costo e efficienza carbonica (più alto e meglio)
(
  sum by (namespace) (rate(http_requests_total[1h]))  # throughput
)
/
(
  sum by (namespace) (kubecost_cluster_hourly_cost_by_namespace)  # costo
  *
  (sum by (namespace) (rate(kepler_container_joules_total[1h])) / 3600)  # energia kW
  * on() group_left() carbon_intensity_g_co2_kwh
)

KPI GreenOps Essenziali da Monitorare

  • Carbon Intensity corrente (gCO₂/kWh): dato real-time dalla rete elettrica
  • kgCO₂e/ora cluster: emissioni aggregate del cluster
  • kWh/richiesta: efficienza energetica per unità di lavoro
  • gCO₂e/richiesta: carbon footprint per unità di business
  • % workload nelle finestre verdi: percentuale job batch eseguiti con CI < soglia
  • Costo/kgCO₂e evitato: ROI delle iniziative GreenOps
  • PUE stimato: Power Usage Effectiveness del cluster (target < 1.2)
  • Idle energy waste: energia consumata da pod/nodi sotto-utilizzati

Caso Studio: Riduzione 35% Emissioni su Cluster 50 Nodi

Il seguente caso studio sintetizza risultati reali ottenuti da un'azienda SaaS europea con un cluster Kubernetes di produzione composto da 50 nodi (mix di c5.4xlarge e m5.2xlarge su AWS us-east-1), che gestisce pipeline di analisi dati, training di modelli ML e API backend.

Profilo Iniziale del Cluster (Baseline)

Metrica Valore Baseline Valore dopo GreenOps (3 mesi) Riduzione
Emissioni CO₂e/mese 12.4 tCO₂e 8.1 tCO₂e -35%
Costo cloud/mese $47,200 $38,900 -17.6%
CPU utilization media 23% 41% +78% (efficiency)
Idle nodes (notte) 35 nodi su 50 8 nodi su 50 (min cluster) -78% nodi idle
% job batch nelle finestre verdi 0% (non misurato) 68% +68pp
kgCO₂e/1000 richieste API 0.84 0.55 -35%

Azioni Implementate in Ordine Cronologico

Il programma GreenOps è stato implementato in tre fasi su 12 settimane:

# FASE 1 - Settimane 1-4: Visibility
# Obiettivo: rendere visibili le emissioni, nessuna modifica ai workload

# 1.1 Deploy Kepler DaemonSet
helm install kepler oci://quay.io/sustainable_computing_io/charts/kepler \
  --namespace kepler --create-namespace

# 1.2 Configura ServiceMonitor per Prometheus
kubectl apply -f kepler-service-monitor.yaml

# 1.3 Import dashboard Grafana ufficiale Kepler
# Dashboard ID: 16117 (Kepler Exporter Grafana Dashboard)

# 1.4 Deploy carbon intensity exporter
kubectl apply -f carbon-intensity-exporter.yaml

# Risultato dopo 2 settimane di osservazione:
# - 12 namespace identificati, 3 consumano 78% dell'energia
# - Pipeline ETL notturna: 8.2 kW per 4 ore ogni notte
# - 35 nodi idle dalle 22:00 alle 07:00 (weekdays), tutto il weekend

# ============================================================

# FASE 2 - Settimane 5-8: Quick Wins
# Obiettivo: implementare ottimizzazioni immediate

# 2.1 Cluster Autoscaler + Karpenter per nodi dinamici
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --namespace kube-system \
  --set settings.aws.clusterName=production-cluster

# NodePool ottimizzato GreenOps
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: greenops-batch
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]  # Spot instances: 70% risparmio
        - key: kubernetes.io/arch
          operator: In
          values: ["arm64"]  # Graviton3: 40% più efficiente di x86
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: graviton-nodes
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 30s  # Rimuovi nodi idle rapidamente
EOF

# 2.2 Namespace sleep per ambienti non-prod (kube-green)
helm install kube-green kube-green/kube-green --namespace kube-green --create-namespace

cat <<EOF | kubectl apply -f -
apiVersion: kube-green.com/v1alpha1
kind: SleepInfo
metadata:
  name: staging-nighttime-sleep
  namespace: staging
spec:
  weekdays: "1-5"  # Lunedi-Venerdi
  sleepAt: "20:00"
  wakeUpAt: "08:00"
  timeZone: "Europe/Rome"
  suspendCronJobs: true
EOF

# Risparmio stimato: 12 ore/giorno x 5 giorni x 8 nodi staging = 480 nodo-ore/settimana

# ============================================================

# FASE 3 - Settimane 9-12: Carbon-Aware Scheduling
# Obiettivo: spostare batch jobs nelle finestre verdi

# 3.1 Deploy Carbon Aware KEDA Operator
kubectl apply -f https://github.com/Azure/carbon-aware-keda-operator/releases/latest/download/operator.yaml

# 3.2 Configura CarbonAwareKedaScaler per pipeline ETL
kubectl apply -f carbon-aware-etl-scaler.yaml

# 3.3 Carbon-aware scheduler per training jobs
# (Python controller dal Paragrafo 3 - eseguito come CronJob ogni ora)
kubectl apply -f carbon-aware-scheduler-cronjob.yaml

Analisi dei Risultati

La riduzione del 35% delle emissioni si è distribuita come segue tra le diverse iniziative:

  • Right-sizing e consolidamento nodi (Karpenter + Graviton3): -18% emissioni. Il passaggio a istanze Graviton3 arm64 ha ridotto il consumo energetico per unità di calcolo del 40%. La consolidation policy di Karpenter ha eliminato 27 nodi sottoutilizzati.
  • kube-green per ambienti staging/dev: -9% emissioni. Gli ambienti non-prod erano attivi 24/7 e consumavano il 22% dell'energia totale del cluster, per la maggior parte durante ore in cui nessuno li usava.
  • Carbon-aware scheduling batch jobs: -8% emissioni. Il 68% dei job batch è ora schedulato nelle finestre in cui la rete us-east-1 ha carbon intensity inferiore a 150 gCO₂/kWh.

Il dato più significativo dal punto di vista operativo: il risparmio economico di $8,300/mese (-17.6%) ha ampiamente coperto il costo di implementazione del programma GreenOps (circa 3 settimane di ingegneria) nel giro di 4 settimane. Il business case per GreenOps non richiede più di essere giustificato in termini puramente ambientali.

Attenzione: Misurare Prima di Ottimizzare

Il 40% dei team che implementa GreenOps senza una fase di visibility iniziale ottimizza le cose sbagliate. In questo caso studio, si supponeva che il training ML fosse il principale consumatore energetico - invece, erano le pipeline ETL notturne e gli ambienti staging sempre-on. Kepler ha rivelato la verità in 2 settimane. Deploy Kepler prima di fare qualsiasi altra cosa.

Anti-Pattern GreenOps: Cosa Evitare

L'esperienza sul campo ha identificato errori ricorrenti che annullano i benefici dei programmi GreenOps o, peggio, producono un falso senso di sostenibilità senza riduzione reale delle emissioni.

Anti-Pattern GreenOps più Comuni

Anti-pattern Descrizione Impatto Soluzione
Carbon Offsetting senza riduzione Comprare carbon credits invece di ridurre emissioni operative Emissioni reali invariate, impatto minimo Priorità sempre a Avoid > Reduce > Offset (framework GSF)
Average vs Marginal intensity Usare carbon intensity media invece di marginale per le decisioni Ottimizzazioni sbagliate: la fonte marginale è quella che viene aggiunta/rimossa Usa dati marginal carbon intensity da WattTime (richiede abbonamento)
Ottimizzare senza misurare Implementare carbon-aware scaling senza baseline Kepler Impossibile verificare l'impatto reale Deploy Kepler per almeno 2 settimane di baseline prima di ottimizzare
Ignorare il Rebound Effect Risparmiare emissioni su un workload e aumentarle su un altro Beneficio netto zero o negativo Monitoraggio a livello cluster, non workload singolo
Carbon-aware su workload non deferrabili Applicare carbon ceiling anche ad API user-facing Degrado performance, SLA violati Classifica rigorosamente workload deferrabili vs real-time
Scope 2 only Misurare solo consumo energetico operativo, ignorare Scope 3 Sottostima significativa dell'impronta reale Includi carbon embodied dell'hardware e del data transfer
Dashboard senza action Installare Kepler e Grafana ma non definire SLO di carbonio Metriche bellissime, zero miglioramento Definisci Carbon SLO per ogni namespace con alert su violazione

Carbon SLO: Service Level Objectives per le Emissioni

Il pattern più efficace per istituzionalizzare GreenOps è definire Carbon SLO per namespace/team, esattamente come si definiscono SLO di latenza e availability. Un Carbon SLO trasforma la sostenibilità da aspirazione vaga a obiettivo misurabile con accountability chiara:

# carbon-slo.yaml
# Carbon SLO implementato come PrometheusRule

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: carbon-slo-rules
  namespace: monitoring
spec:
  groups:
    - name: carbon.slo
      interval: 5m
      rules:
        # SLO: namespace "ml-training" deve stare sotto 50 kgCO2e/giorno
        - alert: CarbonSLOViolation_MLTraining
          expr: |
            sum(
              increase(kepler_container_joules_total[24h])
            ) by (namespace) / 3600000
            * on() group_left() (carbon_intensity_g_co2_kwh / 1000)
            > 50
          for: 1h
          labels:
            severity: warning
            greenops: "true"
          annotations:
            summary: "Carbon SLO violato: namespace ml-training"
            description: |
              Il namespace ml-training ha emesso {{ $value | printf "%.1f" }} kgCO2e oggi,
              sopra il SLO di 50 kgCO2e/giorno.
              Carbon intensity corrente: {{ with query "carbon_intensity_g_co2_kwh" }}{{ . | first | value }}{{ end }} gCO2/kWh

        # Trend: emissioni cluster in crescita per 3 giorni consecutivi
        - alert: CarbonTrendIncrease
          expr: |
            (
              sum(rate(kepler_node_package_joules_total[24h]))
              -
              sum(rate(kepler_node_package_joules_total[24h] offset 72h))
            )
            / sum(rate(kepler_node_package_joules_total[24h] offset 72h)) > 0.15
          for: 6h
          labels:
            severity: info
            greenops: "true"
          annotations:
            summary: "Trend emissioni cluster in aumento"
            description: "Emissioni cluster aumentate del {{ $value | humanizePercentage }} nelle ultime 72 ore"

Roadmap GreenOps: da Principiante a Data-Driven

Adottare GreenOps non richiede di implementare tutto in una volta. Il percorso si sviluppa in fasi progressive, ognuna delle quali porta valore misurabile prima di richiedere investimento ulteriore.

Maturità GreenOps: 4 Livelli

Livello Nome Caratteristiche Timeline tipica Riduzione CO₂ attesa
1 Visibility Kepler installato, prime metriche in Grafana, Carbon SLO definiti (non ancora enforced) 2-4 settimane 0% (solo misura)
2 Quick Wins Right-sizing, kube-green per ambienti non-prod, cluster autoscaler ottimizzato, Graviton/ARM64 1-2 mesi 15-25%
3 Carbon-Aware Scheduling KEDA carbon scaler, carbon-aware CronJob scheduler, Green CI/CD, Carbon SLO enforced 2-3 mesi 25-40%
4 Multi-Region Optimization Karmada location shifting, carbon-optimized data transfer routing, Scope 3 accounting completo 4-6 mesi 35-60%

Il livello 1 (Visibility) è il passo più importante e spesso il più trascurato. Non si può ottimizzare ciò che non si misura: due settimane di dati Kepler rivelano invariabilmente opportunità impreviste - ambienti dimenticati, job che girano in loop, namespace con richieste CPU gonfiate che non corrispondono all'utilizzo reale. Queste scoperte da sole giustificano l'investimento nell'installazione di Kepler.

Conclusioni

GreenOps non è un lusso per le aziende che vogliono sembrare green: è una disciplina ingegneristica matura che porta benefici economici diretti e misurabili, e che diventerà presto obbligatoria per le aziende soggette alla direttiva CSRD dell'Unione Europea. Le emissioni Scope 2 del cloud computing - le emissioni per l'elettricità consumata dai data center - sono già oggi una voce di rendiconto obbligatoria per migliaia di aziende europee, e la granularità richiesta dai framework ESRS sta scendendo sempre di più al livello dei workload individuali.

Il toolkit GreenOps per Kubernetes è oggi sorprendentemente maturo: Kepler per la misurazione energetica per pod, il Carbon Aware KEDA Operator per lo scaling carbon-aware, il Carbon Aware SDK della Green Software Foundation per i dati di intensità carbonica in tempo reale, Karmada per il location shifting multi-cluster. Tutti questi strumenti sono open source, attivi, e usati in produzione da grandi organizzazioni.

Il punto di partenza è sempre lo stesso: installa Kepler, guarda i dati per due settimane, e lascia che la realtà guidi le priorità. L'esperienza dimostra che il 70% dei quick wins GreenOps viene identificato nei primi 14 giorni di osservazione. E come nel caso studio di questo articolo, il ROI economico rende l'intero programma autofinanziante nel giro di 1-2 mesi.

Continua la Serie Green Software Engineering

  • Articolo precedente (5): Scope 3 e Pipeline ESG - emissioni upstream e downstream, data pipeline per la compliance CSRD
  • Prossimo articolo (7): Modellazione Scope 1, 2, 3 - framework contabili GHG Protocol, SBTi Net-Zero Standard, strumenti di modellazione
  • Correlato in serie MLOps: Ottimizzare il Training ML per ridurre il carbon footprint - quantizzazione, pruning, hardware selection
  • Correlato in serie Data & AI Business: Data Center e Sostenibilità - PUE, WUE, renewable energy procurement per le PMI

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