FinOps Team
Multi-account S3 Finops Optimizer -- a cost analysis tool that scans every bucket across an AWS Organization via AWSControlTowerExecution role assumption and produces actionable cost-reduction recommendations with estimated monthly savings.
- Scans one account, a specific account, or all active accounts in the AWS Organization
- Pulls storage metrics from CloudWatch (14-day window to catch daily emission lag)
- Pulls actual S3 spend from Cost Explorer
- Analyzes every bucket for 9 cost and security issues
- Estimates potential monthly savings per bucket and per account
- Outputs results as a terminal report, PDF, Excel workbook, JSON, or CSV
| # | Check | Severity | How Savings Are Estimated |
|---|---|---|---|
| 1 | Versioning enabled with no NoncurrentVersionExpiration rule |
HIGH | 30% of current bucket cost |
| 2 | Incomplete multipart uploads with no AbortIncompleteMultipartUpload rule |
MEDIUM | ~5 GB per upload × STANDARD price |
| 3 | STANDARD storage with no lifecycle transitions | HIGH | 50% of STANDARD GB moved to STANDARD-IA |
| 4 | Reduced Redundancy Storage (RRS) — costs more than STANDARD | CRITICAL | Price delta × RRS GB |
| 5 | S3 Intelligent-Tiering not enabled on large STANDARD buckets | MEDIUM | 40% of STANDARD GB at IT savings rate |
| 6 | No lifecycle policy at all | MEDIUM | Informational |
| 7 | Cross-region / cross-account replication active | INFO | Informational |
| 8 | Public access not fully blocked | HIGH | Informational |
| 9 | Empty bucket (possible orphan) | LOW | Informational |
Python 3.8+
pip install boto3 tabulate reportlab openpyxl| Package | Required for |
|---|---|
boto3 |
AWS API calls (required) |
tabulate |
Formatted terminal tables (optional, degrades gracefully) |
reportlab |
PDF output (optional) |
openpyxl |
Excel output (optional) |
organizations:ListAccounts
sts:AssumeRole
s3:ListAllMyBuckets
s3:GetBucketLocation
s3:GetBucketVersioning
s3:GetLifecycleConfiguration
s3:ListBucketMultipartUploads
s3:GetBucketReplication
s3:GetBucketPublicAccessBlock
s3:ListBucketIntelligentTieringConfigurations
cloudwatch:GetMetricStatistics
ce:GetCostAndUsage
Note: The tool reads only — it makes no changes to any bucket, policy, or configuration.
# Current account only — no role assumption
python s3_cost_analyzer.py --scope single
# All active accounts in the AWS Organization
python s3_cost_analyzer.py --scope all
# Interactive numbered account picker
python s3_cost_analyzer.py --scope pick
# One specific account by ID
python s3_cost_analyzer.py --account-id 123456789012# Rich terminal report (default)
python s3_cost_analyzer.py --scope all
# Executive PDF (4 pages: KPI summary, per-account table, findings, bucket inventory)
python s3_cost_analyzer.py --scope all --output pdf
# Excel workbook (4 sheets: Executive Summary, All Findings, Bucket Detail, Quick Wins)
python s3_cost_analyzer.py --scope all --output excel
# Terminal + PDF + Excel in one run
python s3_cost_analyzer.py --scope all --output all
# Machine-readable JSON (pipe to jq, Lambda, Slack, etc.)
python s3_cost_analyzer.py --scope all --output json
# CSV — one row per bucket
python s3_cost_analyzer.py --scope all --output csv > s3_report.csvpython s3_cost_analyzer.py --scope all --output all \
--pdf-path SP_s3_apr2026.pdf \
--excel-path SP_s3_apr2026.xlsxIf your cross-account role has a different name than AWSControlTowerExecution:
python s3_cost_analyzer.py --scope all --role-name MyCustomCrossAccountRolepython s3_cost_analyzer.py --scope all --profile SP-managementAccount: 123456789012 (SP-prod)
-----------------------------------------------------------------------
CE S3 spend last 30 days: $8,200.00
Scanning 142 bucket(s)...
-> my-data-bucket 2 finding(s) 18.4 GB ~$0.42/mo
-> my-logs-bucket OK 2.1 GB ~$0.05/mo
...
FULL BUCKET INVENTORY
FINDINGS & RECOMMENDATIONS
PER-ACCOUNT SUMMARY
QUICK WIN CHECKLIST
Four-page report with SP navy/gold branding:
| Page | Content |
|---|---|
| 1 | Cover banner + 7 KPI summary boxes |
| 2 | Per-account cost table |
| 3 | All findings sorted by severity (CRITICAL → INFO) |
| 4 | Full bucket inventory + Quick win checklist |
| Sheet | Content |
|---|---|
| Executive Summary | KPI boxes, per-account table with SUM formulas, totals row |
| All Findings | Color-coded severity badges, detail + action columns, auto-filter |
| Bucket Detail | Full inventory, sortable, auto-filter |
| Quick Wins | 10-item checklist with a Done? column |
{
"scan_time": "2026-04-08T12:00:00Z",
"grand_est_monthly_cost": 16919.33,
"grand_est_savings": 4230.10,
"grand_ce_spend_30d": 16919.33,
"skipped_accounts": [],
"buckets": [
{
"account_id": "123456789012",
"account_name": "SP-prod",
"bucket": "my-data-bucket",
"region": "us-east-1",
"total_gb": 18.4,
"obj_count": 12400,
"est_cost_mo": 0.42,
"versioning": "Enabled",
"lifecycle": "No",
"mpu_count": 0,
"replication": false,
"public_blocked": true,
"est_savings": 0.13,
"recommendations": [ ... ]
}
]
}Color codes and Unicode box-drawing characters are emitted only when stdout is a real interactive TTY. They are suppressed automatically when:
- Output is piped or redirected (
> file,| tee) - Running in Windows
cmd.exeor PowerShell without VT100 support - The
NO_COLOR=1environment variable is set
# Force plain ASCII output in any environment
NO_COLOR=1 python s3_cost_analyzer.py --scope all --output excelManagement account credentials
│
├── --scope single ──► use base session directly
│
├── --scope pick ──► organizations:ListAccounts
│ └─ interactive prompt
│ └─ sts:AssumeRole ──► member session
│
└── --scope all ──► organizations:ListAccounts
└─ for each active account:
└─ sts:AssumeRole into AWSControlTowerExecution
├── s3:ListAllMyBuckets
├── cloudwatch:GetMetricStatistics (14d window)
├── ce:GetCostAndUsage (30d window)
└── per-bucket S3 API calls
└─ _safe() wrapper — one failing check
never blocks the rest of the bucket
Accounts where role assumption fails (e.g. blocked by SCP, role missing) are skipped with a warning — the scan continues and skipped accounts are listed in the final report.
Storage class prices used for savings estimates (us-east-1, April 2026 — update STORAGE_PRICE_PER_GB in the script if AWS pricing changes):
| Storage Class | $/GB/mo |
|---|---|
| STANDARD | $0.023 |
| STANDARD_IA | $0.0125 |
| ONEZONE_IA | $0.010 |
| INTELLIGENT_TIERING | $0.023 |
| GLACIER_IR | $0.004 |
| GLACIER | $0.0036 |
| DEEP_ARCHIVE | $0.00099 |
| REDUCED_REDUNDANCY (legacy) | $0.024 |
These are the highest-impact actions regardless of which specific buckets are flagged:
AbortIncompleteMultipartUploadlifecycle rule — 7 days — all bucketsNoncurrentVersionExpiration— 30 days — all versioned buckets- Transition
STANDARD → STANDARD_IAafter 30 days — large cold buckets - Transition
STANDARD_IA → GLACIERafter 90 days — archive workloads - Enable S3 Intelligent-Tiering on variable-access buckets
- Migrate Reduced Redundancy Storage to STANDARD immediately
- Enable S3 Storage Lens org-wide for unified visibility
- Enable Cost Allocation Tags on all buckets
- Delete empty / orphaned buckets
- Block Public Access on all non-CDN buckets
| Limitation | Detail |
|---|---|
| CloudWatch metric lag | BucketSizeBytes is published once per day and can lag 24–48 h. The script uses a 14-day lookback window to compensate. Very new buckets may still show 0 GB. |
| Cost Explorer granularity | CE spend is pulled at the account level, not per bucket. Per-bucket cost breakdown requires S3 Storage Lens with advanced metrics enabled. |
| Savings are estimates | Estimates assume 50% of STANDARD data is cold, 40% of IT savings apply, etc. Actual savings depend on access patterns. |
| Role assumption | Requires AWSControlTowerExecution (or equivalent) to exist and be assumable from the management account. Accounts with blocking SCPs are skipped. |
| Read-only | The script makes no changes to any resource. |
s3_cost_analyzer.py Main script
README.md This file
s3_cost_report.pdf Generated PDF (if --output pdf or --output all)
s3_cost_report.xlsx Generated Excel (if --output excel or --output all)
sunilp303 GitHub org