The Well-Architected review left five high risks in the cost pillar, and the first of them is the one that blocks all the others: MercadoFresco's bill cannot be read. A consolidated amount of 2,237.60 USD a month arrives and, underneath it, a list of services. Nothing tells you which part is production and which part is the test environment Luis left switched on, nor how much the catalogue costs compared with orders, nor whether Madrid works out dearer per customer than Seville.

This lesson solves that problem before touching any analysis tool. You will see how to design a tagging strategy that genuinely works, how to activate cost allocation tags — and the detail that catches so many people out: they are not retroactive — how to enforce tagging instead of asking for it by email, how to audit untagged resources, when the account separates better than the tag, how to group business concepts with cost categories, and how to get to the definitive data with the Cost and Usage Report queried from Athena. The end point is the metric that changes the conversation with the manager: the cost per order.

Cost warning. Tagging is free: tags cost nothing, nor does activating them as cost allocation dimensions, nor do cost categories. What does cost money is the Cost and Usage Report: the S3 storage (pennies at this volume) and, above all, the Athena queries at 5 USD per TB scanned, which with partitions and Parquet format stay under 1 USD a month and without them can shoot up. Fictitious data, accounts and identifiers.

Contents

  1. The real problem: a bill nobody can read
  2. What questions MercadoFresco needs to be able to answer
  3. Designing the tagging strategy
  4. Naming conventions and closed value lists
  5. How many tags are too many, and which resources cannot be tagged
  6. Activating the cost allocation tags
  7. The critical detail: tags are not retroactive
  8. Enforcing tagging instead of asking for it
  9. Organizations tag policies
  10. IAM conditions: aws:RequestTag and aws:TagKeys
  11. Config rules with remediation
  12. CDK aspects, ECS and the pipeline
  13. Which mechanism covers what: the decision table
  14. Auditing what is not tagged and setting a realistic target
  15. The account as a cost dimension
  16. Cost categories: from resource to business concept
  17. The Cost and Usage Report (CUR)
  18. Querying the CUR with Athena
  19. Shared costs and unassignable costs
  20. Unit cost: MercadoFresco's cost per order
  21. Showing and charging back costs: showback and chargeback
  22. Common mistakes and tips
  23. Exercises
  24. Conclusion

The real problem: a bill nobody can read

Marta opens the billing console for the first time with the intention of understanding it. What she sees is this:

August 2026 bill — Organisation o-a1b2c3d4e5
Total before tax ........................................ 2,237.60 USD
  Amazon Relational Database Service .................... 342.60
  Amazon Elastic Container Service ...................... 262.40
  Amazon Virtual Private Cloud .......................... 362.40
  Amazon CloudWatch ..................................... 214.90
  Amazon ElastiCache .................................... 135.40
  Amazon Simple Storage Service ......................... 128.90
  ...

It is information, but it is no use for deciding anything, because none of the manager's questions is answered by that list. "How much does the development environment cost?": the bill does not know, it knows how much RDS costs in total. "Does Sara's analytics pay for itself?": there is no line called "analytics". "These 362 USD of VPC, what are they?": NAT Gateway and endpoints spread across every component. "How much does it cost to serve Seville?": that dimension simply does not exist.

The diagnosis is straightforward: the bill is organised according to AWS's structure, not according to the business's. Tagging is the mechanism for translating from one to the other.

What questions MercadoFresco needs to be able to answer

Before deciding which tags to apply, you have to write down the questions. That is the right order and almost nobody follows it: most tagging strategies fail because they are designed "just in case" and end up with twenty tags that answer nothing. MercadoFresco's questions, agreed between Marta, Sara and the manager:

Question Dimension that answers it Who asks
How much does production cost against pre-production and development? Entorno and account Management
How much does each part of the system cost? Componente Marta
Who is responsible for each item of spend? Propietario Marta
How much do we charge to operations and how much to marketing? CentroCoste Administration
How much does it cost to serve each city? Derived: not tagged, calculated Management
How much does each order cost? Derived: total cost / orders Management
Which spend is not assigned to anybody? Absence of tags Marta

The last two rows contain the most important lesson in this section: not everything is solved with a tag. The city cannot be tagged because the infrastructure is shared: the same ALB, the same cluster and the same database serve all four cities. Cost per city is an allocation, not a measurement, and it is calculated from the number of orders. Forcing it with a Ciudad tag would produce a false figure with the appearance of a true one, which is worse than not having the figure at all.

Designing the tagging strategy

A tag is a key-value pair attached to a resource. It sounds trivial and it is not: it is the only cross-cutting mechanism that runs through every AWS service, and day-one decisions are dragged along for years. MercadoFresco's five mandatory tags, which you have been using since module 1, now with their full rationale:

Key Allowed values What it is for Mandatory on
Proyecto mercadofresco Separating from the rest if several projects ever share an account Everything
Entorno produccion, preproduccion, desarrollo Allocation by environment; the basis of the budgets Everything
Componente tienda, catalogo, pedidos, reparto, analitica The dimension that gets queried most Everything
Propietario marta, luis, sara Knowing who to ask and who to alert Everything
CentroCoste operaciones, marketing Accounting allocation Everything

On top of these five there are four optional ones, permitted but not required, that solve specific problems: Temporal=si and FechaBaja=yyyy-mm-dd mark short-lived resources that can be deleted without asking; Cumplimiento=rgpd flags those containing personal data; and Automatizacion=apagado-nocturno selects what the scheduler switches off.

One golden rule worth being inflexible about: a tag with no consumer does not get created. If nobody is going to filter, group or automate by it, it should not exist, because every tag adds friction to every resource creation and noise to every report.

Naming conventions and closed value lists

AWS tags are case-sensitive. Entorno=Produccion, entorno=produccion and Entorno=produccion are three different tags producing three different lines in the cost report. It is the number one cause of useless allocation reports.

MercadoFresco's conventions, written down in mercadofresco-infra/docs/etiquetado.md:

Rule Correct Incorrect
Keys in PascalCase, no spaces CentroCoste centro coste, centro_coste
Values in lower case, no spaces or accents preproduccion Preproducción, Pre Produccion
No personal data or secrets marta marta.gomez@...
Hyphens, not underscores apagado-nocturno apagado_nocturno
Closed values, from a published list catalogo catálogo-v2, cat
aws: prefix forbidden AWS reserves it and it cannot be used

The point that generates most argument and matters most is the one about closed values. A tag with free values is a text field, and a text field always ends up with pedidos, Pedidos, pedido, pedidos-nuevo and pedidos2, that is, with five components where there is one. The list of allowed values is published, versioned and enforced technically in the sections that follow.

Three technical limits worth remembering: 50 tags per resource, keys of up to 128 characters and values of up to 256, and — the one that leaves most cost unassigned — tags are not inherited: a tagged EC2 instance does not tag its EBS volumes unless you ask for it explicitly.

