Background

ARNs, Tagging, and Quotas

11 min read

Goal: the connective tissue. ARNs are the identifier every policy and tool consumes, tags are what make cost attributable and automation targetable, and quotas are the limits that bite on launch day. None of the three is glamorous; all three cause outages and unattributable bills when neglected.


1. ARNs

The analogy: a full postal address. Country, city, street, house number — enough to identify one thing unambiguously anywhere in the world.

The technical version: an Amazon Resource Name identifies a resource globally:

arn:partition:service:region:account-id:resource
     │         │        │        │         │
     │         │        │        │         └─ resource type and/or identifier
     │         │        │        └─ 12-digit account ID
     │         │        └─ region (empty for global services)
     │         └─ service namespace: ec2, s3, iam, lambda
     └─ aws | aws-cn | aws-us-gov

Worked examples, including the irregular ones:

arn:aws:ec2:eu-west-1:123456789012:instance/i-0abc123def456
arn:aws:iam::123456789012:role/deploy-pipeline           ← no region: IAM is global
arn:aws:s3:::acme-artifacts                              ← no region, no account
arn:aws:s3:::acme-artifacts/api/v2/app.tar.gz            ← the object, not the bucket
arn:aws:lambda:eu-west-1:123456789012:function:process-upload
arn:aws:dynamodb:eu-west-1:123456789012:table/orders

Why S3 bucket ARNs have no account or region: bucket names are globally unique, so the name alone identifies the bucket. It's a historical quirk with a practical consequence — you can't tell from a bucket ARN which account owns it, which is exactly why aws:ResourceAccount and aws:PrincipalOrgID conditions exist.

Separators are inconsistent — some services use type/id, others type:id, some just id. Don't construct ARNs by string concatenation and hope. Get them from the API or from Terraform outputs.

In policies, ARNs take wildcards — and this is where least privilege is actually expressed:

