Skip to content

Enrichment modules: AWS and Azure service coverage

SPRUCE generates its estimates by chaining EnrichmentModules. An EnrichmentModule is the unit of extension in SPRUCE. Each module reads columns from the CUR input row and/or from values set by earlier modules, then writes its results into a shared map. The pipeline materialises one output row per CUR row at the end, avoiding per-module row copies.

The enrichment modules are listed and configured in a configuration file, one per cloud provider and report format. If no configuration is specified, SPRUCE uses the bundled default for the active provider (e.g. default-config-aws.json for a native AWS CUR, or default-config-aws-focus.json / default-config-azure-focus.json for an AWS or Azure FOCUS report). See Configure the modules for instructions on how to modify the enrichment modules, and Write a module to add your own. The list of columns generated by the modules can be found in the SpruceColumn class.

Native and FOCUS report formats

A FOCUS export carries mostly the same values as the provider's native export under different column names (e.g. for Azure x_SkuMeterCategory instead of MeterCategory, for AWS x_Operation instead of line_item_operation and SkuMeter instead of line_item_usage_type). The provider modules therefore work for both formats: when the report format is FOCUS, they automatically rebind their input columns to the FOCUS names. Two AWS specificities: x_ServiceCode carries the CUR product code (line_item_product_code), and the columns with no FOCUS equivalent are derived from SkuMeter — the instance type from meters like BoxUsage:m5.large and the data transfer category from the meter suffixes (DataTransfer-Regional-Bytes, AWS-Out-Bytes, ...).

The pipeline at a glance

The modules run in sequence and fall into five stages. Later stages consume the columns written by earlier ones, which is why the order in the configuration file matters.

flowchart LR
    A[Billing row] --> B("<b>Metadata</b><br>RegionExtraction")
    B --> C("<b>Energy &amp; embodied</b><br>Storage, Networking,<br>Serverless, Accelerators,<br>Boavizta, EcoLogits")
    C --> D("<b>Factors</b><br>PWUE,<br>AverageCarbonIntensity")
    D --> E("<b>Impacts</b><br>OperationalEmissions,<br>Water")
    E --> F("<b>Normalisation</b><br>FOCUSColumns<br>(native formats only)")
    F --> G[Enriched row]
Module Providers Writes Based on
RegionExtraction AWS, Azure, FOCUS region
Storage AWS, Azure operational_energy_kwh, embodied_emissions_co2eq_g Cloud Carbon Footprint for energy, Boavizta and vendor LCAs for embodied
Networking AWS, Azure operational_energy_kwh Boavizta coefficients
Serverless AWS operational_energy_kwh Tailpipe
Accelerators AWS operational_energy_kwh Cloud Carbon Footprint
Compute — Boavizta AWS, Azure operational_energy_kwh, embodied_emissions_co2eq_g, embodied_adp_sbeq_g BoaviztAPI
LLM inference — EcoLogits AWS, Azure operational_energy_kwh, embodied_emissions_co2eq_g EcoLogits
PWUE AWS, Azure power_usage_effectiveness, water_usage_effectiveness Provider-published data
AverageCarbonIntensity AWS, Azure carbon_intensity Ember
OperationalEmissions AWS, Azure operational_emissions_co2eq_g
Water AWS, Azure water_cooling_l, water_electricity_production_l, water_consumption_stress_area_l WRI
FOCUSColumns AWS, Azure (native formats) FOCUS columns

Stage 1 — Metadata extraction

RegionExtraction

Extracts the region information from the provider-specific input columns and stores it in a standard location, where all the following modules can find it. The FOCUS variant is provider-neutral: it reads the standard RegionId column.

Classes com.digitalpebble.spruce.modules.aws.RegionExtraction
com.digitalpebble.spruce.modules.azure.RegionExtraction
com.digitalpebble.spruce.modules.focus.RegionExtraction
Writes region

Stage 2 — Energy and embodied estimates

The modules in this stage estimate the energy used by a row of usage and, for Storage, Boavizta and EcoLogits, the related embodied emissions. Each module handles a different kind of usage (storage, networking, compute, …), so they complement each other rather than overlap.

Storage

Estimates the energy used for storage by applying a flat coefficient per GB, following the approach of the Cloud Carbon Footprint project, and the embodied emissions of the drives holding the data, amortised over their service life. Service-specific replication factors are applied to both, since the same bytes occupy that many times more physical drives. On Azure, managed disks are estimated from their provisioned capacity.

