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
- The real problem: a bill nobody can read
- What questions MercadoFresco needs to be able to answer
- Designing the tagging strategy
- Naming conventions and closed value lists
- How many tags are too many, and which resources cannot be tagged
- Activating the cost allocation tags
- The critical detail: tags are not retroactive
- Enforcing tagging instead of asking for it
- Organizations tag policies
- IAM conditions:
aws:RequestTagandaws:TagKeys - Config rules with remediation
- CDK aspects, ECS and the pipeline
- Which mechanism covers what: the decision table
- Auditing what is not tagged and setting a realistic target
- The account as a cost dimension
- Cost categories: from resource to business concept
- The Cost and Usage Report (CUR)
- Querying the CUR with Athena
- Shared costs and unassignable costs
- Unit cost: MercadoFresco's cost per order
- Showing and charging back costs: showback and chargeback
- Common mistakes and tips
- Exercises
- 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 tableFour 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_TYPEA 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: 111122223333 → produccion |
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=operaciones3. 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: addsline_item_resource_id. Without it the report does not say which resource each line belongs to, which is half the value.SPLIT_COST_ALLOCATION_DATAadditionally splits the cost of an ECS task across its containers.AdditionalArtifacts: ATHENA: generates the manifest and theCREATE TABLEwithout writing the schema by hand. AndRefreshClosedReportsreprocesses closed months when late adjustments or credits arrive.- Destination
mercadofresco-informes-analitica, Sara's own bucket, with a policy allowingbillingreports.amazonaws.comto 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".
- Propose a set of mandatory tags with their allowed values, justifying each one.
- State what you would separate by account instead of by tag, and why.
- Write the Organizations tag policy for the environment tag.
- 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.
- Work out the cost per order for both months.
- Is that good or bad news? Justify it.
- If
analiticahas gone from 214.50 to 361.00 USD and everything else has grown in proportion to orders, what would you investigate? - 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.
- Propose two different ways of charging that cost to the components and say which you would choose.
- Without getting into optimisation yet, which two questions does that split suggest to you?
- 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.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