Pattern Matches
arn:aws:s3:::acme-artifacts The bucket itself — for ListBucket
arn:aws:s3:::acme-artifacts/* Every object in it — for GetObject
arn:aws:s3:::acme-artifacts/api/* Objects under one prefix
arn:aws:ec2:eu-west-1:123456789012:instance/* Every instance in one region and account
* Everything. Rarely correct for data actions

The anatomy of an ARN, labelling partition, service, region, account, and resource, with examples showing how S3 and IAM omit fields


2. Naming

ARNs are assigned; names are yours to choose, and a convention pays for itself the first time someone has to find something at 3 a.m.

A workable pattern:

<org>-<service>-<environment>-<region-short>-<detail>

acme-payments-prod-euw1-alb
acme-payments-prod-euw1-artifacts
acme-shared-dev-use1-terraform-state
Rule Why
Environment in the name The single most useful thing to see in a console list
Lowercase and hyphenated Some services reject uppercase or underscores; consistency avoids memorising which
Avoid dates and ticket numbers They rot; a resource outlives its ticket
Never encode secrets or personal data Names appear in logs, metrics, URLs, and support cases
Leave room for a suffix Many resources need -2 during a blue/green replacement

Some names are globally unique and unchangeable. S3 bucket names are shared across every AWS customer — hence the organisation prefix, which is about collision avoidance as much as tidiness. Many resources also cannot be renamed at all; changing the name in Terraform means destroy and recreate, which for a bucket or a database is a data-loss event rather than a cosmetic change. Choose deliberately the first time.


3. Tagging

The analogy: luggage labels. The bag travels fine without one — right up until it's in a pile with four hundred others and someone needs to know whose it is and where it's going.

Tags are key/value pairs on resources. They look optional. They are the basis of:

Capability How tags enable it
Cost allocation The only way to attribute spend below the account level
Access control (ABAC) aws:ResourceTag conditions — see IAM & Identity
Automation targeting SSM Run Command, Ansible dynamic inventory, start/stop schedulers
Backup selection AWS Backup plans select by tag, so new resources are protected automatically
Compliance reporting Config rules that check for required tags
Incident response "Who owns this?" answered in seconds instead of by asking around

A schema that survives

Four tags carry most of the value. Adding twenty is how tagging initiatives die.

Tag Example Why
Environment prod | staging | dev Policy, cost, and blast-radius decisions all key off it
Service payments-api The unit people actually think in
Owner platform-team Who to contact — a team, never an individual who may leave
CostCenter CC-4471 What finance needs

Optional additions where they earn their place: ManagedBy (terraform), DataClassification, Compliance, ExpiryDate for sandbox cleanup automation.

The rules people learn the hard way

  • Tag keys are case-sensitive. Environment, environment, and ENVIRONMENT are three different tags, and your cost report will show three columns. Fix the case in the schema, then enforce it.
  • aws: is a reserved prefix. You can't create tags starting with it.
  • Cost allocation tags must be activated in the Billing console before they appear in cost reports — and activation is not retroactive. Tagging diligently for six months and only then activating gives you six months of unattributed history you cannot recover. Activate the schema on day one.
  • Not every resource supports tags, and some support them only at creation.
  • There's a per-resource tag limit (commonly 50 user tags). ⚠️ verify current limits against AWS docs
  • Tags don't propagate automatically. An Auto Scaling group tags its instances only if propagate_at_launch is set; an EBS volume gets the instance's tags only if the launch template says so. Untagged child resources are the most common gap in an otherwise disciplined estate.

Enforcement, because voluntary tagging doesn't work

Three layers, and you want at least two:

// SCP: refuse to create an instance without an Environment tag
{
  "Effect": "Deny",
  "Action": ["ec2:RunInstances"],
  "Resource": "arn:aws:ec2:*:*:instance/*",
  "Condition": {
    "Null": { "aws:RequestTag/Environment": "true" }
  }
}
Layer What it does Limitation
Organizations tag policies Define allowed keys and values, and report non-compliance On their own they report; they don't block creation
SCPs with aws:RequestTag/aws:TagKeys Actually refuse untagged creates Must be written per resource type; can be blunt
Terraform default_tags Applies tags automatically to everything a provider creates Only covers what Terraform creates
provider "aws" {
  region = "eu-west-1"

  default_tags {
    tags = {
      Environment = var.environment
      Service     = var.service_name
      Owner       = var.owning_team
      CostCenter  = var.cost_centre
      ManagedBy   = "terraform"
    }
  }
}

default_tags is the cheapest large win available — one block, and everything Terraform creates is tagged. It doesn't cover console-created or console-modified resources, which is one more argument for the pipeline being the only mutation path.

Retrofitting is possible with Resource Groups and the Tag Editor for bulk edits, but it's manual, partial, and — for cost history — too late. Start with the schema.

Tags enabling cost allocation, attribute-based access control, automation targeting, and backup selection from a single set of key-value pairs on a resource


4. Service Quotas

The analogy: a credit limit rather than a speed limit. It caps how much you can have, not how fast you can ask — that's throttling, covered in The API & Control Plane. Confusing the two produces support tickets that go nowhere.

Quotas are per account, per region. Both halves matter:

  • Per account — one team's consumption doesn't have to be another's problem, if they're separated. One more argument for the multi-account model in Accounts & Organizations.
  • Per region — and this is the one that causes outages. A new region starts you at defaults, no matter how much headroom you negotiated elsewhere. Expansion day, launch day, and DR failover day are all moments when a team discovers a limit they thought they'd raised eighteen months ago.
Kind Meaning What to do
Soft The default; raisable on request Request an increase — days ahead, not during the incident
Hard Architectural; not raisable Design around it
Not a quota at all InsufficientInstanceCapacity — AWS is momentarily out of that shape in that AZ Diversify types and AZs; Capacity Reservations for critical launches
# What can I actually have?
aws service-quotas list-service-quotas --service-code ec2 \
  --query 'Quotas[].[QuotaCode,QuotaName,Value]' --output table

# One specific quota
aws service-quotas get-service-quota \
  --service-code ec2 --quota-code L-1216C47A

# Ask for more
aws service-quotas request-service-quota-increase \
  --service-code ec2 --quota-code L-1216C47A --desired-value 512

# Track the request
aws service-quotas list-requested-service-quota-change-history \
  --service-code ec2 --query 'RequestedQuotas[].[QuotaName,DesiredValue,Status]' --output table

Not every limit appears in Service Quotas. Some still require a support case, and some aren't documented as quotas at all until you hit them. ⚠️ verify coverage for the specific service you need

Monitor quotas, don't discover them

AWS publishes usage metrics for many quotas in the AWS/Usage CloudWatch namespace, so you can alarm at a percentage of the limit rather than finding out at 100%:

aws cloudwatch put-metric-alarm \
  --alarm-name ec2-vcpu-quota-80pct \
  --namespace AWS/Usage \
  --metric-name ResourceCount \
  --dimensions Name=Service,Value=EC2 Name=Type,Value=Resource Name=Resource,Value=vCPU Name=Class,Value=Standard/OnDemand \
  --statistic Maximum --period 300 --evaluation-periods 1 \
  --threshold 0.8 --comparison-operator GreaterThanThreshold

Quota increases take time — hours to days, longer for large GPU or specialised requests, and they go through a human for anything substantial. Build the request into your launch checklist alongside capacity planning, and re-run it for every new region.

A quota template in Organizations can apply requested increases automatically to newly created accounts, which removes the "we forgot this account existed" failure mode.


5. Practical audit

# Untagged resources, via the Resource Groups Tagging API
aws resourcegroupstaggingapi get-resources \
  --query 'ResourceTagMappingList[?length(Tags)==`0`].ResourceARN' --output text

# Resources missing a specific required tag
aws resourcegroupstaggingapi get-resources \
  --query "ResourceTagMappingList[?!(Tags[?Key=='Environment'])].ResourceARN" --output text

# Which tag keys are actually in use — reveals the case-sensitivity mess
aws resourcegroupstaggingapi get-tag-keys --output text

# Quota utilisation snapshot for a service
aws service-quotas list-service-quotas --service-code ec2 \
  --query 'Quotas[?Adjustable==`true`].[QuotaName,Value]' --output table

Run the first two on a schedule and alert on growth. Untagged resources accumulate quietly, and every one is spend nobody can attribute.


6. Anti-patterns

Anti-pattern Why it hurts Instead
Constructing ARNs by string concatenation Separator formats differ per service Take them from the API or Terraform outputs
"Resource": "*" on data actions Any bug becomes an account-wide incident Scope to specific ARNs and prefixes
Tagging as an afterthought Cost history can't be reconstructed Schema and activation on day one
Inconsistent tag key case Three columns for one concept in every report Enforce with tag policies
Twenty required tags Nobody complies; the schema is abandoned Four required, more optional
Owner set to a person They leave; the tag rots Team or distribution list
Relying on tag policies alone to block They report, they don't prevent Pair with SCPs using aws:RequestTag
Forgetting propagate_at_launch Auto-scaled instances arrive untagged Set it; tag volumes in the launch template too
Assuming quota headroom carries to a new region Quotas are per region Request before launch; use quota templates
Discovering quotas at 100% An outage instead of a ticket AWS/Usage alarms at 80%
Confusing a quota with throttling The wrong fix, applied slowly Quota = how much; throttling = how fast

Check yourself

  • Why does an S3 bucket ARN have no region or account, and what problem does that create?
  • Name four distinct things that stop working properly without tags.
  • Why must cost allocation tags be activated on day one rather than when you first need a report?
  • A tag policy is in place but untagged resources keep appearing. Why, and what do you add?
  • What's the difference between a quota and throttling? Which does a Service Quotas request fix?
  • Your DR failover to a second region fails to launch enough capacity. What did nobody check?
  • Why is renaming an S3 bucket in Terraform a data-loss event rather than a cosmetic change?

That's Foundations

You now have the five things that are true of every AWS service: the account as the real boundary, one signed regional API behind every tool, one IAM evaluation algorithm, the region/AZ geography, and the ARN/tag/quota connective tissue.

Every service topic assumes this and links back to it rather than repeating it. Start with EC2, or pick whatever you need from the full contents.

← Back to the Foundations overview · ← Previous: Regions & Availability