Report instances missing required tags
Problem statement
Your tagging policy says every instance must carry owner, env and cost-center, with non-blank values. Finance cannot allocate the bill for anything else. Given a saved "describe instances" response, report every instance that breaks the policy.
The file is a simplified version of an AWS EC2 DescribeInstances response: instances are nested inside Reservations[].Instances[], the state is State.Name, and tags are a list of {"Key", "Value"} pairs that may be missing entirely.
- Ignore
terminatedandshutting-downinstances; they are on their way out. Stopped instances still count (their volumes still cost money). - Tag keys are case-sensitive. If a required key is missing but exists with different casing (
Owner), say so: it is the most common cause and the fix is obvious. - A value that is empty or only whitespace counts as missing.
- Print one line per non-compliant instance, sorted by instance ID, then
compliant/checked. Exit 1 if anything is non-compliant.
instances.json
{ "Reservations": [ {"Instances": [ {"InstanceId": "i-0aa1", "State": {"Name": "running"}, "Tags": [{"Key": "Name", "Value": "web-1"}, {"Key": "owner", "Value": "web-team"}, {"Key": "env", "Value": "prod"}, {"Key": "cost-center", "Value": "cc-104"}]}, {"InstanceId": "i-0bb2", "State": {"Name": "running"}, "Tags": [{"Key": "Name", "Value": "web-2"}, {"Key": "Owner", "Value": "web-team"}, {"Key": "env", "Value": "prod"}]} ]}, {"Instances": [ {"InstanceId": "i-0cc3", "State": {"Name": "stopped"}}, {"InstanceId": "i-0dd4", "State": {"Name": "terminated"}}, {"InstanceId": "i-0ee5", "State": {"Name": "running"}, "Tags": [{"Key": "Name", "Value": "batch-7"}, {"Key": "owner", "Value": " "}, {"Key": "env", "Value": "dev"}, {"Key": "cost-center", "Value": "cc-221"}]} ]} ]}Examples
Example 1
Input: python solution.py instances.json; echo "exit: $?"
Output: i-0bb2 (web-2, running): owner missing (found 'Owner', wrong case); cost-center missing
i-0cc3 (-, stopped): owner missing; env missing; cost-center missing
i-0ee5 (batch-7, running): owner is empty
1/4 instances compliant
exit: 1
Explanation: i-0dd4 is terminated and not counted. i-0cc3 has no Tags key at all. i-0ee5 has an owner tag whose value is only spaces.
Hints
Approach
Optimal
- Flatten.
instances()walksReservationsand yields each instance. The response groups instances by the launch request that created them, which is irrelevant for a tag audit, so hide that nesting early. - Filter by state.
State.Nameinterminatedorshutting-downis skipped. Every other state is checked; skipping stopped instances would hide exactly the forgotten machines this report is meant to find. - Normalize tags.
inst.get("Tags") or []covers both a missing key and an explicitnull. The list becomes aKey -> Valuedict, plus a lowercase index for the case check. - Check each required key. Present with a blank value (after
strip()) is "empty"; absent but present in another case is "wrong case"; otherwise "missing". Each instance gets all of its problems in one line, so the owner can fix everything in one go. - Report. Sort by instance ID for stable output, print the compliance ratio, and exit 1 if anything failed so the script can run as a scheduled policy check.
i is the number of instances and t the number of tags per instance.
O(i * t)Space O(t)import jsonimport sys REQUIRED = ["owner", "env", "cost-center"]IGNORE_STATES = {"terminated", "shutting-down"} def instances(response): for reservation in response.get("Reservations", []): yield from reservation.get("Instances", []) def check(instance): # Returns a list of human-readable problems for one instance. tags = {t["Key"]: t.get("Value", "") for t in instance.get("Tags") or []} by_lower = {k.lower(): k for k in tags} problems = [] for key in REQUIRED: if key in tags: if not tags[key].strip(): problems.append(f"{key} is empty") elif key in by_lower: problems.append(f"{key} missing (found '{by_lower[key]}', wrong case)") else: problems.append(f"{key} missing") return tags.get("Name", "-"), problems def main(path): with open(path, encoding="utf-8") as f: response = json.load(f) checked = compliant = 0 report = [] for inst in instances(response): state = inst.get("State", {}).get("Name", "unknown") if state in IGNORE_STATES: continue checked += 1 name, problems = check(inst) if problems: report.append((inst["InstanceId"], name, state, problems)) else: compliant += 1 for iid, name, state, problems in sorted(report): print(f"{iid} ({name}, {state}): {'; '.join(problems)}") print(f"{compliant}/{checked} instances compliant") return 1 if report else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "instances.json"))Follow-up questions
- Also validate values:
envmust be one ofdev,staging,prod. - Group the report by the
ownertag so each team gets its own list. - Generate the tag-fix commands (or API calls) for the wrong-case cases, but only print them unless
--applyis given.
Frequently asked questions
Tag hygiene drives cost allocation, ownership and automated cleanup. Cloud and platform teams routinely write audits like this, and interviewers use it to check that you can navigate nested API responses and handle the messy cases: missing lists, wrong case, blank values.
Because the tools consuming the tags usually will not. Cost allocation and many policy engines treat tag keys as case-sensitive, so Owner and owner are different keys. The report should flag it so the tag gets fixed at the source, rather than hide it.
Report-after-the-fact is the second line of defence. The first is preventing it: enforce required tags in your infrastructure-as-code modules, or with the provider's policy features that deny creation of untagged resources.