@@ -98,39 +98,41 @@ Resources:
9898 Properties :
9999 FunctionName : !Sub '${ResourcePrefix}${CFDataName}-Lambda'
100100 Description : !Sub "Lambda function to retrieve ${CFDataName}"
101- Runtime : python3.10
101+ Runtime : python3.12
102102 Architectures : [x86_64]
103103 Code :
104104 ZipFile : |
105- #Author Stephanie Gooch 2021
106- #Mohideen - Added Budgets tag collection module
105+ #Authors:
106+ # Stephanie Gooch - initial version
107+ # Mohideen - Added Budgets tag collection module
107108 import os
108109 import json
109110 import logging
110111 import datetime
111112 from json import JSONEncoder
112113 import sys
114+
115+ # update boto3 for list_tags_for_resource api
113116 from pip._internal import main
114117 main(['install', '-I', '-q', 'boto3', '--target', '/tmp/', '--no-cache-dir', '--disable-pip-version-check'])
115118 sys.path.insert(0,'/tmp/')
116119
117- import boto3
120+ import boto3 #pylint: disable=C0413
118121
119122 BUCKET = os.environ["BUCKET_NAME"]
120123 PREFIX = os.environ["PREFIX"]
121- ROLE_NAME = os.environ['ROLENAME ']
124+ ROLE_NAME = os.environ['ROLE_NAME ']
122125 TMP_FILE = "/tmp/data.json"
123- REGIONS = ["us-east-1"]
124126
125127 logger = logging.getLogger(__name__)
126128 logger.setLevel(getattr(logging, os.environ.get('LOG_LEVEL', 'INFO').upper(), logging.INFO))
127129
128- # subclass JSONEncoder
129130 class DateTimeEncoder(JSONEncoder):
130- # Override the default method
131- def default(self, obj):
132- if isinstance(obj, (datetime.date, datetime.datetime)):
133- return obj.isoformat()
131+ """encoder for json with time object"""
132+ def default(self, o):
133+ if isinstance(o, (datetime.date, datetime.datetime)):
134+ return o.isoformat()
135+ return None
134136
135137 def assume_role(account_id, service, region):
136138 cred = boto3.client('sts', region_name=region).assume_role(
@@ -144,63 +146,54 @@ Resources:
144146 aws_session_token=cred['SessionToken']
145147 )
146148
147- def lambda_handler(event, context):
149+ def lambda_handler(event, context): #pylint: disable=W0613
148150 logger.info(f"Event data {json.dumps(event)}")
149151 if 'account' not in event:
150152 raise ValueError(
151153 "Please do not trigger this Lambda manually."
152154 "Find the corresponding state machine in Step Functions and Trigger from there."
153155 )
154156 collection_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
155- try:
156- account = json.loads(event["account"])
157- account_id = account["account_id"]
158- account_name = account["account_name"]
159- payer_id = account["payer_id"]
160- logger.info(f"Collecting data for account: {account_id}")
161- budgets_client = assume_role(account_id, "budgets", REGIONS[0])
162- paginator = budgets_client.get_paginator("describe_budgets") #Paginator for a large list of accounts
163- response_iterator = paginator.paginate(AccountId=account_id)
164- count = 0
165- with open(TMP_FILE, "w") as f:
166- for budgets in response_iterator:
167- if not 'Budgets' in budgets: continue
168- for budget in budgets['Budgets']:
169- count += 1
170- budget['collection_time'] = collection_time
171- logger.debug(budget)
172- # Fetch tags for the budget using List tag for resource API
173- budget_name = budget['BudgetName']
174- resource_arn = f"arn:aws:budgets::{account_id}:budget/{budget_name}"
175- budget_tag = budgets_client.list_tags_for_resource(
176- ResourceARN=f"{resource_arn}"
177- )
178- if budget_tag['ResourceTags'] is not None:
179- budget.update({'Account_ID': account_id, 'Account_Name': account_name, 'Tags': budget_tag['ResourceTags']})
180- else:
181- budget.update({'Account_ID': account_id, 'Account_Name': account_name})
182- # Fetch CostFilters if available
183- if 'CostFilters' not in budget or len(budget['CostFilters']) == 0 or 'PlannedBudgetLimits' not in budget:
184- budget.update({'CostFilters': {'Filter': ['None']}})
185- dataJSONData = json.dumps(budget, cls=DateTimeEncoder)
186- f.write(dataJSONData)
187- f.write("\n")
188- logger.info(f"Budgets collected: {count}")
189- s3_upload(account_id, payer_id)
190- except Exception as e:
191- logger.warning(f"Error: {type(e)} {e}")
157+ aws_partition = boto3.session.Session().get_partition_for_region(boto3.session.Session().region_name)
158+ account = json.loads(event["account"])
159+ account_id = account["account_id"]
160+ account_name = account["account_name"]
161+ payer_id = account["payer_id"]
162+
163+ logger.info(f"Collecting data for account: {account_id}")
164+ budgets_client = assume_role(account_id, "budgets", "us-east-1") # must be us-east-1
165+ count = 0
166+ with open(TMP_FILE, "w", encoding='utf-8') as f:
167+ for budget in budgets_client.get_paginator("describe_budgets").paginate(AccountId=account_id).search('Budgets'):
168+ budget['collection_time'] = collection_time
169+
170+ # Fetch tags for the budget using List tag for resource API
171+ budget_name = budget['BudgetName']
172+ budget_tags = budgets_client.list_tags_for_resource(ResourceARN=f"arn:{aws_partition}:budgets::{account_id}:budget/{budget_name}")
173+ budget.update({
174+ 'Account_ID': account_id,
175+ 'Account_Name': account_name,
176+ 'Tags': budget_tags.get('ResourceTags') or []
177+ })
178+
179+ # Fetch CostFilters if available
180+ if 'CostFilters' not in budget or len(budget['CostFilters']) == 0 or 'PlannedBudgetLimits' not in budget:
181+ budget.update({'CostFilters': {'Filter': ['None']}})
182+
183+ f.write(json.dumps(budget, cls=DateTimeEncoder) + "\n")
184+ count += 1
185+ logger.info(f"Budgets collected: {count}")
186+ s3_upload(account_id, payer_id)
187+
192188
193189 def s3_upload(account_id, payer_id):
194190 if os.path.getsize(TMP_FILE) == 0:
195191 logger.info(f"No data in file for {PREFIX}")
196192 return
197193 key = datetime.datetime.now().strftime(f"{PREFIX}/{PREFIX}-data/payer_id={payer_id}/year=%Y/month=%m/budgets-{account_id}.json")
198- try:
199- res = boto3.client('s3').upload_file(TMP_FILE, BUCKET, key)
200- logger.info(f'res={res}')
201- logger.info(f"Budget data for {account_id} stored at s3://{BUCKET}/{key}")
202- except Exception as exc:
203- logger.warning(exc)
194+ boto3.client('s3').upload_file(TMP_FILE, BUCKET, key)
195+ logger.info(f"Budget data for {account_id} stored at s3://{BUCKET}/{key}")
196+
204197 Handler : ' index.lambda_handler'
205198 MemorySize : 2688
206199 Timeout : 300
@@ -209,7 +202,7 @@ Resources:
209202 Variables :
210203 BUCKET_NAME : !Ref DestinationBucket
211204 PREFIX : !Ref CFDataName
212- ROLENAME : !Ref MultiAccountRoleName
205+ ROLE_NAME : !Ref MultiAccountRoleName
213206
214207 Metadata :
215208 cfn_nag :
0 commit comments