Practical infra coding

Find unattached volumes from a mocked cloud API response

easyCloud SDK automation Must-do

Problem statement

Block storage volumes keep costing money after the instance they belonged to is gone. You have saved the output of a "describe volumes" call to volumes.json. The shape is simplified from an AWS EC2 DescribeVolumes response and keeps its well-known field names (VolumeId, Size in GiB, VolumeType, State, Attachments, Tags, CreateTime).

Print every volume that is available (not attached to anything), with an estimated monthly cost, largest cost first (ties by volume ID), then a summary line. Use these made-up prices per GiB-month: gp2 0.10, gp3 0.08, io2 0.125. A volume type with no price is still listed, with ? as its cost, and named separately so it is not silently left out of the total.

Skip volumes in any other state (in-use, creating, deleting...). Tags may be missing. Use Decimal, not float, for money.

volumes.json

JSON
{
"Volumes": [
{"VolumeId": "vol-0a1", "Size": 100, "VolumeType": "gp3", "State": "in-use",
"Attachments": [{"InstanceId": "i-01", "State": "attached"}],
"Tags": [{"Key": "Name", "Value": "web-1-root"}]},
{"VolumeId": "vol-0b2", "Size": 500, "VolumeType": "gp2", "State": "available",
"Attachments": [], "CreateTime": "2026-03-02T10:15:00Z",
"Tags": [{"Key": "Name", "Value": "old-elasticsearch-data"}]},
{"VolumeId": "vol-0c3", "Size": 20, "VolumeType": "gp3", "State": "available",
"Attachments": [], "CreateTime": "2026-09-20T08:00:00Z"},
{"VolumeId": "vol-0d4", "Size": 1000, "VolumeType": "io2", "State": "available",
"Attachments": [], "CreateTime": "2026-06-11T12:00:00Z",
"Tags": [{"Key": "Name", "Value": "db-migration-scratch"}, {"Key": "owner", "Value": "dba"}]},
{"VolumeId": "vol-0e5", "Size": 50, "VolumeType": "gp3", "State": "creating",
"Attachments": []},
{"VolumeId": "vol-0f6", "Size": 8, "VolumeType": "standard", "State": "available",
"Attachments": [], "CreateTime": "2025-11-30T00:00:00Z"}
]
}

Examples

Example 1

Input: python solution.py volumes.json

Output: VOLUME TYPE GiB $/MONTH CREATED NAME vol-0d4 io2 1000 125.00 2026-06-11 db-migration-scratch vol-0b2 gp2 500 50.00 2026-03-02 old-elasticsearch-data vol-0c3 gp3 20 1.60 2026-09-20 - vol-0f6 standard 8 ? 2025-11-30 - 4 unattached volume(s), 1528 GiB, $176.60/month no price for: vol-0f6 (not included in the total)

Explanation: vol-0e5 is unattached but still creating, so it is skipped. vol-0f6 is kept in the list even though its type has no price.

Hints

Approach

Optimal

  1. Filter. unattached yields volumes whose State is available and whose Attachments list is empty. Checking the state matters: a volume that is creating has no attachments yet but is about to be used.
  2. Price. Look up the type in a price table. A missing type produces None rather than a crash or a silent zero, and the volume ID is remembered for the final warning. Using Decimal keeps 0.08 * 20 at exactly 1.60.
  3. Sort. The key (cost is None, -cost, VolumeId) puts priced volumes first, most expensive at the top, with a stable tie-break, and unpriced volumes at the end.
  4. Print. tag() scans the Tags list (which may be missing or null) for Name. CreateTime[:10] shows the date part, so a reviewer can see that a volume has been idle since March.
  5. Summary. Count, total size and total monthly cost, plus an explicit line for volumes that could not be priced. A report that quietly omits things is worse than one that says what it could not do.

v is the number of volumes.

ComplexityTime O(v log v)Space O(v)
Python
import json
import sys
from decimal import Decimal
# Made-up prices per GiB-month for this exercise (not real pricing).
PRICE_PER_GIB = {"gp2": Decimal("0.10"), "gp3": Decimal("0.08"), "io2": Decimal("0.125")}
def tag(volume, key, default="-"):
for t in volume.get("Tags") or []: # Tags may be absent or null
if t.get("Key") == key:
return t.get("Value", default)
return default
def unattached(response):
for v in response.get("Volumes", []):
# "available" = exists and is not attached. Skip creating/deleting/error states.
if v.get("State") == "available" and not v.get("Attachments"):
yield v
def main(path):
with open(path, encoding="utf-8") as f:
response = json.load(f)
rows, unknown = [], []
for v in unattached(response):
price = PRICE_PER_GIB.get(v.get("VolumeType"))
if price is None:
unknown.append(v["VolumeId"])
cost = None
else:
cost = price * v["Size"]
rows.append((cost, v))
rows.sort(key=lambda r: (r[0] is None, -(r[0] or 0), r[1]["VolumeId"]))
print(f"{'VOLUME':<9}{'TYPE':<10}{'GiB':>5} {'$/MONTH':>8} {'CREATED':<11}NAME")
for cost, v in rows:
shown = f"{cost:.2f}" if cost is not None else "?"
created = v.get("CreateTime", "")[:10] or "-"
print(f"{v['VolumeId']:<9}{v['VolumeType']:<10}{v['Size']:>5} {shown:>8} {created:<11}{tag(v, 'Name')}")
total = sum(c for c, _ in rows if c is not None)
gib = sum(v["Size"] for _, v in rows)
print(f"{len(rows)} unattached volume(s), {gib} GiB, ${total:.2f}/month")
if unknown:
print(f"no price for: {', '.join(unknown)} (not included in the total)")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "volumes.json")

Follow-up questions

  • Only report volumes that have been unattached for more than 14 days. What data would you need, given CreateTime is not the detach time?
  • Run it across every region and account, and merge the results into one report.
  • Snapshot each volume before deleting it, and record the snapshot ID in a tag.

Frequently asked questions

Orphaned volumes are one of the most common sources of cloud waste, and finding them is a typical first automation task for cloud and FinOps work. The interview version checks that you read nested API data carefully and handle missing fields.

Not by default. An unattached volume may be a deliberate backup or waiting for a migration. The usual flow is report first, tag or snapshot, notify the owner, and delete only after a grace period, ideally behind an explicit --delete flag.

With boto3 you would call the EC2 client's describe_volumes with a filter on status=available and use a paginator, because real accounts return results in pages. The filtering and reporting code stays the same; only the data source changes. Check the SDK documentation for the exact response fields.