Build a Simple Inventory Script
Learn how to build a simple inventory script in Python for DevOps automation. Step-by-step hands-on tutorial with practical examples, troubleshooting tips, and what to study next.
Focus: build a simple inventory script
You're staring at a spreadsheet of 400 EC2 instances, a requirements.txt that's drifted from production, or a list of Kubernetes nodes that no one quite remembers provisioning. Manual inventory tracking is the silent killer of DevOps velocity — it's slow, error-prone, and by the time you notice a discrepancy, a security patch has already been missed. This lesson shows you how to build a simple inventory script in Python that turns scattered infrastructure data into a single, trustworthy source of truth — the kind of script that saves hours every week and makes you the person who always knows what's running where.
The problem this lesson solves
Infrastructure sprawl is real. Cloud resources get launched, containers get deployed, and servers get decommissioned — but the documentation rarely keeps up. The painful symptoms are familiar:
- You SSH into a box that was supposed to be terminated months ago.
- A critical service runs on an instance type that's no longer cost-efficient.
- Auditors ask for a list of all production instances, and you scramble through AWS Console screenshots and stale spreadsheets.
A simple inventory script automates the discovery and reporting of your infrastructure. It pulls live data from APIs, normalizes it into a consistent format, and outputs a clean report. This isn't just a nice-to-have; it's the foundation for capacity planning, cost optimization, and security compliance.
Pro tip: The difference between a good DevOps engineer and a great one is often just the ability to answer "what do we have running right now?" in under a minute. An inventory script gives you that superpower.
Core concept / mental model
Think of your infrastructure as a library. The books (servers, databases, containers) are scattered across different floors (cloud providers, regions, on-prem racks). A manual inventory is like asking a librarian to check every shelf by hand every morning. An automated script is the card catalog — it queries a central index, sorts the books, and gives you a map in seconds.
In technical terms, an inventory script follows a simple collect → normalize → report pattern:
- Collect: Query infrastructure providers (AWS, Azure, GCP, Kubernetes, or even local files) using their APIs.
- Normalize: Convert the raw responses into a consistent Python data structure — typically a list of dictionaries.
- Report: Output the data in a human-friendly format (CSV, JSON, or plain text) or feed it into another system.
The key abstraction is the inventory entry — one dictionary that represents a single resource. Every resource type (EC2 instance, S3 bucket, Docker container) can be reduced to a few essential fields: name, ID, type, status, region, and tags.
Here's the mental model in pseudo-code:
for each provider in providers:
resources = provider.fetch_all_resources()
for resource in resources:
inventory.append(normalize(resource))
print_inventory(inventory)
The elegance is that the provider-specific logic is isolated in one function, while the reporting stays generic. Add a new provider later? Just write another fetch function.
How it works step by step
Let's break down the process of building a simple inventory script from scratch. We'll use a mix of Python standard library and a couple of popular DevOps libraries to make it realistic.
1. Define the inventory schema
First, decide what fields every entry should have, regardless of the source. A minimal but useful schema:
name: Human-readable identifierid: Unique provider-assigned IDtype: EC2, S3, Pod, etc.status: running, stopped, active, etc.region: cloud region or on-prem zonetags: dictionary of key-value labels
2. Fetch resources from a provider
For AWS, that's the boto3 library. For Kubernetes, kubernetes client. For this example, we'll simulate a simple provider to keep the focus on the pattern.
3. Normalize the data
Write a function per provider that maps raw API responses to your schema.
4. Aggregate and report
Use Python's csv module for a CSV report, or json for machine-readable output.
5. Make it reusable
Wrap everything in functions, use argparse for CLI flags, and handle errors gracefully.
The cause-and-effect chain is clear: better data collection → normalized rows → reliable reports → informed decisions.
Hands-on walkthrough
Let's build a working inventory script step by step. We'll start with a simulated provider, then show how to plug in real AWS data.
Example 1: The basic pattern (no external dependencies)
This version uses a hardcoded list to demonstrate the normalize-and-report flow.
# inventory_basic.py
import json
# Simulated raw data from a provider API
raw_servers = [
{"InstanceId": "i-12345", "InstanceType": "t3.micro", "State": {"Name": "running"}, "Tags": [{"Key": "env", "Value": "prod"}]},
{"InstanceId": "i-67890", "InstanceType": "m5.large", "State": {"Name": "stopped"}, "Tags": [{"Key": "env", "Value": "dev"}]},
]
def normalize_aws_instance(raw):
"""Convert AWS raw dict to our inventory schema."""
tags = {t["Key"]: t["Value"] for t in raw.get("Tags", [])}
return {
"name": tags.get("Name", raw["InstanceId"]),
"id": raw["InstanceId"],
"type": "EC2",
"status": raw["State"]["Name"],
"region": "us-east-1", # would come from client
"tags": tags,
}
def build_inventory(raw_list, normalizer):
return [normalizer(item) for item in raw_list]
inventory = build_inventory(raw_servers, normalize_aws_instance)
print(json.dumps(inventory, indent=2))
Expected output:
[
{
"name": "prod-server",
"id": "i-12345",
"type": "EC2",
"status": "running",
"region": "us-east-1",
"tags": {"env": "prod"}
},
{
"name": "dev-server",
"id": "i-67890",
"type": "EC2",
"status": "stopped",
"region": "us-east-1",
"tags": {"env": "dev"}
}
]
Example 2: Pulling real AWS data with boto3
Now replace the simulation with a live AWS query. Install boto3 first: pip install boto3.
# inventory_aws.py
import boto3
import json
from datetime import datetime
def fetch_ec2_instances():
ec2 = boto3.client("ec2", region_name="us-east-1")
response = ec2.describe_instances()
instances = []
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
tags = {t["Key"]: t["Value"] for t in instance.get("Tags", [])}
instances.append({
"name": tags.get("Name", "(untagged)"),
"id": instance["InstanceId"],
"type": instance["InstanceType"],
"status": instance["State"]["Name"],
"region": ec2.meta.region_name,
"tags": tags,
"launch_time": instance["LaunchTime"].isoformat()
})
return instances
def save_inventory(instances, filename="inventory.json"):
with open(filename, "w") as f:
json.dump(instances, f, indent=2)
print(f"Saved {len(instances)} instances to {filename}")
if __name__ == "__main__":
inventory = fetch_ec2_instances()
save_inventory(inventory)
Run it: python inventory_aws.py. You'll get a JSON file listing every instance in your default region.
Example 3: Adding a CSV report
CSV is the lingua franca for spreadsheets. Add a function to export to CSV using csv.DictWriter.
# report_csv.py
import csv
from inventory_aws import fetch_ec2_instances
def write_csv(instances, filename="inventory.csv"):
if not instances:
print("No instances to write.")
return
fieldnames = ["name", "id", "type", "status", "region", "tags"]
with open(filename, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for inst in instances:
inst["tags"] = ";".join(f"{k}={v}" for k, v in inst["tags"].items())
writer.writerow(inst)
print(f"Wrote {len(instances)} rows to {filename}")
if __name__ == "__main__":
write_csv(fetch_ec2_instances())
The output file will open directly in Excel or Google Sheets.
Example 4: Adding a simple filter
Filter to only running instances or a specific tag — common for operational queries.
# filter_inventory.py
def filter_by_tag(instances, key, value):
return [inst for inst in instances if inst["tags"].get(key) == value]
# Demo with simulated data
simulated = [
{"id": "i-1", "status": "running", "tags": {"env": "prod"}},
{"id": "i-2", "status": "stopped", "tags": {"env": "dev"}},
]
print(filter_by_tag(simulated, "env", "prod"))
# Output: [{'id': 'i-1', 'status': 'running', 'tags': {'env': 'prod'}}]
Compare options / when to choose what
You have several ways to store and report inventory data. Here's a comparison:
| Approach | Pros | Cons | Best for |
|---|---|---|---|
| JSON file | Simple, machine-readable, easy to parse with Python | Not human-friendly for large datasets | API responses, configuration files |
| CSV file | Universal spreadsheet support, version-control friendly | Loses nested data structure (tags flattened) | Reporting to business teams, audits |
| SQLite database | Queryable with SQL, scalable to thousands of rows | Requires schema design, extra dependency | Long-term inventory tracking with history |
| Cloud provider console | Real-time, no code | Not scriptable, manual, limited filtering | Quick spot checks, one-off inspection |
When to choose what?
- For a simple one-off audit, use a JSON or CSV file.
- If you need recurring reports and history, move to SQLite.
- If you need to automate actions (e.g., stop untagged instances), your script should output JSON so other tools can consume it.
As variations, you could use pandas for heavy data manipulation, or click for a nicer CLI interface.
Troubleshooting & edge cases
1. boto3 raises NoCredentialsError
Symptom: botocore.exceptions.NoCredentialsError: Unable to locate credentials.
Fix: Configure AWS credentials via environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or run aws configure.
2. Pagination — you only see the first 100 instances
Symptom: You get fewer results than expected.
Fix: The AWS API returns results in pages. Use pagination with paginator = ec2.get_paginator("describe_instances") and iterate pages.
paginator = ec2.get_paginator("describe_instances")
for page in paginator.paginate():
for reservation in page["Reservations"]:
for instance in reservation["Instances"]:
# process instance
pass
3. Instances without tags
Symptom: KeyError: 'Name' when accessing tags.
Fix: Always use .get() with a default value, as shown in the examples.
4. Timezone-aware timestamps in JSON
Symptom: TypeError: Object of type datetime is not JSON serializable.
Fix: Call .isoformat() on datetime objects before storing them, as done in the boto3 example.
5. Empty inventory
Symptom: No data output. Fix: Check that your filters are correct and that you have permission to describe instances (IAM policy).
What you learned & what's next
You've built a simple inventory script that collects, normalizes, and reports infrastructure data. You can now:
- Explain the core idea behind building an inventory script — the collect → normalize → report pattern.
- Apply this pattern in a hands-on exercise, using both simulated data and real AWS APIs.
- Handle common edge cases like missing tags, pagination, and timezone serialization.
- Decide between JSON, CSV, and SQLite based on your use case.
You've also practiced key DevOps automation patterns: API integration, data normalization, and reporting. These skills are the foundation for more advanced topics like configuration drift detection and auto-remediation.
Next up in the Python for DevOps automation track: you'll learn how to schedule and deploy this inventory script — turning a one-off script into a scheduled cron job that runs every morning and sends you a Slack summary. That's where your script really starts paying dividends.
Practice recap
Take your inventory_aws.py script and add a --tag Key=Value filter that only lists instances with a specific tag. Then output the result to a CSV file. Run it against your real AWS account (or a local mock) and confirm you can filter by env=prod to see only production instances.
Common mistakes
- Forgetting to handle pagination in cloud APIs — you only get the first page of results, missing most of your inventory.
- Assuming every resource has a 'Name' tag — always use
.get('Name', 'unknown')to avoidKeyErrorcrashes. - Storing datetime objects directly in JSON — convert to
.isoformat()first or you'll hit serialization errors. - Hardcoding region names instead of making them configurable — your script breaks when you move to another region.
- Not checking AWS credentials before running — you get cryptic
NoCredentialsErrorinstead of a friendly message.
Variations
- Use
pandasinstead ofcsvfor data manipulation and analysis — supports grouping, filtering, and Excel export. - Use
clickorargparseto add CLI flags (e.g.,--region,--tag,--format) — makes the script reusable across teams. - Store inventory in a SQLite database to keep historical snapshots — enables 'what changed since yesterday' reports.
Real-world use cases
- Generate a weekly CSV report of all AWS EC2 instances with tags, region, and status for finance to audit cloud costs.
- Track Kubernetes node inventory across clusters for capacity planning, checking for unused nodes or version drift.
- Produce a JSON inventory of all S3 buckets and their encryption status for security compliance checks before an audit.
Key takeaways
- An inventory script follows a collect → normalize → report pattern — isolate provider logic from reporting.
- Always define a consistent schema (name, id, type, status, region, tags) for every resource type.
- Use pagination and handle missing tags to avoid incomplete or crashing scripts.
- Output format depends on your audience: JSON for machines, CSV for humans, SQLite for history.
- A simple script that runs reliably beats a complex solution that no one maintains.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.