How many tags are too many, and which resources cannot be tagged

How many. Industry experience is consistent: between 4 and 8 mandatory tags is the range that works. Below 4 you cannot allocate cost with any judgement; above 8, people copy and paste values without thinking and data quality collapses. MercadoFresco has five, which is a good place to be. The sign that one is surplus is easy to spot: when somebody creates a resource and has to ask what value to put, that tag is either surplus or badly defined.

What cannot be tagged. This is the part that breaks expectations and has to be understood before promising 100 % coverage:

Item Taggable? Consequence for cost
EC2, EBS, Lambda, DynamoDB, Aurora, SQS, SNS, EventBridge Yes
S3 bucket Yes, at bucket level, not per object or prefix A bucket shared between components cannot be split with tags
Cross-AZ data transfer No Appears untagged; it has to be allocated
KMS requests The key yes, the usage not always Small amount; assumed to be shared
AWS Support and Marketplace No An account-level charge; allocated through a cost category
Credits, discounts and taxes No Applied to the total, not to the tag

From that comes a principle that saves a lot of frustration: the goal is not to tag 100 % of the cost, but to tag 100 % of what is taggable and allocate the rest with a written rule.

Activating the cost allocation tags

Here is the step MercadoFresco had not taken, the one that turns a tag into a dimension of the bill. Putting a tag on a resource does not make it appear in cost reports. It has to be activated explicitly as a cost allocation tag, and that can only be done from the management account (999988887777), in Billing → Cost allocation tags. There are two families: the AWS-generated ones, with the aws: prefix, and the user-defined ones, which are yours. The former are free and surprisingly useful: aws:createdBy records which identity created the resource and answers "who switched this on?" even if nobody set Propietario; aws:cloudformation:stack-name gives cost per stack, which with the IaC from module 9 is an almost free and very precise allocation; and aws:ecs:serviceName separates the Fargate cost between svc-mercadofresco-tienda-fg and svc-mercadofresco-trabajadores without tagging anything.

Activating them from the CLI, in the management account:

# See which tags the system knows about and what their status is
aws ce list-cost-allocation-tags --status Inactive --output table

# Activate the five mandatory ones and three AWS-generated ones
aws ce update-cost-allocation-tags-status --cost-allocation-tags-status \
  'TagKey=Proyecto,Status=Active' \
  'TagKey=Entorno,Status=Active' \
  'TagKey=Componente,Status=Active' \
  'TagKey=Propietario,Status=Active' \
  'TagKey=CentroCoste,Status=Active' \
  'TagKey=aws:createdBy,Status=Active' \
  'TagKey=aws:cloudformation:stack-name,Status=Active' \
  'TagKey=aws:ecs:serviceName,Status=Active'

# Check the result
aws ce list-cost-allocation-tags --status Active --output table

Four details about this block. The command lives in the ce namespace even though the feature sits in the billing console, and it only works in the management account. A tag only appears in the list if AWS has seen it at least once on some resource: first you tag something, then you activate it. After activating it, it takes up to 24 hours to show up in the reports. And there is a limit of 500 active tags per organisation, irrelevant with five and very relevant in organisations that let them grow unchecked.

The critical detail: tags are not retroactive

This is the sentence to underline in the whole lesson:

A cost allocation tag only applies to cost generated from the moment it is activated. Never backwards.

Consequences with MercadoFresco's numbers: Marta activates the tags on 12 August, and in Cost Explorer all the cost from the 1st to the 11th appears as (untagged) across the five dimensions even though the resources had been tagged since March. The first month with clean data is September; August is mixed and no use for comparison. And if a sixth tag is added three months from now, year-on-year comparisons for that dimension will not exist until the following year.

From that come three pieces of advice that save months: activate the tags the day you define the convention, even if coverage is low, because the clock starts when you activate and not when you finish tagging; activate the AWS-generated ones from the start too, since they are free and depend on nobody; and note down the activation date in the tagging document, so that six months from now the answer to "why does July have no allocation?" is written somewhere.

There is a partial exception: the Cost and Usage Report can be regenerated backwards up to a point when new tags are activated, and some previous months do get filled in. It is neither reliable nor universal, so the mental rule remains: they are not retroactive.

Enforcing tagging instead of asking for it

MercadoFresco has had a written tagging convention for months and real coverage of 68 %. It is not a lack of willingness: it is that asking people by email to tag things never works, in any company. What works is making it impossible to create a badly tagged resource, or making sure that if one is created, it fixes itself.

graph TD
  DEV["Luis creates a resource"] --> Q1{"Through CDK<br/>or pipeline?"}
  Q1 -->|Yes| CDK["CDK aspect<br/>tags the whole stack"]
  Q1 -->|No, by hand| IAM["IAM policy with<br/>aws:RequestTag"]
  IAM -->|Tag missing| DENY["AccessDenied"]
  IAM -->|Tags OK| CREA["Resource created"]
  CDK --> CREA
  CREA --> TPOL["Organizations<br/>tag policy"]
  CREA --> CFG["Config required-tags<br/>rule + remediation"]

The five mechanisms are complementary, not alternatives. One by one.

Organizations tag policies

A tag policy is defined in the management account and attached to the root or to an OU. It defines which keys exist, with what exact spelling and which values they accept, and it can also block operations that break the policy for specific resource types.

{
  "tags": {
    "Entorno": {
      "tag_key": { "@@assign": "Entorno" },
      "tag_value": { "@@assign": ["produccion", "preproduccion", "desarrollo"] },
      "enforced_for": {
        "@@assign": ["ec2:instance", "ec2:volume", "rds:db", "rds:cluster",
                     "s3:bucket", "lambda:function", "dynamodb:table",
                     "elasticache:replicationgroup"]
      }
    },
    "Componente": {
      "tag_key": { "@@assign": "Componente" },
      "tag_value": {
        "@@assign": ["tienda", "catalogo", "pedidos", "reparto", "analitica"]
      }
    },
    "CentroCoste": {
      "tag_key": { "@@assign": "CentroCoste" },
      "tag_value": { "@@assign": ["operaciones", "marketing"] }
    }
  }
}

The Proyecto and Propietario keys are declared the same way, with their closed list of values.

What each part does. tag_key with @@assign fixes the canonical spelling: from then on, a resource tagged entorno=produccion in lower case is flagged as non-compliant. tag_value with @@assign defines the closed list. enforced_for is the part with teeth: for the listed types, creation or tagging that breaks the policy is rejected; without it, the policy only reports. And the @@assign operator overwrites what is inherited, with @@append and @@remove available for combining policies from different levels of the hierarchy.

How it is applied:

# Create the policy in the management account 999988887777
aws organizations create-policy --name "etiquetas-mercadofresco" \
  --type TAG_POLICY --content file://etiquetas-mercadofresco.json

# Attach it to the Cargas OU (produccion, preproduccion and desarrollo)
aws organizations attach-policy \
  --policy-id p-mfetiq0001 --target-id ou-a1b2-cargas001

# Check compliance across the whole organisation
aws resourcegroupstaggingapi get-compliance-summary \
  --target-id-filters 111122223333 222233334444 333344445555 \
  --group-by RESOURCE_TYPE

A warning about enforced_for: roll it out in reporting mode first, because switching it on all at once in production can break the pipeline, the auto scaling or any automation that creates resources without the five tags. MercadoFresco applies it in three phases: reporting mode for two weeks, enforced_for in development for two weeks more, and only then in production.

IAM conditions: aws:RequestTag and aws:TagKeys

The tag policy governs values; IAM governs who can do what, and it lets you demand tags at creation time with two condition keys: aws:RequestTag/<Key>, the value being set in the request, and aws:TagKeys, the list of keys present in it. This policy demands the five tags when creating instances and volumes, and additionally prevents Entorno from being changed afterwards:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ExigirLasCincoEtiquetasAlCrear",
      "Effect": "Allow",
      "Action": ["ec2:RunInstances", "ec2:CreateVolume"],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestTag/Proyecto": "mercadofresco",
          "aws:RequestTag/Entorno": ["produccion", "preproduccion", "desarrollo"],
          "aws:RequestTag/Componente": [
            "tienda", "catalogo", "pedidos", "reparto", "analitica"
          ]
        },
        "ForAllValues:StringEquals": {
          "aws:TagKeys": [
            "Proyecto", "Entorno", "Componente", "Propietario", "CentroCoste",
            "Temporal", "FechaBaja", "Cumplimiento", "Automatizacion"
          ]
        },
        "ForAnyValue:StringEquals": {
          "aws:TagKeys": ["Propietario", "CentroCoste"]
        }
      }
    },
    {
      "Sid": "ProhibirCambiarElEntornoDeUnRecursoExistente",
      "Effect": "Deny",
      "Action": ["ec2:CreateTags", "ec2:DeleteTags"],
      "Resource": "*",
      "Condition": {
        "ForAnyValue:StringEquals": { "aws:TagKeys": ["Entorno", "CentroCoste"] }
      }
    }
  ]
}

IAM's set operators are the part people get wrong most often. StringEquals on aws:RequestTag/X demands that tag X be present and hold one of those values: if it is missing, the action is denied, and that is how you make a tag mandatory. ForAllValues:StringEquals on aws:TagKeys means "every key you send must be in this list", that is, an allow list that stops people inventing tags. ForAnyValue:StringEquals means "at least one of these keys must be present". And the second Statement is an explicit Deny on changing Entorno and CentroCoste afterwards: without it, anybody could create a properly tagged resource and five minutes later move its cost to another cost centre.

A practical warning: demanding tags through IAM on ec2:RunInstances is trickier than it looks, because a single call creates the instance, its volumes and its network interfaces. That is why this mechanism is reserved for manual creation and the bulk of tagging is solved in the CDK.

Config rules with remediation

IAM stops things being created badly. Config detects what is already wrong, including anything created before the rules were put in place. The managed required-tags rule accepts up to six keys with their values.

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-etiquetas-obligatorias",
  "Description": "The five mandatory tags of the project",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "REQUIRED_TAGS"
  },
  "InputParameters": "{\"tag1Key\":\"Proyecto\",\"tag1Value\":\"mercadofresco\",\"tag2Key\":\"Entorno\",\"tag2Value\":\"produccion,preproduccion,desarrollo\",\"tag3Key\":\"Componente\",\"tag3Value\":\"tienda,catalogo,pedidos,reparto,analitica\",\"tag4Key\":\"Propietario\",\"tag5Key\":\"CentroCoste\",\"tag5Value\":\"operaciones,marketing\"}",
  "Scope": {
    "ComplianceResourceTypes": [
      "AWS::EC2::Instance", "AWS::EC2::Volume", "AWS::RDS::DBInstance",
      "AWS::S3::Bucket", "AWS::Lambda::Function", "AWS::DynamoDB::Table",
      "AWS::ElasticLoadBalancingV2::LoadBalancer"
    ]
  }
}'

On remediation, you need judgement rather than automating everything the same way:

Tag Automatically remediable? How
Proyecto Yes It is always mercadofresco; applied without asking
Entorno Yes Deduced from the account: 111122223333produccion
Propietario Semi Deduced from CloudTrail's aws:createdBy and proposed
CentroCoste Partly operaciones by default; reviewed by hand
Componente No Nobody but the creator knows whether it is catalogue or orders

MercadoFresco automates the first two with a Systems Manager document and raises an alert on the other three, with a daily notification to alertas-mercadofresco. The rule that avoids disaster: automatic remediation never invents a business value. Tagging everything as Componente=tienda by default would produce a beautiful and completely false cost report.

CDK aspects, ECS and the pipeline

The mechanism that in practice solves 90 % of the problem is none of the above: it is tagging at source, in the infrastructure as code from module 9. In CDK, aspects (Tags) apply tags to a stack's entire construct tree, including the resources a level-3 construct creates without you writing them:

from aws_cdk import App, Stack, Tags

app = App()
stack = MercadoFrescoShopStack(app, "MercadoFrescoTiendaProd", environment="produccion")

# A single block tags ALL the resources in the stack, including those that
# ApplicationLoadBalancedFargateService creates underneath without you writing them.
for key, value in {
    "Proyecto": "mercadofresco", "Entorno": "produccion",
    "Componente": "tienda", "Propietario": "luis",
    "CentroCoste": "operaciones",
}.items():
    Tags.of(stack).add(key, value)

# One-off exceptions without giving up the global aspect
Tags.of(stack).add("Automatizacion", "apagado-nocturno",
                   exclude_resource_types=["AWS::ECS::Service"])

app.synth()

Tags.of(stack).add(...) walks the entire tree of the stack, which is the difference from tagging resource by resource, and exclude_resource_types allows exceptions. Since the stack is created by CloudFormation, you also get aws:cloudformation:stack-name automatically, which gives allocation per stack for free.

In ECS, two specific options stop the largest compute cost from going unassigned:

aws ecs create-service \
  --cluster ecs-mercadofresco --service-name svc-mercadofresco-tienda-fg \
  --task-definition mercadofresco-tienda \
  --propagate-tags TASK_DEFINITION \
  --enable-ecs-managed-tags \
  --tags key=Proyecto,value=mercadofresco key=Entorno,value=produccion \
         key=Componente,value=tienda key=Propietario,value=luis \
         key=CentroCoste,value=operaciones