Classes com.digitalpebble.spruce.modules.ccf.aws.Storage
com.digitalpebble.spruce.modules.ccf.azure.Storage
Writes operational_energy_kwh
embodied_emissions_co2eq_g

Energy configuration (in Wh per TB-hour):

Key Default Description
hdd_coefficient_tb_h 0.65 Energy per TB-hour for HDD storage
ssd_coefficient_tb_h 1.2 Energy per TB-hour for SSD storage

Embodied emissions configuration:

Key Default Description
hdd_embodied_kg_per_drive 30.0 Embodied emissions of one hard drive, in kg CO2eq
hdd_capacity_gb 15000.0 Capacity assumed for one hard drive
ssd_embodied_kg_per_gb 0.055 Embodied emissions of an SSD, per GB of capacity
storage_lifetime_hours 43800.0 Service life the embodied emissions are amortised over (5 years)

The two media are modelled on different bases. A hard drive costs roughly the same to manufacture whatever its capacity, since the platters, motor, actuator, casing and PCB are near-fixed for a 3.5" unit and areal density does the work, so its embodied emissions are a constant per drive divided by an assumed capacity. An SSD's die area scales with capacity, so its figure is a rate per GB. With the defaults that works out at 0.40 kg CO2eq per TB-year for HDD and 11 kg for SSD.

Note that hdd_capacity_gb describes the physical drive rather than a provisioned volume, so it is not the same thing as an Azure Managed Disk size or an EBS volume size.

Data sources for the embodied figures:

Figure Value Source
HDD, per drive 31.11 kg CO2eq BoaviztAPI HDD component, from Umweltbundesamt, Green Cloud Computing 2021
HDD, Seagate Exos X22 LCA 28.7 kg CO2eq for a 22 TB drive Tailpipe manufacture methodology
HDD, per TB-year 0.27 kg CO2eq Seagate, hard drives and data centre sustainability
HDD, 24 vendor LCAs 0.02 kg CO2eq/GB over a 512 GB to 6 TB sample Tannu & Nair, The Dirty Secret of SSDs: Embodied Carbon
SSD, die-area formula 0.052 kg CO2eq/GB BoaviztAPI SSD component
SSD, 3D NAND study 0.056 kg CO2eq/GB Tailpipe manufacture methodology, from Embodied Carbon Footprint of 3D NAND Memories
Drive capacity mix 15 TB installed-fleet average Backblaze Drive Stats 2025

The first four rows converge on roughly 30 kg per drive across a 40x range of capacities, which is itself the evidence for treating HDD embodied carbon as capacity independent. The Tannu & Nair rate cannot be applied per GB to current hardware: it encodes the drive sizes of a pre-2023 corpus and overstates per-byte embodied carbon by about an order of magnitude. See issue #102 for the full derivation.

Networking

Estimates the energy used for networking in and out of data centres. The module distinguishes between three transfer types with separate coefficients, taken from the Boavizta Cloud Emissions Working Group.

In a CUR, the transfer type is read from the transfer_type product attribute of the AWSDataTransfer lines. A FOCUS report carries neither, so the category is derived from the SkuMeter suffixes instead (DataTransfer-Regional-Bytes and DataTransfer-xAZ-*-Bytes → intra, AWS-*-Bytes and CloudFront-*-Bytes → inter, DataTransfer-In/Out-Bytes → external), which map one-to-one to the CUR transfer types.

Classes com.digitalpebble.spruce.modules.aws.Networking
com.digitalpebble.spruce.modules.azure.Networking
Writes operational_energy_kwh

Configuration — the network_coefficients_kwh_gb map (in kWh/GB):

Transfer type Key Default Description
Intra-region intra 0.001 Traffic within the same region
Inter-region inter 0.0015 Traffic between regions
External extra 0.059 Traffic to/from the internet (Inbound / Outbound)

Attributing networking emissions

The relevance and usefulness of attributing emissions for networking based on usage is subject for debate, as the energy use of networking is pretty constant independently of traffic. The consequences of reducing networking are probably negligible but since the approach in SPRUCE is attributional, we do the same for networking in order to be consistent.

Serverless

Estimates the energy for the memory and vCPU usage of serverless services like Fargate, EMR Serverless or DocumentDB Elastic, which bill compute in vCPU-hours and GB-hours rather than by instance type. The default coefficients are taken from the Tailpipe methodology.

DocumentDB Elastic clusters bill their compute as ElasticCPUUsage in vCPU-hours; the storage and backup lines of the same clusters are covered by Storage instead. AWS does not document the underlying hardware, so the x86 coefficient is applied.

