I always ask myself what I want to become in the future. The AI era came too fast and I felt both fear (am I going to be redundant?) and unlimited possibility (now I have all the knowledge, the skills, and the appropriate time to achieve whatever I want). I will start with the possibilities and I will finish with my fears. I always wanted to develop software that can reduce carbon footprint and cost on AWS. In one of my previous companies my focus was the cost optimization pillar. At first I thought it was not interesting, but from a stakeholder's perspective money always matters (well, this is not always true — some companies would never care about the costs). Years went by and now I think I am mature enough to develop something that could actually save costs. I focused on Strands SDK because it is a model-driven approach to agent architecture. It mimics how you do OOP, or how you model the solution for a difficult problem — you define the problem, you select the inputs, and you understand the connections between the problem, the inputs, and the final solution. With experience I understood that selecting and writing the right tools for a single agent could lead to better results than reaching for a swarm architecture right away. Contents The model: configuration as the single source of truth The session: one object, every analyzer reads from it Cost Explorer wrappers: the most expensive API in AWS Bootstrap: let the spend tell you which regions matter The cost layer: trends, anomalies, commitments Compute: where Strands' model-driven approach earns its keep Storage: the single biggest hidden cost Databases and network: same shape, different problem Logs, KMS, secrets: small line items that compound Security tooling and governance: paying for what you don't have The synthesis tool: one PDF, one call, at the end Wiring the agent: the most underrated part of Strands CLI: same code, your account or somebody else's PDF renderer Fears Below is the example. The model: configuration as the single source of truth The first decision was where the agent's knobs live. I have seen too many AWS scripts where the threshold for "idle" is hardcoded somewhere on line 437, and the lookback window is hardcoded somewhere else, and you have to read the entire file to figure out what the script considers waste. So I started with a single CONFIG dict, fed by environment variables, and I pinned the small set of constants that the analyzers reference (the EOL Lambda runtimes, the Cost Explorer filter that strips out credits and refunds). The same code now runs in Lambda, in CI, or on my laptop — only the env vars change. from future import annotations import argparse import os import sys import traceback from datetime import date , datetime , timedelta , timezone from pathlib import Path from typing import Any , Callable import boto3 from botocore.exceptions import ClientError from strands import Agent , tool from strands.models.bedrock import BedrockModel import pdf_renderer CONFIG = { " model " : os . getenv ( " FINOPS_BEDROCK_MODEL " , " us.anthropic.claude-sonnet-4-5-20250929-v1:0 " ), " region " : os . getenv ( " AWS_REGION " , " us-east-1 " ), " output_dir " : Path ( os . getenv ( " FINOPS_OUTPUT_DIR " , str ( Path . home () / " Documents/finops-reports " ))), " lookback_months " : int ( os . getenv ( " FINOPS_LOOKBACK_MONTHS " , " 6 " )), " delta_usd " : float ( os . getenv ( " FINOPS_DELTA_USD " , " 5 " )), " delta_pct " : float ( os . getenv ( " FINOPS_DELTA_PCT " , " 25 " )), " stale_days " : int ( os . getenv ( " FINOPS_STALE_DAYS " , " 90 " )), " idle_days " : int ( os . getenv ( " FINOPS_IDLE_DAYS " , " 14 " )), " max_regions " : int ( os . getenv ( " FINOPS_MAX_REGIONS " , " 8 " )), " sns_topic_arn " : os . getenv ( " FINOPS_SNS_TOPIC_ARN " ), } NO_CREDITS = { " Not " : { " Dimensions " : { " Key " : " RECORD_TYPE " , " Values " : [ " Credit " , " Refund " ]}}} EOL_LAMBDA_RUNTIMES = { # Python " python2.7 " , " python3.6 " , " python3.7 " , " python3.8 " , " python3.9 " , # Node.js " nodejs4.3 " , " nodejs6.10 " , " nodejs8.10 " , " nodejs10.x " , " nodejs12.x " , " nodejs14.x " , " nodejs16.x " , " nodejs18.x " , # Ruby " ruby2.5 " , " ruby2.6 " , " ruby2.7 " , # Go (now use provided.al2/al2023) " go1.x " , # Java " java8 " , " java8.al2 " , " java11 " , # .NET " dotnetcore1.0 " , " dotnetcore2.0 " , " dotnetcore2.1 " , " dotnetcore3.1 " , " dotnet5.0 " , " dotnet6 " , " dotnet7 " , # Custom " provided " , } The session: one object, every analyzer reads from it If I follow the OOP analogy, the Session is my object model. It holds the boto3 session, the account identity, the active regions, and a lazy client cache so I do not pay the cost of constructing a new boto3 client for every region in every tool. The two globals ( SESSION and FINDINGS ) are deliberate — every @tool reads from s() and writes through record() , and that is the only state the agent shares. I tried passing the session through tool arguments first, but Strands tools are called by the model, not by my code, and I do not want the model to invent session arguments. The helpers below — try_aws , paginate , per_region , cw_stats — are the boring infrastructure that lets every analyzer survive a region with broken IAM and keep going instead of crashing the whole run. class Session : """ Bundles credentials, account identity, region list, and a client cache. """ def init ( self , boto : boto3 . Session , account_id : str , alias : str | None , primary_region : str , regions : list [ str ]): self . boto = boto self . account_id = account_id self . alias = alias self . primary_region = primary_region self . regions = regions self . _clients : dict [ tuple [ str , str ], Any ] = {} def client ( self , service : str , region : str | None = None ): key = ( service , region or self . primary_region ) if key not in self . _clients : self . _clients [ key ] = self . boto . client ( service , region_name = key [ 1 ]) return self . _clients [ key ] SESSION : Session | None = None FINDINGS : dict [ str , dict ] = {} def s () -> Session : if SESSION is None : raise RuntimeError ( " Session not initialized — call build_session() first " ) return SESSION def record ( section : str , data : dict ) -> dict : """ Store findings under section AND return them. Each tool ends with this. """ FINDINGS [ section ] = data return data def today () -> date : return datetime . now ( timezone . utc ). date () def money ( x ) -> float : try : return round ( float ( x or 0 ), 4 ) except ( TypeError , ValueError ): return 0.0 def try_aws ( fn : Callable , default ): """ Run fn(); on any AWS/runtime error log to stderr and return default. Lets tools keep going even if some regions/services lack permission. """ try : return fn () except Exception as e : print ( f " [finops-agent] { type ( e ). name } : { e } " , file = sys . stderr ) return default def paginate ( client , op : str , key : str , ** kw ) -> list : items : list = [] for page in client . get_paginator ( op ). paginate ( ** kw ): items . extend ( page . get ( key , [])) return items def per_region ( service : str , fn : Callable [[ str , Any ], dict ]) -> dict [ str , dict ]: """ Call fn(region, client_for_service) for each active region, swallow errors, return {region: result}. Tools that scan multi-region use this. """ out = {} for region in s (). regions : client = s (). client ( service , region ) out [ region ] = try_aws ( lambda c = client , r = region : fn ( r , c ), {}) return out def cw_stats ( client , namespace : str , metric : str , dims : list [ dict ], days : int , stat : str = " Average " , period : int = 86400 ) -> list [ float ]: end = datetime . now ( timezone . utc ) start = end - timedelta ( days = days ) r = try_aws ( lambda : client . get_metric_statistics ( Namespace = namespace , MetricName = metric , Dimensions = dims , StartTime = start , EndTime = end , Period = period , Statistics = [ stat ], ), { " Datapoints " : []}, ) return [ d [ stat ] for d in r . get ( " Datapoints " , [])] Cost Explorer wrappers: the most expensive API in AWS Cost Explorer is the most opinionated API in AWS — and one of the few where every call costs you money (0.01perrequest).EveryanalyzerthattouchesmoneygoesthroughthesetwohelperscegroupsforoneshotservicegroupedcostwindowsandcemonthlywithtopforthetrailingNmonthstrendwiththetopservicespermonth.IfIleteverytoolissueitsownrawgetcostandusagecalls,Iwouldbepayingfortheprivilegeoflookingatmyownwaste.IalsoalwaysstripoutcreditsandrefundswithNOCREDITS,otherwisethetrendchartshowsphantomdropseverytimeAWSappliesacreditandtheagentgetsconfusedaboutwhatthespendactuallylookslike.defcegroups(start:date,end:date,key:str,grouptype:str="DIMENSION")>dict[str,float]:"""RunaCEGetCostAndUsageandflattenServicegroupedresultstoservice:usd."""r=tryaws(lambda:s().client("ce").getcostandusage(TimePeriod="Start":start.isoformat(),"End":end.isoformat(),Granularity="MONTHLY",Metrics=["UnblendedCost"],GroupBy=["Type":grouptype,"Key":key],Filter=NOCREDITS,),"ResultsByTime":[],)out:dict[str,float]=fortpinr.get("ResultsByTime",[]):forgintp.get("Groups",[]):k=g["Keys"][0]out[k]=out.get(k,0.0)+money(g["Metrics"]["UnblendedCost"]["Amount"])returnoutdefcemonthlywithtop(months:int)>list[dict]:"""LastNmonthsgroupedbyservice,withtop8servicespermonth."""end=today()+timedelta(days=1)start=(today()timedelta(days=30months)).replace(day=1)r=tryaws(lambda:s().client("ce").getcostandusage(TimePeriod="Start":start.isoformat(),"End":end.isoformat(),Granularity="MONTHLY",Metrics=["UnblendedCost"],GroupBy=["Type":"DIMENSION","Key":"SERVICE"],Filter=NOCREDITS,),"ResultsByTime":[],)out=[]fortpinr.get("ResultsByTime",[]):services=sorted(("service":g["Keys"][0],"costusd":money(g["Metrics"]["UnblendedCost"]["Amount"])forgintp.get("Groups",[])),key=lambdax:x["costusd"],)out.append("month":tp["TimePeriod"]["Start"],"totalusd":round(sum(svc["costusd"]forsvcinservices),2),"top":services[:8],)returnoutBootstrap:letthespendtellyouwhichregionsmatterNowthebootstrap.ThisiswheretheagentfiguresoutwhichAWSaccountitiseventalkingtoandwhichregionsareworthscanning.ThetrickisdetectactiveregionsinsteadofscanningeveryAWSregion(28+atthetimeofwriting),IaskCostExplorerwhichregionshadnonzerospendinthelast30daysandonlyscanthose.Apersonaldevaccountendsupwithoneregion.Anenterpriseworkloadendsupwitheight.Theagentdoesnotneedtoknowinadvancewhichscenarioitisin;thedatatellsit.Thesamebuildsessionalsohandlesthecrossaccountcaseviarolearn,whichwastheoriginalusecaserunningtheagentagainstcustomeraccountswhereIhadauditaccess.defbuildsession(profile:strNone=None,rolearn:strNone=None,region:str=None,explicitregions:list[str]None=None)>Session:region=regionorCONFIG["region"]base=boto3.Session(profilename=profile)ifprofileelseboto3.Session()ifrolearn:creds=base.client("sts",regionname=region).assumerole(RoleArn=rolearn,RoleSessionName="finopsagent",)["Credentials"]sess=boto3.Session(awsaccesskeyid=creds["AccessKeyId"],awssecretaccesskey=creds["SecretAccessKey"],awssessiontoken=creds["SessionToken"],regionname=region,)else:sess=baseaccountid=sess.client("sts",regionname=region).getcalleridentity()["Account"]aliases=tryaws(lambda:sess.client("iam").listaccountaliases().get("AccountAliases",[]),[])alias=aliases[0]ifaliaseselseNoneregions=explicitregionsordetectactiveregions(sess,region)returnSession(sess,accountid,alias,region,regions[:CONFIG["maxregions"]])defdetectactiveregions(sess:boto3.Session,primary:str)>list[str]:"""UseCEtofindregionswith>0.01 per request). Every analyzer that touches money goes through these two helpers — _ce_groups for one-shot service-grouped cost windows and _ce_monthly_with_top for the trailing-N-months trend with the top services per month. If I let every tool issue its own raw get_cost_and_usage calls, I would be paying for the privilege of looking at my own waste. I also always strip out credits and refunds with NO_CREDITS , otherwise the trend chart shows phantom drops every time AWS applies a credit and the agent gets confused about what the spend actually looks like. def _ce_groups ( start : date , end : date , key : str , group_type : str = " DIMENSION " ) -> dict [ str , float ]: """ Run a CE GetCostAndUsage and flatten Service-grouped results to {service: usd}. """ r = try_aws ( lambda : s (). client ( " ce " ). get_cost_and_usage ( TimePeriod = { " Start " : start . isoformat (), " End " : end . isoformat ()}, Granularity = " MONTHLY " , Metrics = [ " UnblendedCost " ], GroupBy = [{ " Type " : group_type , " Key " : key }], Filter = NO_CREDITS , ), { " ResultsByTime " : []}, ) out : dict [ str , float ] = {} for tp in r . get ( " ResultsByTime " , []): for g in tp . get ( " Groups " , []): k = g [ " Keys " ][ 0 ] out [ k ] = out . get ( k , 0.0 ) + money ( g [ " Metrics " ][ " UnblendedCost " ][ " Amount " ]) return out def _ce_monthly_with_top ( months : int ) -> list [ dict ]: """ Last N months grouped by service, with top 8 services per month. """ end = today () + timedelta ( days = 1 ) start = ( today () - timedelta ( days = 30 * months )). replace ( day = 1 ) r = try_aws ( lambda : s (). client ( " ce " ). get_cost_and_usage ( TimePeriod = { " Start " : start . isoformat (), " End " : end . isoformat ()}, Granularity = " MONTHLY " , Metrics = [ " UnblendedCost " ], GroupBy = [{ " Type " : " DIMENSION " , " Key " : " SERVICE " }], Filter = NO_CREDITS , ), { " ResultsByTime " : []}, ) out = [] for tp in r . get ( " ResultsByTime " , []): services = sorted ( ({ " service " : g [ " Keys " ][ 0 ], " cost_usd " : money ( g [ " Metrics " ][ " UnblendedCost " ][ " Amount " ])} for g in tp . get ( " Groups " , [])), key = lambda x : - x [ " cost_usd " ], ) out . append ({ " month " : tp [ " TimePeriod " ][ " Start " ], " total_usd " : round ( sum ( svc [ " cost_usd " ] for svc in services ), 2 ), " top " : services [: 8 ], }) return out Bootstrap: let the spend tell you which regions matter Now the bootstrap. This is where the agent figures out which AWS account it is even talking to and which regions are worth scanning. The trick is _detect_active_regions — instead of scanning every AWS region (28+ at the time of writing), I ask Cost Explorer which regions had non-zero spend in the last 30 days and only scan those. A personal dev account ends up with one region. An enterprise workload ends up with eight. The agent does not need to know in advance which scenario it is in; the data tells it. The same build_session also handles the cross-account case via --role-arn , which was the original use case — running the agent against customer accounts where I had audit access. def build_session ( profile : str | None = None , role_arn : str | None = None , region : str = None , explicit_regions : list [ str ] | None = None ) -> Session : region = region or CONFIG [ " region " ] base = boto3 . Session ( profile_name = profile ) if profile else boto3 . Session () if role_arn : creds = base . client ( " sts " , region_name = region ). assume_role ( RoleArn = role_arn , RoleSessionName = " finops-agent " , )[ " Credentials " ] sess = boto3 . Session ( aws_access_key_id = creds [ " AccessKeyId " ], aws_secret_access_key = creds [ " SecretAccessKey " ], aws_session_token = creds [ " SessionToken " ], region_name = region , ) else : sess = base account_id = sess . client ( " sts " , region_name = region ). get_caller_identity ()[ " Account " ] aliases = try_aws ( lambda : sess . client ( " iam " ). list_account_aliases (). get ( " AccountAliases " , []), []) alias = aliases [ 0 ] if aliases else None regions = explicit_regions or _detect_active_regions ( sess , region ) return Session ( sess , account_id , alias , region , regions [: CONFIG [ " max_regions " ]]) def _detect_active_regions ( sess : boto3 . Session , primary : str ) -> list [ str ]: """ Use CE to find regions with >0 spend in the last 30 days. Falls back to [primary]. """ end = date . today () + timedelta ( days = 1 ) start = end - timedelta ( days = 31 ) r = try_aws ( lambda : sess . client ( " ce " , region_name = " us-east-1 " ). get_cost_and_usage ( TimePeriod = { " Start " : start . isoformat (), " End " : end . isoformat ()}, Granularity = " MONTHLY " , Metrics = [ " UnblendedCost " ], GroupBy = [{ " Type " : " DIMENSION " , " Key " : " REGION " }], Filter = NO_CREDITS , ), { " ResultsByTime " : []}, ) pairs : list [ tuple [ str , float ]] = [] for tp in r . get ( " ResultsByTime " , []): for g in tp . get ( " Groups " , []): rg , cost = g [ " Keys " ][ 0 ], money ( g [ " Metrics " ][ " UnblendedCost " ][ " Amount " ]) if cost > 0 and rg and rg not in ( " NoRegion " , " global " ): pairs . append (( rg , cost )) pairs . sort ( key = lambda x : - x [ 1 ]) out = [ r for r , _ in pairs ] if primary not in out : out . insert ( 0 , primary ) return out or [ primary ] The cost layer: trends, anomalies, commitments These are the first four tools the agent will call. discover_account always runs first — it anchors the account identity in the conversation and tells the agent "here is what you are looking at." Then analyze_cost_trends builds the spend profile and the 7-day-vs-prior-7-day deltas, which is where most "why is the bill suddenly higher?" questions are answered. analyze_anomalies_and_budgets checks whether the account even has Cost Anomaly Detection or Budgets configured — most accounts I have looked at do not, and that itself is a finding. analyze_commitments looks at Reserved Instance and Savings Plans coverage, but it includes a guardrail recommendation: skip RI/SP if compute spend is below $50/month. Telling a hobby account to buy a 1-year commitment is the kind of advice that makes people stop trusting the agent. # TOOLS @tool def discover_account () -> dict : """ First tool to call. Returns account identity, alias, organization status, and the regions that have non-zero spend. No arguments. """ sess = s () org = try_aws ( lambda : sess . client ( " organizations " ). describe_organization (). get ( " Organization " , {}), {}, ) return record ( " account " , { " account_id " : sess . account_id , " account_alias " : sess . alias , " primary_region " : sess . primary_region , " active_regions " : sess . regions , " is_payer_or_member " : bool ( org . get ( " Id " )), " master_account_id " : org . get ( " MasterAccountId " ), }) @tool def analyze_cost_trends () -> dict : """ Last N months by service + 7d-vs-prior-7d service deltas + current-month forecast. """ months = _ce_monthly_with_top ( CONFIG [ " lookback_months " ]) end = today () + timedelta ( days = 1 ) cur = _ce_groups ( today () - timedelta ( days = 7 ), end , " SERVICE " ) prior = _ce_groups ( today () - timedelta ( days = 14 ), today () - timedelta ( days = 7 ), " SERVICE " ) deltas = [] for svc in set ( cur ) | set ( prior ): c , p = cur . get ( svc , 0.0 ), prior . get ( svc , 0.0 ) delta = c - p pct = ( delta / p * 100 ) if p > 0 else ( 100.0 if c > 0 else 0.0 ) flagged = abs ( delta ) > CONFIG [ " delta_usd " ] or abs ( pct ) > CONFIG [ " delta_pct " ] if flagged or abs ( delta ) >= 0.5 : deltas . append ({ " service " : svc , " current_7d " : round ( c , 2 ), " prior_7d " : round ( p , 2 ), " delta_usd " : round ( delta , 2 ), " delta_pct " : round ( pct , 1 ), " flagged " : flagged , }) deltas . sort ( key = lambda x : - abs ( x [ " delta_usd " ])) forecast_resp = try_aws ( lambda : s (). client ( " ce " ). get_cost_forecast ( TimePeriod = { " Start " : today (). isoformat (), " End " : ( today (). replace ( day = 1 ) + timedelta ( days = 32 )). replace ( day = 1 ). isoformat (), }, Granularity = " MONTHLY " , Metric = " UNBLENDED_COST " , ), None , ) forecast = ( round ( money ( forecast_resp [ " Total " ][ " Amount " ]), 2 ) if forecast_resp and " Total " in forecast_resp else None ) return record ( " costs " , { " monthly " : months , " deltas_7d " : deltas [: 15 ], " forecast_current_month_usd " : forecast , " last_month_total_usd " : months [ - 1 ][ " total_usd " ] if months else 0 , " trailing_months " : CONFIG [ " lookback_months " ], }) @tool def analyze_anomalies_and_budgets () -> dict : """ Cost Anomaly Detection (last 90d) + all AWS Budgets state. """ sess = s () anom_resp = try_aws ( lambda : sess . client ( " ce " ). get_anomalies ( DateInterval = { " StartDate " : ( today () - timedelta ( days = 90 )). isoformat (), " EndDate " : today (). isoformat (), }, ), { " Anomalies " : []}, ) anomalies = [ { " id " : a . get ( " AnomalyId " ), " score " : a . get ( " AnomalyScore " , {}). get ( " CurrentScore " ), " impact_usd " : round ( money ( a . get ( " Impact " , {}). get ( " TotalImpact " , 0 )), 2 ), " service " : ( a . get ( " RootCauses " ) or [{}])[ 0 ]. get ( " Service " ), " start " : a . get ( " AnomalyStartDate " ), " end " : a . get ( " AnomalyEndDate " ), } for a in anom_resp . get ( " Anomalies " , []) ] monitor_count = len ( try_aws ( lambda : sess . client ( " ce " ). get_anomaly_monitors (). get ( " AnomalyMonitors " , []), [])) raw_budgets = try_aws ( lambda : sess . client ( " budgets " ). describe_budgets ( AccountId = sess . account_id ). get ( " Budgets " , []), [], ) budgets = [] for b in raw_budgets : actual = money ( b . get ( " CalculatedSpend " , {}). get ( " ActualSpend " , {}). get ( " Amount " , 0 )) forecast = money ( b . get ( " CalculatedSpend " , {}). get ( " ForecastedSpend " , {}). get ( " Amount " , 0 )) limit = money ( b [ " BudgetLimit " ][ " Amount " ]) budgets . append ({ " name " : b [ " BudgetName " ], " limit_usd " : limit , " actual_usd " : round ( actual , 2 ), " forecast_usd " : round ( forecast , 2 ), " exceeded_actual " : bool ( limit ) and actual > limit , " exceeded_forecast " : bool ( limit ) and forecast > limit , " time_unit " : b . get ( " TimeUnit " ), }) return record ( " anomalies_budgets " , { " anomalies " : anomalies , " anomaly_monitor_count " : monitor_count , " anomaly_detection_configured " : monitor_count > 0 , " budgets " : budgets , " budget_count " : len ( budgets ), }) @tool def analyze_commitments () -> dict : """ RI + Savings Plans coverage and OnDemand exposure for the last 30 days. """ sess = s () start , end = today () - timedelta ( days = 30 ), today () + timedelta ( days = 1 ) sp_cov = try_aws ( lambda : sess . client ( " ce " ). get_savings_plans_coverage ( TimePeriod = { " Start " : start . isoformat (), " End " : end . isoformat ()}, Granularity = " MONTHLY " , ), { " SavingsPlansCoverages " : []}, ) sp_summary = [ { " covered_usd " : money ( c . get ( " Coverage " , {}). get ( " SpendCoveredBySavingsPlans " , 0 )), " ondemand_usd " : money ( c . get ( " Coverage " , {}). get ( " OnDemandCost " , 0 )), " coverage_pct " : money ( c . get ( " Coverage " , {}). get ( " CoveragePercentage " , 0 )), } for c in sp_cov . get ( " SavingsPlansCoverages " , []) ] ri_cov = try_aws ( lambda : sess . client ( " ce " ). get_reservation_coverage ( TimePeriod = { " Start " : start . isoformat (), " End " : end . isoformat ()}, Granularity = " MONTHLY " , ), { " CoveragesByTime " : []}, ) ri_summary = [ { " metric " : k , " value " : v } for tp in ri_cov . get ( " CoveragesByTime " , []) for k , v in tp . get ( " Total " , {}). get ( " CoverageHours " , {}). items () ] ondemand = sum ( x [ " ondemand_usd " ] for x in sp_summary ) rec = ( " skip RI/SP —