--propagate-tags TASK_DEFINITION makes every task inherit the tags: without this option the Fargate cost does not carry your tags, which is exactly what was happening to MercadoFresco with 262.40 USD a month left unallocated. And --enable-ecs-managed-tags adds aws:ecs:clusterName and aws:ecs:serviceName, useful for separating the shop from the workers.

And in the pipeline (08-04), the deployment stage exports the tags as variables and cdk deploy receives them. The check is also made in build-mercadofresco-puerta-calidad: if cdk synth generates a template with any taggable resource missing the five keys, the pipeline fails. That is the definitive barrier.

Which mechanism covers what: the decision table

Mechanism When What it guarantees What it does NOT cover Cost
CDK aspects (09-02) At deployment Everything created by IaC carries the 5 tags Anything created by hand or by another route 0
Pipeline quality gate (08-02) Before deploying No badly tagged template reaches AWS Anything that does not go through the pipeline 0
IAM conditions (04-01) At manual creation Nothing can be created without correct tags Existing resources; unsupported services 0
Tag policy (09-04) At creation and at tagging Canonical spelling and values across the organisation Total absence of the tag if there is no enforced_for 0
Config required-tags rule (05-04) Continuous, after the fact Detects everything non-compliant, including old resources Does not prevent creation; only remediates what can be deduced ~0.001 USD per evaluation
ECS --propagate-tags At service creation That the tasks' cost carries tags Other services with the same problem 0

MercadoFresco's complete strategy is the sum of them: CDK and pipeline for the normal path, IAM for the manual path, tag policy for the spelling, and Config as a safety net that audits everything. None of the five is surplus and none is enough on its own.

Auditing what is not tagged and setting a realistic target

With the mechanisms in place, what remains is the archaeology: the resources created earlier. 1. Tag Editor (inside Resource Groups) searches for resources by region, type and tag — including searching by the absence of a tag — and lets you bulk tag hundreds of resources at once: it is the shortcut for fixing the history. 2. The resource tagging API, for automating it:

# Resources WITHOUT the Componente tag in the production account
aws resourcegroupstaggingapi get-resources --region eu-west-1 \
  --query 'ResourceTagMappingList[?!(Tags[?Key==`Componente`])].ResourceARN' \
  --output text | tr '\t' '\n' | sort

# Bulk tag a set of known ARNs
aws resourcegroupstaggingapi tag-resources \
  --resource-arn-list \
    arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-almacen \
    arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado \
  --tags Proyecto=mercadofresco,Entorno=produccion,Componente=pedidos,Propietario=luis,CentroCoste=operaciones

3. A coverage report that runs every Monday and publishes the result as a metric:

import boto3
from collections import defaultdict

MANDATORY = {"Proyecto", "Entorno", "Componente", "Propietario", "CentroCoste"}

def coverage(session, environment):
    api = session.client("resourcegroupstaggingapi", region_name="eu-west-1")
    cw = session.client("cloudwatch", region_name="eu-west-1")
    total, complete, missing, orphans = 0, 0, defaultdict(int), []

    # The paginator is mandatory: without it only the first 100 resources are seen
    for page in api.get_paginator("get_resources").paginate(ResourcesPerPage=100):
        for resource in page["ResourceTagMappingList"]:
            total += 1
            absent = MANDATORY - {t["Key"] for t in resource["Tags"]}
            if not absent:
                complete += 1
                continue
            for key in absent:
                missing[key] += 1
            if len(absent) == 5:                # no mandatory tag at all
                orphans.append(resource["ResourceARN"])

    pct = round(100 * complete / total, 1) if total else 100.0
    print(f"[{environment}] {complete}/{total} complete = {pct} % "
          f"| orphans: {len(orphans)}")
    for key, n in sorted(missing.items(), key=lambda x: -x[1]):
        print(f"    missing {key}: {n} resources")

    cw.put_metric_data(Namespace="MercadoFresco/Tienda", MetricData=[{
        "MetricName": "CoberturaEtiquetado",
        "Dimensions": [{"Name": "Entorno", "Value": environment}],
        "Value": pct, "Unit": "Percent"}])
    return orphans

# In practice a role is assumed in each account with sts:AssumeRole
coverage(boto3.Session(), "produccion")

Two decisions in the script deserve a comment. It separates incomplete resources (missing some tag) from orphans (having none at all): orphans are almost always abandoned leftovers and they are the best place to start deleting. And it publishes CoberturaEtiquetado as a custom metric in MercadoFresco/Tienda with an Entorno dimension, so that coverage lives on the same dashboard as the business and can carry an alarm for when it drops.

A target of 100 % is an elegant way of guaranteeing that nobody takes the indicator seriously, because there is cost that cannot be tagged by definition. MercadoFresco's targets, written down and agreed:

Indicator Initial At 3 months Steady state
Resources with the 5 tags 68 % 90 % 95 %
Cost assigned to a Componente 61 % 92 % 97 % of what is taggable
Resources with no tag at all 47 0 0
Cost with Entorno assigned 74 % 98 % 99 %

It is worth measuring the two indicators separately, because they are very different: fifty untagged Lambda functions push coverage by resources down a long way and coverage by cost hardly at all, while an untagged Aurora cluster does the opposite. The one that matters for the bill is coverage by cost; the one that matters for operational hygiene is coverage by resources.

The account as a cost dimension

After all that tagging work it is worth admitting something: for the Entorno dimension, MercadoFresco did not need tags. The account structure from module 9 already separates the environments perfectly and unfalsifiably:

Account Environment Monthly cost %
111122223333 production 1,482.10 USD 66.2 %
222233334444 pre-production 402.80 USD 18.0 %
333344445555 development 246.50 USD 11.0 %
555566667777 tooling (pipeline, ECR) 62.40 USD 2.8 %
444455556666 security (trail, Config, GuardDuty) 38.90 USD 1.7 %
999988887777 management 4.90 USD 0.2 %
Total 2,237.60 USD 100 %

This split has three properties no tag can match: it is complete, because all cost belongs to an account, including what cannot be tagged — cross-AZ transfer, support, Marketplace; it cannot be falsified or forgotten, because nobody creates a resource "without an account"; and it is retroactive, because it has existed since day one without anything being activated. Hence the practical rule, which is one of the conclusions of module 9 seen from the money side:

Whatever you really want to separate, separate it by account. Whatever you want to analyse within one scope, separate it by tag.

You separate by account when, as well as the cost, you want to isolate the blast radius, the access or the quotas: environments, business units, large customers. You separate by tag when things share a scope and all you need to do is attribute: components, owners, cost centres.

Cost categories: from resource to business concept

One jump remains. The manager does not ask about accounts or tags: he asks about business concepts. "Platform", "product" and "analytics" are neither an account nor a tag: they are combinations.

