How can I find untagged AWS EC2 instances for cost governance using Python?
Quick Answer
Use boto3's describe_instances paginator to walk every running instance in a region, build a set of each instance's existing tag keys, and flag any instance missing a required key like owner. Pagination matters because a single API call caps out at roughly 1,000 instances, so a naive single-call approach silently misses resources in any account with a real EC2 footprint.
Detailed Answer
Imagine a shared office building where every desk is supposed to have a nameplate showing which team owns it, but facilities can only fit a limited number of desks on one clipboard page — if the person doing rounds only checks the first page and calls it done, half the building's desks never get inspected, and desks without nameplates on the missed pages stay invisible forever even though they're still using power, space, and the janitorial budget.
This script exists because cloud costs quietly become unowned costs — an EC2 instance launched without a required tag, like owner or cost-center, can run for months, accumulating real spend, with nobody able to answer "who launched this and can we turn it off?" during a cost review. FinOps and platform teams write governance scripts like this specifically because manual tag audits don't scale past a handful of instances, and because tagging policy is only enforceable if something actually checks compliance on a recurring basis rather than trusting that engineers remembered the rule at launch time.
The script calls ec2.describe_instances with a filter for running state, but critically uses get_paginator('describe_instances') rather than a single call, because the EC2 API returns results in pages, using a NextToken, and any account with more than roughly 1,000 instances in scope will silently truncate results without pagination. For each instance, it builds a Python set from the Tags list, where each tag is a Key/Value dict, and checks whether the required key is a member of that set — using a set rather than scanning the list repeatedly keeps the lookup efficient even across accounts with thousands of instances and dozens of tags each.
In production, this script is typically scheduled as a daily Lambda function or CI job, with findings posted to Slack or written to a compliance dashboard rather than just printed to a console. What to monitor: the trend of untagged-instance count over time, since a sudden spike usually means a new automation or a new team started launching instances without going through the standard provisioning path that applies tags automatically, and false positives from instances tagged through mechanisms this script doesn't check, like tags applied only at the Auto Scaling Group level that don't propagate to instances unless PropagateAtLaunch is explicitly enabled.
The non-obvious gotcha is that describe_instances only reflects tags as of the API call, and tags applied moments after instance launch, common with automation that launches first and then tags in a follow-up API call, can create a race condition where a perfectly compliant, soon-to-be-tagged instance gets flagged as a false positive if the governance script runs in that narrow window. Mature versions of this script add a grace period, skipping instances launched less than 15 to 30 minutes ago, to avoid paging someone about a tagging violation that's actually just eventual consistency in the provisioning pipeline.