Class com.digitalpebble.spruce.modules.aws.Serverless
Writes operational_energy_kwh

Configuration:

Key Default Description
memory_coefficient_kwh 0.0000598 kWh per GB of memory
arm_cpu_coefficient_kwh 0.00191015625 kWh per vCPU (ARM)
x86_cpu_coefficient_kwh 0.0088121875 kWh per vCPU (x86)

Accelerators

Estimates the energy used by accelerators (GPUs), following the approach of the Cloud Carbon Footprint project: the power draw is interpolated between the minimum and maximum wattage of the accelerator at an assumed utilisation rate.

Instance types which expose only a fraction of a physical GPU (e.g. g6f.large, which gets an eighth of an NVIDIA L4) are supported: their share of the GPU is a decimal number and the power draw is scaled accordingly.

Class com.digitalpebble.spruce.modules.ccf.aws.Accelerators
Writes operational_energy_kwh

Configuration:

Key Default Description
gpu_utilisation_percent 50 Assumed GPU utilisation rate; decimals are allowed (e.g. 12.5)

Compute — Boavizta

Estimates the final energy used for computation (e.g. EC2, OpenSearch, RDS, Amazon MQ, ElastiCache on AWS; virtual machines on Azure), as well as the related embodied emissions and abiotic resource depletion, using the BoaviztAPI.

In a CUR, the instance type is read from product_instance_type; a FOCUS report does not carry it, so it is parsed from the SkuMeter column instead (e.g. EUW2-BoxUsage:t3.xlarge).

Several managed services report their instance shape behind a service-specific prefix, which is removed before the lookup: db. for RDS, mq. for Amazon MQ, cache. for ElastiCache, and the .search suffix for OpenSearch. The two formats do not always agree on this, so both are normalised: product_instance_type already reads m5.large for an Amazon MQ broker, while the matching SkuMeter still reads mq.m5.large.

Amazon MQ is also the one service here that does not bill per instance. A clustered deployment is charged one cluster-hour whatever its size, so a three-node RabbitMQ cluster running for an hour appears as a single unit of USE1-RabbitMQ-3-InstanceUsage:mq.m5.large. The node count is read back from the usage type and applied to both the energy and the embodied impacts; usage types carrying no such marker are treated as a single instance, and Multi-AZ as two.

