DEV Community

ThankGod Chibugwum Obobo
ThankGod Chibugwum Obobo

Posted on • Originally published at actocodes.hashnode.dev

Green DevOps: How to Optimize Your Cloud Carbon Footprint Using Carbon-Aware SDKs

Cloud computing accounts for approximately 2–3% of global electricity consumption a figure that rivals the aviation industry and continues to grow as engineering teams scale infrastructure to meet demand. For most of that growth, carbon impact has been invisible, infrastructure decisions are made on cost, latency, and availability, with sustainability treated as someone else's problem.

Green DevOps changes the accounting. It integrates carbon awareness directly into the engineering decision loop, workload scheduling, region selection, CI/CD pipeline timing, and autoscaling policies, making the carbon cost of infrastructure decisions as visible and actionable as the financial cost.

The Carbon-Aware SDK, developed by the Green Software Foundation, provides the tooling layer that makes this practical. A standardized API for querying real-time and forecast carbon intensity data, enabling your applications and pipelines to make time and location-based decisions that minimize carbon emissions without sacrificing availability or performance.

This guide covers how to integrate carbon awareness into your cloud infrastructure and DevOps workflows, from SDK setup and workload time-shifting to Kubernetes scheduling policies and measurable sustainability metrics.

Understanding Carbon Intensity

Before implementing carbon-aware systems, understand what you're measuring:

Carbon intensity measures the amount of CO₂ equivalent (gCO₂eq) emitted per kilowatt-hour of electricity consumed. It varies dramatically by:

  • Location: A data center in Norway running on hydroelectric power has a carbon intensity of ~20 gCO₂eq/kWh. The same workload in a coal-heavy grid region might emit 700+ gCO₂eq/kWh.
  • Time: Renewable energy sources like solar and wind are intermittent. Carbon intensity on the same grid can vary by 3–5x between peak solar hours and overnight periods when fossil fuel plants pick up demand.

The two levers carbon-aware systems pull are:

Spatial shifting, running workloads in the region with the lowest current carbon intensity, where latency requirements permit.

Temporal shifting, delaying non-time-sensitive workloads (batch jobs, CI pipelines, data exports, model training) until carbon intensity in your region is lower, typically during high renewable generation periods.

The Carbon-Aware SDK

The Carbon-Aware SDK (Green Software Foundation) provides a unified interface to multiple carbon intensity data providers, Electricity Maps, WattTime, and the UK National Grid ESO, through a consistent REST API and CLI.

Installation and Setup

# Clone and run the Carbon-Aware SDK as a local API server
git clone https://github.com/Green-Software-Foundation/carbon-aware-sdk.git
cd carbon-aware-sdk/src/CarbonAware.WebApi

# Configure your data provider
cat > appsettings.json << EOF
{
  "DataSources": {
    "EmissionsDataSource": "ElectricityMaps",
    "ForecastDataSource": "ElectricityMaps"
  },
  "ElectricityMaps": {
    "APITokenHeader": "auth-token",
    "APIToken": "${ELECTRICITY_MAPS_API_TOKEN}",
    "BaseURL": "https://api.electricitymap.org/v3/"
  }
}
EOF

dotnet run
# API now running at http://localhost:5073
Enter fullscreen mode Exit fullscreen mode

Alternatively, deploy as a container alongside your infrastructure:

# docker-compose.yml
services:
  carbon-aware-api:
    image: ghcr.io/green-software-foundation/carbon-aware-sdk:latest
    ports:
      - "5073:5073"
    environment:
      DataSources__EmissionsDataSource: "ElectricityMaps"
      DataSources__ForecastDataSource: "ElectricityMaps"
      ElectricityMaps__APIToken: ${ELECTRICITY_MAPS_API_TOKEN}
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Querying Carbon Intensity

// src/carbon/carbon-aware.client.ts
export class CarbonAwareClient {
  private readonly baseUrl: string;

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

  // Get current carbon intensity for a location
  async getCurrentIntensity(location: string): Promise<number> {
    const response = await fetch(
      `${this.baseUrl}/emissions/current?location=${location}`
    );
    const data = await response.json();
    return data[0]?.rating ?? 0; // gCO₂eq/kWh
  }

  // Get forecast to find optimal execution window
  async getForecast(location: string, windowMinutes: number = 360): Promise<CarbonForecast[]> {
    const now = new Date();
    const end = new Date(now.getTime() + windowMinutes * 60 * 1000);

    const response = await fetch(
      `${this.baseUrl}/emissions/forecasts/current?` +
      `location=${location}&` +
      `dataStartAt=${now.toISOString()}&` +
      `dataEndAt=${end.toISOString()}&` +
      `windowSize=60`
    );
    const data = await response.json();
    return data[0]?.optimalDataPoints ?? [];
  }