Cost categories are rules defined in the management account that create a new dimension out of accounts, tags, services or charge types. They behave like just another tag in Cost Explorer, Budgets and the CUR.

aws ce create-cost-category-definition \
  --name "AreaNegocio" \
  --rule-version CostCategoryExpression.v1 \
  --default-value "plataforma" \
  --rules '[
    {
      "Value": "analitica",
      "Rule": {
        "Or": [
          {"Tags": {"Key": "Componente", "Values": ["analitica"], "MatchOptions": ["EQUALS"]}},
          {"Dimensions": {"Key": "SERVICE", "Values": ["Amazon Redshift", "Amazon Athena"], "MatchOptions": ["EQUALS"]}}
        ]
      },
      "Type": "REGULAR"
    },
    {
      "Value": "producto",
      "Rule": {
        "And": [
          {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": ["111122223333"], "MatchOptions": ["EQUALS"]}},
          {"Tags": {"Key": "Componente", "Values": ["tienda", "catalogo", "pedidos", "reparto"], "MatchOptions": ["EQUALS"]}}
        ]
      },
      "Type": "REGULAR"
    }
  ]'

A third rule, entornos-no-productivos, groups accounts 222233334444 and 333344445555 by LINKED_ACCOUNT.

Four details make this work. Rule order matters: they are evaluated top to bottom and the first match wins, which is why analitica comes before producto. --default-value "plataforma" catches everything that does not fit — NAT, ALB, endpoints, observability, governance — and it is the key to leaving no cost unclassified, which is precisely the problem with tags. You can combine And, Or and Not, mixing accounts with tags. And cost categories do apply with some retroactive effect when created, unlike tags.

The result for MercadoFresco, with the categories mutually exclusive:

Business area Monthly cost % Interpretation
producto 1,129.80 USD 50.5 % What the customer uses directly
entornos-no-productivos 585.80 USD 26.2 % Pre-production + development
plataforma 306.10 USD 13.7 % Network, observability, governance, CI/CD
analitica 215.90 USD 9.6 % Redshift, Athena, reports
Total 2,237.60 USD 100 %

This table opens the conversation Marta had been unable to have for months: more than a quarter of the bill is environments with not a single customer in them. That is not necessarily bad — they are needed — but it is a number that deserves a decision, not an accident.

The Cost and Usage Report (CUR)

Cost Explorer, which we will look at in 11-03, is an interactive analysis tool with limits: when the question is too specific — "cost per component and per hour cross-referenced with the orders from each city" — you need the raw data. The AWS Cost and Usage Report (CUR) is the most detailed record AWS publishes: one row per resource, per usage type and per hour, with more than 150 columns. It contains:

Column group Content Example
lineItem/* Account, service, usage type, quantity, unblended cost line_item_unblended_cost
product/* Product attributes: region, instance type, family product_region
pricing/* Pricing model applied pricing_term: OnDemand, Reserved, SavingsPlan
reservation/*, savingsPlan/* Coverage and amortised cost of the commitments savings_plan_effective_cost
resourceTags/* Your tags, one column per activated tag resource_tags_user_componente
costCategory/* Your cost categories cost_category_area_negocio

MercadoFresco's configuration, created from the management account:

aws cur put-report-definition --report-definition '{
  "ReportName": "cur-mercadofresco-horario",
  "TimeUnit": "HOURLY",
  "Format": "Parquet",
  "Compression": "Parquet",
  "AdditionalSchemaElements": ["RESOURCES", "SPLIT_COST_ALLOCATION_DATA"],
  "S3Bucket": "mercadofresco-informes-analitica",
  "S3Prefix": "cur",
  "S3Region": "eu-west-1",
  "AdditionalArtifacts": ["ATHENA"],
  "RefreshClosedReports": true,
  "ReportVersioning": "OVERWRITE_REPORT"
}'

Each option and why:

  • TimeUnit: HOURLY: without hourly granularity you cannot see the Friday peak or work out the stable baseline for the Savings Plans in 11-05.
  • Format: Parquet: columnar and compressed. Athena charges by bytes scanned, and Parquet cuts the scan by between 10 and 30 times compared with CSV: the difference between paying 0.60 USD a month and paying 18.
  • AdditionalSchemaElements: RESOURCES: adds line_item_resource_id. Without it the report does not say which resource each line belongs to, which is half the value. SPLIT_COST_ALLOCATION_DATA additionally splits the cost of an ECS task across its containers.
  • AdditionalArtifacts: ATHENA: generates the manifest and the CREATE TABLE without writing the schema by hand. And RefreshClosedReports reprocesses closed months when late adjustments or credits arrive.
  • Destination mercadofresco-informes-analitica, Sara's own bucket, with a policy allowing billingreports.amazonaws.com to write to it.

A warning about volume: this CUR generates around 1.8 GB a month in Parquet. With a lifecycle rule to Glacier Instant Retrieval after 90 days it costs pennies; without one, in three years it is tens of GB nobody queries.

Querying the CUR with Athena

With the table created by the Athena artefact, the question that opened the lesson can finally be answered. This query allocates cost by Componente and works out the percentage of the total:

-- Cost for the month by Componente, with the percentage of the total,
-- separating what is shared and what is unassigned.
WITH line_items AS (
  SELECT
    CASE
      WHEN resource_tags_user_componente IS NULL
        OR resource_tags_user_componente = '' THEN 'untagged'
      ELSE resource_tags_user_componente
    END                                   AS component,
    line_item_usage_account_id            AS account,
    line_item_product_code                AS service,
    line_item_unblended_cost              AS cost
  FROM cur_mercadofresco_horario
  WHERE year  = '2026'
    AND month = '8'
    AND line_item_line_item_type IN ('Usage', 'DiscountedUsage', 'SavingsPlanCoveredUsage')
),
totals AS (
  SELECT SUM(cost) AS total FROM line_items
)
SELECT
  l.component,
  ROUND(SUM(l.cost), 2)                             AS cost_usd,
  ROUND(100 * SUM(l.cost) / MAX(t.total), 1)        AS percentage,
  COUNT(DISTINCT l.service)                         AS services_involved
FROM line_items l
CROSS JOIN totals t
GROUP BY l.component
ORDER BY cost_usd DESC;

The parts that are not obvious. WHERE year AND month uses the partitions: without that filter Athena scans the whole history and a 0.05 USD query ends up costing several euros, which is the number one source of surprise Athena bills. line_item_line_item_type filters the line type; without it Tax, Credit, Refund and RIFee get mixed in with real usage and the totals tally with nothing. line_item_unblended_cost is the unblended cost, the one matching that hour's usage — in 11-03 we will see when the amortised one is preferable. And the CASE turns nulls and empty strings into 'untagged': without it, unassigned cost disappears from the report, which is the most common way of fooling yourself.

The result for MercadoFresco's month:

Component Cost USD % Services
pedidos 604.30 27.0 % 11
tienda 512.40 22.9 % 9
shared (with no component of its own) 452.30 20.2 % 14
catalogo 286.70 12.8 % 8
analitica 214.50 9.6 % 6
reparto 118.90 5.3 % 7
untagged 48.50 2.2 % 5
Total 2,237.60 100 %

And the second query, the cost per city one, which illustrates the allocation of what cannot be tagged. It cross-references cost with an orders table Sara already has in Redshift and exports to S3:

-- Cost allocated by city: nothing is direct, it is all allocated by orders.
WITH variable_cost AS (             -- compute, database, queues and shop network
  SELECT SUM(line_item_unblended_cost) AS total
  FROM cur_mercadofresco_horario
  WHERE year = '2026' AND month = '8'
    AND line_item_usage_account_id = '111122223333'
    AND line_item_line_item_type IN ('Usage', 'SavingsPlanCoveredUsage')
    AND resource_tags_user_componente IN ('tienda', 'catalogo', 'pedidos', 'reparto')
),
orders_by_city AS (
  SELECT ciudad AS city, COUNT(*) AS orders FROM pedidos_agosto_2026 GROUP BY ciudad
),
total_orders AS (SELECT SUM(orders) AS n FROM orders_by_city)
SELECT
  p.city,
  p.orders,
  ROUND(100.0 * p.orders / MAX(t.n), 1)                    AS pct_orders,
  ROUND(MAX(c.total) * p.orders / MAX(t.n), 2)             AS allocated_cost_usd,
  ROUND(MAX(c.total) / MAX(t.n), 5)                        AS cost_per_order_usd
FROM orders_by_city p
CROSS JOIN total_orders t
CROSS JOIN variable_cost c
GROUP BY p.city, p.orders
ORDER BY p.orders DESC;
City Orders % Allocated cost Cost per order
Madrid 75,600 42.0 % 639.30 USD 0.00846 USD
Barcelona 50,400 28.0 % 426.20 USD 0.00846 USD
Valencia 30,600 17.0 % 258.70 USD 0.00846 USD
Seville 23,400 13.0 % 197.90 USD 0.00846 USD
Total 180,000 100 % 1,522.10 USD 0.00846 USD

And here you have to be honest about the result, because it is a classic trap: the cost per order comes out identical in all four cities because we allocated it in proportion to orders. The table discovers nothing about the cities; it merely translates cost into a language the business understands. For it to say something real you would have to separate out what genuinely differs — for example, if Seville had its own warehouse with dedicated infrastructure, or if one city's deliveries made more calls to the maps API. A linear allocation is an accounting convention, not a discovery. Saying so out loud when you present the table is what separates an analysis from an illusion.

Shared costs and unassignable costs

The 452.30 USD in the shared row are the most interesting part of the report, because that is where the spend nobody claims lives:

Shared item Cost Why it has no component
NAT Gateway (3 environments) 243.80 USD Every component uses them at the same time
VPC endpoints and cross-AZ transfer 118.60 USD Common network infrastructure
ALB alb-mercadofresco-tienda 47.20 USD A single load balancer for the whole shop
CloudTrail, Config, GuardDuty (security account) 38.90 USD Governance for the whole organisation
Route 53 and certificates 3.80 USD A global service

There are three ways of handling it and it is worth choosing one and writing it down: leaving it as "shared" and presenting it separately, which is the most honest option and what MercadoFresco does, because nobody argues about a figure that is not charged to them; allocating it proportionally to each component's direct cost, simple and defensible; or allocating it by real usage — bytes through the NAT, requests through the ALB — the fairest and the most expensive to calculate.

Marta's rule: only allocate when the allocation changes a decision. If nobody is going to do anything different depending on how 3.80 USD of Route 53 is charged, allocating it is wasted work. With the 243.80 USD of NAT something does change — it leads straight to the optimisation in 11-03 — and that is why it gets analysed in detail.

And then there is the genuinely unassignable cost: the 48.50 USD of untagged, where the goal is not to allocate it but to make it disappear. Every month the review takes that list, identifies the resources and either tags or deletes them: half an hour of work that then keeps itself under control.

Unit cost: MercadoFresco's cost per order

Everything above leads here. Absolute cost is a poor metric: if the bill goes from 2,237 to 2,600 USD, is that bad news? It depends entirely on whether the business grew by more or less than that. The metric that does say something is unit cost: cost divided by a unit of business value. For MercadoFresco, the natural unit is the order.

Total monthly cost (August) .... 2,237.60 USD
Orders served in August ........ 180,000
Cost per order ................. 0.01243 USD (1.24 US cents)

And the breakdown of the cost per order by component, which is where the useful conversations show up:

Component Monthly cost Cost per order Comment
pedidos 604.30 USD 0.00336 USD Aurora, queues, Step Functions, Lambdas
tienda 512.40 USD 0.00285 USD Fargate, ALB, CloudFront
shared 452.30 USD 0.00251 USD The second largest: NAT and network
catalogo 286.70 USD 0.00159 USD ElastiCache, S3, CloudFront
analitica 214.50 USD 0.00119 USD Does not scale with orders
reparto 118.90 USD 0.00066 USD DynamoDB, Lambdas
untagged 48.50 USD 0.00027 USD To be eliminated
Total 2,237.60 USD 0.01243 USD

Why this metric beats the total, with three concrete examples. Healthy growth: if orders rise 30 % and the bill 18 %, the cost per order falls, the bill has gone up and it is good news; with the absolute total, that news looks bad. Detecting inefficiency: if the cost per order rises two months running with no architectural change, something is wrong — a resource left on, a query that has become expensive, a log growing out of control. And product decisions: it lets you answer "is a 12-euro order profitable?" and "how much will opening in Portugal cost?" with a number instead of a hunch.

MercadoFresco publishes CostePorPedido as a custom metric in MercadoFresco/Tienda and puts it on the mercadofresco-negocio dashboard, next to PedidosConfirmados. It is the first time a figure from the bill lives beside a figure from the business, and that proximity is half the value. As support, three other units are calculated: cost per active customer (0.048 USD/month), cost per city served (559.40 USD) and infrastructure cost as a share of revenue (0.42 %).

Showing and charging back costs: showback and chargeback

With the allocation done, what remains is the organisational part, which is where these initiatives either die or take root. Two models:

Model What it is Effect Risk
Showback Each team is told what it spends, with no money moving Creates awareness; almost no friction It can be ignored
Chargeback The cost is charged to the team's budget Genuinely changes behaviour Arguments about the allocation; can encourage bad decisions

MercadoFresco, with three people, does showback: an automatic monthly report by Propietario on the 3rd, which is not a list of numbers but the variation against the previous month, the cost per order and a single concrete recommendation; and with the rule that nobody gets a surprise in public, because if Luis's spending has shot up, Marta tells him before the meeting and not during it.

Chargeback arrives when there are real departmental budgets. And it is worth knowing the perverse effect it produces when badly applied: if a team is charged for every test environment, it stops creating test environments and starts testing in production. A cost model that makes the engineering worse is not a saving.

Common Mistakes and Tips

Mistake: believing that applying tags is enough to see them on the bill. The resource has been tagged since March and the report still says "untagged". Tip: each tag has to be activated as a cost allocation tag in the management account, and it does not show up until up to 24 hours later.

Mistake: expecting tags to be retroactive. They are activated in August and somebody asks for May's allocation. Tip: it does not exist and it is not going to. Activate them the day you define the convention, even if coverage is 40 %.

Mistake: free-text values. Six months later Componente has the values tienda, Tienda, tienda-web, web and front. Tip: a closed list, published and enforced with an Organizations tag policy. A report with five values where there is one is useless and can no longer be fixed backwards.

Mistake: asking for tagging by email. Coverage climbs to 70 % and stays there forever. Tip: 90 % is solved in the CDK with Tags.of(stack) and in the pipeline quality gate; whatever arrives by another route is covered by IAM and audited with Config.

Mistake: forgetting --propagate-tags in ECS. The service is tagged but the tasks are not, and the largest compute cost turns up unallocated. Tip: --propagate-tags TASK_DEFINITION and --enable-ecs-managed-tags on every service.

Mistake: chasing 100 % coverage. Weeks are lost trying to tag things that cannot be tagged. Tip: the target is 95 % of resources and 97 % of taggable cost; the rest is left as "shared" and in plain sight.

Mistake: scanning the whole CUR on every Athena query. A query that should have cost 0.03 USD ends up costing several euros and is repeated every hour on a dashboard. Tip: always filter by year and month, use Parquet and set a scanned-bytes limit on the Athena workgroup.

Mistake: presenting absolute cost in the monthly meeting. The bill goes up, everyone panics and nobody knows whether that is good or bad. Tip: present the cost per order first and the total afterwards: it changes the conversation from "we spend a lot" to "we spend well or badly".

Tip: activate aws:createdBy from day one. It is free, it depends on nobody's discipline and it answers the most frequent question in cost analysis: "who switched this on?".

Tip: tag things that cost no money too and keep the convention in mercadofresco-infra, not in a stray document. A security group tagged Proyecto=mercadofresco is the difference between deleting with confidence and not daring to on clean-up day; and a versioned convention gets reviewed by pull request and does not fork.

Exercises

Exercise 1: designing the tagging for a new company

GranjaDigital sells vegetable boxes by subscription. It has a single AWS account with two environments mixed together (production and testing), four people and four systems: the subscription website, the recurring billing engine, the delivery route planner and the reporting dashboard. It charges its costs to two areas: "technology" and "logistics".

  1. Propose a set of mandatory tags with their allowed values, justifying each one.
  2. State what you would separate by account instead of by tag, and why.
  3. Write the Organizations tag policy for the environment tag.
  4. Which enforcement mechanism do you recommend first, bearing in mind they do not use IaC yet?

Exercise 2: reading an allocation report

Start from the per-component allocation you saw in the lesson: a total bill of 2,237.60 USD with 180,000 orders, of which analitica accounts for 214.50 USD. The following month, orders rise to 234,000 (+30 %) and the total bill to 2,594.00 USD.

  1. Work out the cost per order for both months.
  2. Is that good or bad news? Justify it.
  3. If analitica has gone from 214.50 to 361.00 USD and everything else has grown in proportion to orders, what would you investigate?
  4. Write the SQL query over the CUR that would let you confirm your suspicion.

Exercise 3: shared costs and decisions

Of MercadoFresco's 452.30 USD of shared cost, 243.80 USD is NAT Gateway, split like this: 100.00 USD in production, 74.00 in pre-production and 69.80 in development.

  1. Propose two different ways of charging that cost to the components and say which you would choose.
  2. Without getting into optimisation yet, which two questions does that split suggest to you?
  3. Would your allocation recommendation change if the NAT cost 12 USD a month instead of 243.80? Why?

Solutions

Solution to exercise 1

(1) Proposed mandatory tags — four, not five:

Key Values Rationale
Entorno produccion, pruebas They share an account: it is the only way to separate them
Componente web, facturacion, rutas, informes The four systems; "what does each thing cost me"
Area tecnologia, logistica The accounting allocation they already use
Propietario The names of the four people Knowing who to alert

Proyecto is not included: with a single project and a single account it would be a tag with one value, that is, noise.

(2) What to separate by account. Production and testing should be in separate accounts, and this is the most valuable recommendation in the exercise: blast radius isolation, independent quotas, clean permissions and — what matters here — complete, retroactive and unfalsifiable cost allocation, including cost that cannot be tagged. With that separation, Entorno becomes redundant and three mandatory tags remain.

(3) Tag policy for the environment:

{
  "tags": {
    "Entorno": {
      "tag_key": { "@@assign": "Entorno" },
      "tag_value": { "@@assign": ["produccion", "pruebas"] },
      "enforced_for": { "@@assign": ["ec2:instance", "ec2:volume", "rds:db",
                                     "s3:bucket", "lambda:function"] }
    }
  }
}

With the warning that enforced_for should be rolled out in two phases: first without it, to see what breaks.

(4) What to enforce first without IaC, from highest to lowest immediate return: Config with required-tags and a daily notification, which prevents nothing but gives you the real picture within 24 hours; Tag Editor for bulk tagging what already exists, an afternoon's work; the tag policy in reporting mode, to fix the spelling before it forks; and the IAM conditions only once the rest is stable. With the underlying recommendation to start using IaC: four people creating resources by hand guarantee that coverage will never get past 80 %.

Solution to exercise 2

(1) Cost per order:

Month 1: 2,237.60 / 180,000 = 0.012431 USD · Month 2: 2,594.00 / 234,000 = 0.011085 USD · change −10.8 %.

(2) It is good news, and clearly so. The bill has gone up 15.9 % while the business grew 30 %. The cost per order falls 10.8 %, which means the architecture is scaling better than linearly: there are components with a fixed cost — NAT, ALB, observability, governance — that get spread across more orders. Presenting only the total ("the bill has gone up 356 USD") would give the opposite impression and could lead to holding back growth out of fear.

(3) What to investigate in analitica. It has grown 68.3 % while the business grew 30 %: it is the only line that deviates. Hypotheses in order of likelihood: a scheduled load or query running more often than it needs to, or a Redshift workgroup that does not pause; Athena queries with no partition filter, scanning the whole CUR history every time; real growth from more historical data, which would justify 30 % but not 68 %; or a new report nobody sized.

(4) Query to confirm it:

-- Daily breakdown of the analitica component, by service and usage type,
-- comparing the two months.
SELECT
  month,
  line_item_product_code                                AS service,
  line_item_usage_type                                  AS usage_type,
  ROUND(SUM(line_item_unblended_cost), 2)               AS cost_usd,
  ROUND(SUM(line_item_usage_amount), 2)                 AS quantity
FROM cur_mercadofresco_horario
WHERE year = '2026'
  AND month IN ('8', '9')
  AND resource_tags_user_componente = 'analitica'
  AND line_item_line_item_type = 'Usage'
GROUP BY month, line_item_product_code, line_item_usage_type
ORDER BY month, cost_usd DESC;

The line_item_usage_type column is what cracks the case: it distinguishes Redshift's RPU-Hours from Athena's DataScanned-Bytes. If the growth is in DataScanned-Bytes, the cause is an unpartitioned query; if it is in RPU-Hours, it is a workgroup that does not pause. Adding line_item_resource_id to the GROUP BY would pinpoint the exact resource.

Solution to exercise 3

(1) Two ways of charging the 243.80 USD of NAT. Option A, in proportion to each component's direct cost, is trivial to calculate, stable and understandable, but it assumes that an expensive component uses more network, which is not always true: analitica is expensive and sends little out through the NAT, while reparto is cheap and calls an external maps API constantly. Option B, by real bytes processed according to the VPC flow logs, is the fair allocation, but it requires enabling and storing those logs — which cost money — processing them, and accepting that the result varies every month.

Choice: option A, and also present the NAT as a line of its own. At 243.80 USD, the difference between one allocation and the other is tens of dollars and the cost of calculating option B eats up the benefit. What matters is not who it is charged to, but that the number is in plain sight.

(2) Two questions the split suggests. First: why do development and pre-production pay 143.80 USD of NAT, almost as much as production, if there is not a single customer inside them? Almost all of that amount is the fixed hourly charge for each NAT Gateway, not the traffic, and if there are two for redundancy it is fair to ask whether a development environment needs highly available internet egress. Second: what traffic goes out through the NAT that could avoid it? ECR image pulls, S3 calls, logs to CloudWatch: all of that can go through VPC endpoints. It is the analysis 10-02 left open and that 11-03 closes with numbers.

(3) Would it change at 12 USD? Yes, completely. At 12 USD a month, the right answer is not to allocate it at all: leave it in the "shared" bucket and do not give it an hour of your time. The criterion is not accounting purity, but whether the analysis can change a decision. Twelve dollars change no decision; 243.80 USD a month is almost 2,926 USD a year and it does. The allocation effort should be proportional to the amount at stake, and that is the difference between cost management and ritual accounting.

Conclusion

MercadoFresco no longer has a bill: it has a cost model. You know why the problem was not the figure but its shape: the bill comes organised according to AWS's structure — services — and the business asks according to its own — environments, components, cities, orders. And you know that the right order for solving it is to write the questions first and only then decide the tags, because tagging strategies designed "just in case" end up with twenty keys that answer nothing.

You have the complete tagging strategy: the course's five mandatory tags with their rationale, the optional ones with their specific consumer, the spelling conventions — values in lower case, no accents, keys in PascalCase, aws: prefix forbidden — and, above all, the closed value lists, because a free-text tag always ends up with five variants of the same component. Along with the range that works in practice, 4 to 8 mandatory tags, and the unmistakable sign that there is one too many: when somebody has to ask what value to put.

You are clear that a tag is not a cost dimension until it is activated in the management account, that the AWS-generated ones — aws:createdBy, aws:cloudformation:stack-name, aws:ecs:serviceName — are free and depend on nobody's discipline, and the detail that catches so many people out: activation is not retroactive. Earlier cost stays "untagged" forever, even if the resources had been tagged for months.

You have the five mechanisms for enforcing tagging instead of asking for it, with what each one covers: CDK aspects with Tags.of(stack), which solve 90 % of the problem in one go; the pipeline quality gate, which stops a badly tagged template reaching AWS; the IAM conditions with aws:RequestTag and aws:TagKeys, including the explicit Deny that stops somebody changing the cost centre afterwards; the Organizations tag policies with enforced_for rolled out in phases; and the Config rules as a safety net, with the rule that avoids disaster: automatic remediation never invents a business value. Plus ECS's --propagate-tags TASK_DEFINITION, without which the largest compute cost turns up unallocated. And the audit that closes it all: Tag Editor for the archaeology, the tagging API for automating it, the weekly report published as the CoberturaEtiquetado metric, and the two indicators that have to be measured separately — coverage by resources and by cost — with targets of 95 % and 97 % instead of an impossible 100 %.

You have the two dimensions tags do not cover. The account, which separates better than any tag because it is complete, retroactive and impossible to falsify — hence the rule: whatever you really want to separate, separate it by account; whatever you want to analyse within one scope, separate it by tag. And the cost categories, which translate accounts and tags into business concepts with a default value that guarantees no cost goes unclassified, and which revealed the uncomfortable figure: more than a quarter of the bill is environments with not a single customer inside. All of it underpinned by the Cost and Usage Report configured with judgement — hourly, in Parquet, with RESOURCES and the Athena artefact, delivered to mercadofresco-informes-analitica — and by the SQL queries that allocate cost by component and by city, with the two details that separate a 0.03 USD query from a several-euro one: always filter by partition and by line_item_line_item_type. And with the honesty to recognise that a linear allocation by orders is an accounting convention, not a discovery.

And you have the metric that changes the conversation: the cost per order, today 0.01243 USD across 180,000 orders and a bill of 2,237.60 USD, published on the mercadofresco-negocio dashboard next to PedidosConfirmados. With the reason it beats the total: when the business grows 30 % and the bill 16 %, the total says "bad news" and the unit cost tells the truth. Plus the organisational model chosen — showback, not chargeback — and the perverse effect worth bearing in mind: a cost model that makes people stop creating test environments is not a saving.

With the bill finally readable, the next question is no longer who spends, but on what, and above all what part of all this was not needed. The 243.80 USD of NAT Gateway, the 214.90 of CloudWatch and those 452.30 of shared cost are crying out for somebody to open them up and look inside.

In 11-03, "AWS Cost Explorer", the bill really is opened up: the full breakdown by service, the billing concepts you have to understand before looking at a single chart — unblended, amortised and net cost — the three classic surprises that are almost always there, automatic anomaly detection, and the ten concrete optimisations MercadoFresco is going to execute with their saving calculated one by one.

© Copyright 2026. All rights reserved