Each provider has two variants:

  • BoaviztAPI queries a running instance of the API (docker run -p 5000:5000 ghcr.io/boavizta/boaviztapi:latest). The address can be set with the address configuration key (default http://localhost:5000).
  • BoaviztAPIstatic reads the same information from a static file generated from the API and bundled with SPRUCE. No API instance is needed, which makes it simpler to use — this is the variant enabled in the default configurations.
Classes com.digitalpebble.spruce.modules.boavizta.aws.BoaviztAPI
com.digitalpebble.spruce.modules.boavizta.aws.BoaviztAPIstatic
com.digitalpebble.spruce.modules.boavizta.azure.BoaviztAPI
com.digitalpebble.spruce.modules.boavizta.azure.BoaviztAPIstatic
Writes operational_energy_kwh, embodied_emissions_co2eq_g, embodied_adp_sbeq_g
What is Abiotic Depletion Potential (ADP)?

From the BoaviztAPI documentation:

Abiotic Depletion Potential (ADP) is an environmental impact indicator. This category corresponds to mineral and resources used and is, in this sense, mainly influenced by the rate of resources extracted. The effect of this consumption on their depletion is estimated according to their availability stock at a global scale. This impact category is divided into two components: a material component and a fossil fuels component (we use a version of ADP which includes both). This impact is expressed in grams of antimony equivalent (gSbeq).

Source: sciencedirect

LLM inference — EcoLogits

Estimates the energy consumption and embodied emissions of LLM inference on AWS Bedrock and Azure AI Foundry (initially the Azure OpenAI models), based on static per-model coefficients derived from the EcoLogits project. Like BoaviztAPIstatic, a static data file bundled in the JAR is loaded at initialisation time; the modules then match billing rows to per-model coefficients.

BedrockEcoLogits parses the line_item_usage_type field (format: {REGION}-{ModelKey}-{input|output}-tokens[-batch]) to extract both the model key and the token type, then normalises the token count from pricing_unit (handling real-world values such as 1K tokens or 1M tokens). Only output-token rows are scored — the EcoLogits methodology attributes ~all generation cost to the autoregressive output phase, so input-token rows are skipped.

AzureFoundryTokenEcoLogits does the same for token meters billed under the Azure OpenAI (or newer Foundry Models) category: the model label and token direction are extracted from MeterName (e.g. GPT 5 outpt Glbl 1M Tokens) and the token count is read from Quantity (ConsumedQuantity in FOCUS reports). Both hold the number of tokens consumed: the 1K/1M unit in UnitOfMeasure only describes the pricing block and does not scale the quantity — Microsoft's FOCUS conversion defines ConsumedQuantity = Quantity and ContractedCost = UnitPrice × Quantity / x_PricingBlockSize. Provisioned throughput (PTU), hourly hosting and fine-tuning meters are not token-based and are not covered. The other Foundry model families (Mistral, Cohere, Llama, ...) bill through the same kind of token meters and only need mapping.csv entries verified against real exports — the module initially ships with Azure OpenAI mappings.

Classes com.digitalpebble.spruce.modules.ecologits.BedrockEcoLogits
com.digitalpebble.spruce.modules.ecologits.AzureFoundryTokenEcoLogits
Writes operational_energy_kwh, embodied_emissions_co2eq_g

Batch size assumption

EcoLogits hardcodes a batch size of B=64 concurrent requests. The resulting coefficients are a mid-batch estimate — they underestimate energy for low-traffic scenarios and overestimate it for high-throughput batch inference (e.g. Bedrock Batch mode). Making B dynamic requires provider telemetry not available in billing data.

Stage 3 — Efficiency and intensity factors

The modules in this stage do not estimate energy themselves; they attach the per-region factors that the impact modules in the next stage multiply the energy estimates by.

PWUE

Loads both Power Usage Effectiveness (PUE) and Water Usage Effectiveness (WUE) factors from a CSV resource file bundled per provider: aws-pue-wue.csv carries the 2022–2025 figures published by AWS, and azure-pue-wue.csv is sourced from Microsoft's data centre sustainability pages.

These factors are published per year and move noticeably from one year to the next, so they are keyed by region and year rather than applied as a blanket value. The year is that of the line item's usage date, read from whichever of ChargePeriodStart, line_item_usage_start_date, Date or BILLING_PERIOD the report carries.

The lookup logic follows this priority:

  1. Exact region match (e.g. us-east-1)
  2. Regex pattern match (e.g. eu-.+), i.e. the geography-level average
  3. The provider-wide GLOBAL average, where the CSV has one
  4. Default configured value (fallback to 1.15 for PUE, null for WUE)

Within a tier the entry for the usage year is used; when that year is not covered — a region AWS started reporting on recently, a WUE only published from 2024 onwards, or a row with no usable date — the closest year available for that region is used instead.

Azure figures are not dated

Microsoft does not break its PUE and WUE down by year, so the rows in azure-pue-wue.csv leave the year empty and apply to every year.

Class com.digitalpebble.spruce.modules.PWUE
Reads region, the usage date of the line item
Writes power_usage_effectiveness, water_usage_effectiveness

Configuration:

Key Default Description
default 1.15 PUE used when a region matches no entry at any tier, global average included

AverageCarbonIntensity

Adds average carbon intensity factors derived from Ember's electricity data, distributed under the Creative Commons Attribution Licence (CC-BY-4.0). Values are keyed directly by cloud provider and region (e.g. aws:us-east-1). For regions in countries with sub-national data (currently the US and India), the carbon intensity is taken from the Ember value for the state hosting the data centre; otherwise the country-level Ember value is used.

The data is loaded from ember/ember_co2_intensity.csv, which is generated from cloud_regions.json — see the scripts under scripts/ and the dedicated README for how to refresh it.

Class com.digitalpebble.spruce.modules.ember.AverageCarbonIntensity
Reads region
Writes carbon_intensity

Stage 4 — Impacts

The modules in this stage combine the energy estimates from stage 2 with the factors from stage 3 to produce the final impact columns.

OperationalEmissions

Computes operational emissions from the energy usage, carbon intensity and PUE estimated by the preceding modules:

operational_emissions_co2eq_g =
    operational_energy_kwh × carbon_intensity × power_usage_effectiveness
    × powerSupplyEfficiency × powerTransmissionLosses

It accounts for two additional overheads:

  • Power Supply Efficiency: the power lost between the data centre mains electricity and the server (default 1.04).
  • Power Transmission Losses: the power lost between the power station and the data centre mains electricity (default 1.08).
Class com.digitalpebble.spruce.modules.OperationalEmissions
Reads operational_energy_kwh, carbon_intensity, power_usage_effectiveness
Writes operational_emissions_co2eq_g

Configuration:

Key Default Description
powerSupplyEfficiency 1.04 Losses between mains and server
powerTransmissionLosses 1.08 Losses between power station and data centre

Water

Estimates the water consumption associated with cloud usage, producing three columns:

  • water_cooling_l — the volume of water (in litres) used for data centre cooling. Computed as operational_energy_kwh × power_usage_effectiveness × WUE, where WUE is the ratio of litres of water consumed for cooling per kWh of IT energy, loaded per region by the PWUE module.

  • water_electricity_production_l — the volume of water (in litres) consumed during electricity generation to power the data centre. Computed as operational_energy_kwh × power_usage_effectiveness × WCF, where WCF (Water Consumption Factor) represents the litres of water consumed per kWh of electricity generated. The WCF values per electricity grid zone are sourced from the WRI methodology for calculating water use embedded in purchased electricity.

  • water_consumption_stress_area_l — the total water consumption (water_cooling_l + water_electricity_production_l) attributed to regions under high or extremely high water stress. This field is only populated when the electricity grid zone for the region has a baseline water stress category of 3 (High) or 4 (Extremely High) in the WRI Aqueduct 4.0 dataset; it is absent otherwise. The Aqueduct data is licensed through Creative Commons and has been extracted and mapped to cloud provider region codes.

Class com.digitalpebble.spruce.modules.Water
Reads operational_energy_kwh, power_usage_effectiveness, water_usage_effectiveness, region
Writes water_cooling_l, water_electricity_production_l, water_consumption_stress_area_l

Stage 5 — Output normalisation

FOCUSColumns

Bridges provider-native billing columns to provider-neutral FOCUS (FinOps Open Cost & Usage Specification) columns, so the reporting scripts and dashboard can consume enriched data with the same column names whatever the input format. Only the columns the reporting scripts and dashboard rely on are covered — this is not a full native-to-FOCUS conversion, and values are copied as-is (e.g. ChargeCategory carries the raw line_item_line_item_type / ChargeType vocabulary). Runs last in the pipeline, after region has been set by RegionExtraction.

These modules are only needed for the native formats: a FOCUS report already carries these columns on input, so the FOCUS pipelines do not include them.

For AWS CURs:

FOCUS column AWS source
BilledCost line_item_unblended_cost
ListCost pricing_public_on_demand_cost
RegionId region (normalised by RegionExtraction)
ServiceName product_servicecode
ChargeCategory line_item_line_item_type
SubAccountId line_item_usage_account_id
ChargePeriodStart line_item_usage_start_date
ChargePeriodEnd line_item_usage_end_date
Tags resource_tags, copied as a map of tag key/values as in AWS FOCUS exports
SkuMeter line_item_usage_type (e.g. EUW2-BoxUsage:t3.xlarge, from which reporting derives instance types)
x_ServiceCode line_item_product_code
x_Operation line_item_operation

For Azure cost details exports (columns that already carry a FOCUS-compatible name in the input, such as BillingCurrency and Tags, pass through unchanged — except that Tags, which EA exports truncate to a JSON fragment without the enclosing braces, is wrapped back into a valid JSON object when the report is loaded):

FOCUS column Azure source
BilledCost CostInBillingCurrency
RegionId region (normalised by RegionExtraction)
ServiceName MeterCategory
ChargeCategory ChargeType
SubAccountId SubscriptionId
ChargePeriodStart Date
ChargePeriodEnd Date + 1 day
Classes com.digitalpebble.spruce.modules.aws.FOCUSColumns
com.digitalpebble.spruce.modules.azure.FOCUSColumns
Reads region and the provider-native columns above
Writes BilledCost, ListCost, RegionId, ServiceName, ChargeCategory, SubAccountId, ChargePeriodStart, ChargePeriodEnd, Tags, SkuMeter, x_ServiceCode, x_Operation

Supporting data

Cloud region metadata

SPRUCE ships with cloud_regions.json, a single JSON file listing the AWS, GCP, and Azure cloud regions together with their location (country, metro area, latitude/longitude), service status, and number of availability zones. It is the canonical source for the region-to-location mapping used by the modules and resource files (e.g. AverageCarbonIntensity).

The file is produced in two steps by scripts under scripts/, see scripts/README.md for the full usage details.