  // Find the lowest-carbon region from a set of candidates
  async getBestRegion(locations: string[]): Promise<string> {
    const params = locations.map(l => `location=${l}`).join('&');
    const response = await fetch(
      `${this.baseUrl}/emissions/current?${params}`
    );
    const data = await response.json();

    return data.reduce((best: any, current: any) =>
      current.rating < best.rating ? current : best
    ).location;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 1 - Time-Shifting Batch Workloads

The highest-value application of carbon awareness is temporal shifting of batch workloads, jobs that must complete within a window but don't need to start immediately. Data exports, report generation, ML model training, database maintenance, and archive compression are all candidates.

// src/carbon/workload-scheduler.ts
import { CarbonAwareClient } from './carbon-aware.client';

interface WorkloadConfig {
  location: string;
  maxDelayMinutes: number;       // how long can we wait?
  carbonThreshold: number;       // only run if below this gCO₂eq/kWh
}

export class CarbonAwareScheduler {
  private readonly client: CarbonAwareClient;

  constructor() {
    this.client = new CarbonAwareClient();
  }

  async findOptimalExecutionTime(config: WorkloadConfig): Promise<Date> {
    const forecast = await this.client.getForecast(
      config.location,
      config.maxDelayMinutes
    );

    if (forecast.length === 0) {
      // No forecast data — run now rather than block indefinitely
      return new Date();
    }

    // Find the lowest carbon intensity window within our delay budget
    const optimal = forecast.reduce((best, current) =>
      current.value < best.value ? current : best
    );

    const optimalTime = new Date(optimal.timestamp);
    const maxWaitTime = new Date(Date.now() + config.maxDelayMinutes * 60 * 1000);

    // Don't wait longer than our deadline
    return optimalTime < maxWaitTime ? optimalTime : new Date();
  }

  async shouldRunNow(config: WorkloadConfig): Promise<boolean> {
    const current = await this.client.getCurrentIntensity(config.location);
    return current <= config.carbonThreshold;
  }
}
Enter fullscreen mode Exit fullscreen mode

Integrate the scheduler into your batch job runner:

// src/jobs/data-export.job.ts
import { CarbonAwareScheduler } from '../carbon/workload-scheduler';
import { scheduleJob } from 'node-schedule';

export async function scheduleDataExport() {
  const scheduler = new CarbonAwareScheduler();

  const optimalTime = await scheduler.findOptimalExecutionTime({
    location: 'eastus',
    maxDelayMinutes: 360,     // can delay up to 6 hours
    carbonThreshold: 200,     // prefer below 200 gCO₂eq/kWh
  });

  console.log(`Data export scheduled for: ${optimalTime.toISOString()}`);
  console.log(`Delay: ${Math.round((optimalTime.getTime() - Date.now()) / 60000)} minutes`);

  scheduleJob(optimalTime, async () => {
    console.log('Executing carbon-optimized data export...');
    await runDataExport();
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 2 - Carbon-Aware CI/CD Pipelines

CI/CD pipelines are one of the most consistently overlooked sources of cloud carbon emissions, running on every commit, often around the clock, with flexible timing requirements. A test suite that takes 15 minutes can run at 2am during low-carbon hours just as effectively as at 2pm during peak demand.

# .github/workflows/carbon-aware-ci.yml
name: Carbon-Aware CI Pipeline

on:
  push:
    branches: [main, develop]
  schedule:
    - cron: '0 */6 * * *'  # Check every 6 hours for deferred runs

jobs:
  check-carbon:
    name: Carbon Intensity Check
    runs-on: ubuntu-latest
    outputs:
      should-run: ${{ steps.carbon-check.outputs.should-run }}
      intensity: ${{ steps.carbon-check.outputs.intensity }}

    steps:
      - name: Check current carbon intensity
        id: carbon-check
        run: |
          INTENSITY=$(curl -s \
            "https://api.electricitymap.org/v3/carbon-intensity/latest?zone=US-MIDA" \
            -H "auth-token: ${{ secrets.ELECTRICITY_MAPS_TOKEN }}" \
            | jq '.carbonIntensity')

          echo "intensity=$INTENSITY" >> $GITHUB_OUTPUT

          # Run immediately for pushes to main regardless of carbon
          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
            echo "should-run=true" >> $GITHUB_OUTPUT
            echo "Main branch push — running regardless of carbon intensity ($INTENSITY gCO₂eq/kWh)"
          elif (( $(echo "$INTENSITY < 250" | bc -l) )); then
            echo "should-run=true" >> $GITHUB_OUTPUT
            echo "Carbon intensity acceptable: $INTENSITY gCO₂eq/kWh — running pipeline"
          else
            echo "should-run=false" >> $GITHUB_OUTPUT
            echo "Carbon intensity too high: $INTENSITY gCO₂eq/kWh — deferring pipeline"
          fi

  build-and-test:
    name: Build and Test
    runs-on: ubuntu-latest
    needs: check-carbon
    if: needs.check-carbon.outputs.should-run == 'true'

    steps:
      - uses: actions/checkout@v4

      - name: Log carbon context
        run: |
          echo "Running pipeline at carbon intensity: ${{ needs.check-carbon.outputs.intensity }} gCO₂eq/kWh"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build
Enter fullscreen mode Exit fullscreen mode

The key design decision: main branch pushes always run, carbon awareness is applied to non-critical developer branch pipelines, not to production deployments where timing is dictated by business requirements, not grid carbon intensity.

Step 3 - Carbon-Aware Kubernetes Scheduling

For Kubernetes workloads with flexible placement, integrate carbon intensity into pod scheduling decisions using a custom scheduler plugin or node labels updated by a controller:

// carbon-node-labeler/src/labeler.ts
// A controller that labels Kubernetes nodes with current carbon intensity zones
import { KubeConfig, CoreV1Api } from '@kubernetes/client-node';
import { CarbonAwareClient } from './carbon-aware.client';

const kc = new KubeConfig();
kc.loadFromDefault();
const k8sApi = kc.makeApiClient(CoreV1Api);
const carbonClient = new CarbonAwareClient();

const REGION_TO_LOCATION: Record<string, string> = {
  'us-east-1':    'eastus',
  'eu-west-1':    'northeurope',
  'ap-southeast-1': 'southeastasia',
};

async function labelNodesWithCarbonZone(): Promise<void> {
  const nodes = await k8sApi.listNode();

  for (const node of nodes.body.items) {
    const region = node.metadata?.labels?.['topology.kubernetes.io/region'];
    if (!region || !REGION_TO_LOCATION[region]) continue;

    const intensity = await carbonClient.getCurrentIntensity(REGION_TO_LOCATION[region]);

    // Classify carbon zone
    const zone = intensity < 150 ? 'green'
               : intensity < 300 ? 'yellow'
               : 'red';

    // Label node with current carbon zone
    await k8sApi.patchNode(node.metadata!.name!, {
      metadata: {
        labels: {
          'carbon.fdn.dev/intensity-zone': zone,
          'carbon.fdn.dev/intensity-value': Math.round(intensity).toString(),
        }
      }
    }, undefined, undefined, undefined, undefined, undefined, {
      headers: { 'Content-Type': 'application/merge-patch+json' }
    });
  }
}

// Run every 15 minutes
setInterval(labelNodesWithCarbonZone, 15 * 60 * 1000);
labelNodesWithCarbonZone();
Enter fullscreen mode Exit fullscreen mode

Use node affinity in batch job manifests to prefer green-zone nodes:

# k8s/batch-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: data-processing-job
spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          # Prefer green nodes — schedule here if available
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: carbon.fdn.dev/intensity-zone
                    operator: In
                    values: ["green"]
            - weight: 50
              preference:
                matchExpressions:
                  - key: carbon.fdn.dev/intensity-zone
                    operator: In
                    values: ["yellow"]

          # Never schedule on red-zone nodes for non-critical jobs
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: carbon.fdn.dev/intensity-zone
                    operator: NotIn
                    values: ["red"]

      containers:
        - name: data-processor
          image: your-org/data-processor:latest
Enter fullscreen mode Exit fullscreen mode

Step 4 - Spatial Shifting for Multi-Region Workloads

For workloads without strict data residency requirements, route to the lowest-carbon region at scheduling time:

// src/carbon/region-selector.ts
export class CarbonAwareRegionSelector {
  private readonly client: CarbonAwareClient;

  // Candidate regions for this workload
  private readonly candidateRegions = [
    { id: 'eastus',         name: 'us-east-1' },
    { id: 'northeurope',    name: 'eu-west-1' },
    { id: 'westus2',        name: 'us-west-2' },
    { id: 'australiaeast',  name: 'ap-southeast-2' },
  ];

  async selectOptimalRegion(): Promise<string> {
    const bestLocation = await this.client.getBestRegion(
      this.candidateRegions.map(r => r.id)
    );

    const region = this.candidateRegions.find(r => r.id === bestLocation);
    console.log(`Carbon-optimal region selected: ${region?.name} (${bestLocation})`);
    return region?.name ?? 'us-east-1';
  }
}

// Use in your infrastructure provisioning or job dispatch
const selector = new CarbonAwareRegionSelector();
const region = await selector.selectOptimalRegion();
await dispatchBatchJob({ region, jobConfig });
Enter fullscreen mode Exit fullscreen mode

Step 5 - Measuring and Reporting Carbon Impact

Carbon awareness only delivers accountability if emissions are measured and reported. The Cloud Carbon Footprint open-source tool aggregates billing data from AWS, GCP, and Azure into CO₂ estimates:

# Install Cloud Carbon Footprint
npx create-cloud-carbon-footprint-app@latest

# Configure AWS credentials
export AWS_ACCESS_KEY_ID=your-key
export AWS_SECRET_ACCESS_KEY=your-secret
export AWS_DEFAULT_REGION=us-east-1

# Run the estimation
npx ts-node packages/cli/src/CreateLookupTableAndEstimate.ts
Enter fullscreen mode Exit fullscreen mode

Build carbon metrics into your observability stack:

// src/carbon/carbon-metrics.ts
import { MeterProvider } from '@opentelemetry/sdk-metrics';

export class CarbonMetricsReporter {
  private readonly meter = new MeterProvider().getMeter('carbon-metrics');

  private readonly intensityGauge = this.meter.createObservableGauge(
    'carbon.grid.intensity',
    { description: 'Current grid carbon intensity (gCO₂eq/kWh)', unit: 'gCO2eq/kWh' }
  );

  private readonly emissionsCounter = this.meter.createCounter(
    'carbon.workload.emissions',
    { description: 'Estimated carbon emitted by workloads (gCO₂eq)', unit: 'gCO2eq' }
  );

  async recordWorkloadEmissions(
    workloadName: string,
    durationHours: number,
    powerWatts: number,
    location: string,
  ): Promise<void> {
    const intensity = await new CarbonAwareClient().getCurrentIntensity(location);

    // Emissions (gCO₂eq) = Power (kW) × Duration (h) × Intensity (gCO₂eq/kWh)
    const emissionsGrams = (powerWatts / 1000) * durationHours * intensity;

    this.emissionsCounter.add(emissionsGrams, {
      workload: workloadName,
      location,
    });

    console.log(`${workloadName}: estimated ${emissionsGrams.toFixed(2)} gCO₂eq`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Surface carbon metrics in Grafana alongside your standard engineering dashboards:

# Average carbon intensity by region over the last 24 hours
avg_over_time(carbon_grid_intensity{location="eastus"}[24h])

# Total estimated emissions from CI pipelines this week
sum(increase(carbon_workload_emissions{workload="ci-pipeline"}[7d]))

# Carbon savings from time-shifted jobs vs immediate execution baseline
(carbon_baseline_emissions - carbon_actual_emissions) / carbon_baseline_emissions * 100
Enter fullscreen mode Exit fullscreen mode

Step 6 - Green DevOps Principles Checklist

Beyond tooling, Green DevOps is a set of engineering practices:

Right-size before you optimize carbon. An oversized VM running at 5% CPU is wasting energy regardless of grid carbon intensity. Right-size your infrastructure first, carbon optimization on already-efficient infrastructure has a multiplied impact.

Prefer managed services. Cloud providers' managed services (RDS, Fargate, Cloud Functions) are typically more energy-efficient than self-managed equivalents at the same compute capacity, cloud hyperscalers achieve PUE (Power Usage Effectiveness) ratios that on-premise or self-managed rarely match.

Implement autoscaling aggressively. Scale to zero during off-hours for non-production environments. A staging environment running 24/7 at minimum capacity consumes energy around the clock, including during high-carbon grid periods.

Choose renewable-powered regions. AWS regions like eu-north-1 (Stockholm) and us-gov-west-1 run on high renewable percentages. GCP and Azure publish similar sustainability region data. When latency requirements permit region flexibility, factor carbon into the selection.

Add carbon to your architecture decision records. When making infrastructure decisions, region selection, instance type, deployment frequency, document the carbon trade-offs alongside cost and performance trade-offs. Visibility drives accountability.

Conclusion

Green DevOps is not a compliance exercise or a marketing initiative, it is an engineering discipline that optimizes cloud workloads against a metric that has been invisible in most infrastructure decision-making, carbon emissions.

The Carbon-Aware SDK makes the data available. Time-shifting batch jobs, deferring non-critical CI pipelines, routing spatially flexible workloads to lower-carbon regions, and scheduling Kubernetes batch jobs on green-zone nodes are all achievable with the patterns in this guide, with measurable impact on your infrastructure's carbon footprint.

Start with the highest-leverage intervention, identify your largest batch workloads and shift their execution timing by a few hours based on carbon intensity forecasts. Measure the before-and-after emissions. Once the pattern is proven, expand to CI pipeline scheduling, region selection policies, and Kubernetes node affinity rules.

Sustainable infrastructure is not slower infrastructure. Done right, it is simply infrastructure that knows what time it is, and chooses wisely.

What's your team's current approach to cloud sustainability — are you tracking emissions today? Share your stack and what you've measured in the comments.

Top comments (0)