AWS Budgets: Update alert thresholds unlimitedly with Lambda
shimo
Posted on December 27, 2022
Motivation
When I use AWS Budgets, I can receive notifications when the billing is above a threshold.
The facts are that:
- We can set only 10 alert thresholds for one budget.
- Two budgets are free but we are charged for using more budgets.
For personal use, usually, I'm not charged so much (around 10 USD per month). I want to be notified every 1 USD but there is a limitation of 10 alerts like above fact.
I this post, I share how to update the alert threshold incremently when AWS Budgets triggers.
Note from the pricing page:
Your first two action-enabled budgets are free (regardless of the number of actions you configure per budget) per month. Afterwards each subsequent action-enabled budget will incur a $0.10 daily cost.
Architecture
EventBridge triggers Lambda(setBudget) at the 1st of months. This Lambda delete and re-create the budget to initialize for a month and create 10 alert thresholds: 1, 2, 3, ... 10 USD thresholds, for example.
When the billing is above the threshold, the user is notified via SNS email. At the same time, the second Lambda function is triggered and updates the threshold. If the triggered threshold is 1 USD, the Lambda function update the threshold to 11 USD.
CloudFormation template
- Deploy from the Console. Create stack -> Upload a template -> Choose this file.
- Enter Stack name and parameters: budget name, email, increment.
- Leave the rest default. Choose Next, Next, check IAM acknowledge, and Submit.
- When starting in the middle of the month, manually run the
SetBudgetHandler
Lambda function. Note that you'll get notified of all alerts of the current used cost.
AWSTemplateFormatVersion: "2010-09-09"
Description: "Increment alert threshold of AWS Budgets"
Parameters:
budgetname:
Type: String
Default: increment-notification
increment:
Type: String
Default: 1
email:
Type: String
AllowedPattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"
ConstraintDescription: Must be a valid email address.
Resources:
MyTopic:
Type: AWS::SNS::Topic
MyTopicTokenSubscription:
Type: AWS::SNS::Subscription
Properties:
Protocol: email
TopicArn: !Ref MyTopic
Endpoint: !Ref email
MyTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
PolicyDocument:
Statement:
- Action: sns:Publish
Effect: Allow
Principal:
Service: budgets.amazonaws.com
Resource: !Ref MyTopic
Sid: "0"
Version: "2012-10-17"
Topics:
- !Ref MyTopic
UpdateBudgetHandlerServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
UpdateBudgetHandlerServiceRoleDefaultPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action: budgets:*
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: UpdateBudgetHandlerServiceRoleDefaultPolicy
Roles:
- !Ref UpdateBudgetHandlerServiceRole
UpdateBudgetHandler:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import os
import re
import boto3
budget_name = os.environ['BUDGET_NAME']
increment = os.environ['INCREMENT']
def handler(event, context):
print(event)
client = boto3.client('budgets')
account_id = context.invoked_function_arn.split(":")[4]
print("account_id", account_id)
message = event['Records'][0]['Sns']['Message']
print("message", message)
# catch 1 from "Alert Threshold: > $1.00"
match = re.search(r"Alert Threshold: > \$(\d{1,})\.00", message)
if not match:
print("No Budget Notification. Exit.")
return
current_value_str = match.groups()[0]
print("current_value", current_value_str)
current_value = int(current_value_str)
next_value = current_value + 10 * int(increment)
response = client.update_notification(
AccountId=account_id,
BudgetName=budget_name,
OldNotification={
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": current_value,
"ThresholdType": "ABSOLUTE_VALUE",
},
NewNotification={
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": next_value,
"ThresholdType": "ABSOLUTE_VALUE",
},
)
Role: !GetAtt UpdateBudgetHandlerServiceRole.Arn
Environment:
Variables:
BUDGET_NAME: !Ref budgetname
INCREMENT: !Ref increment
Handler: index.handler
Runtime: python3.9
Timeout: 10
DependsOn:
- UpdateBudgetHandlerServiceRoleDefaultPolicy
- UpdateBudgetHandlerServiceRole
UpdateBudgetHandlerEventInvokeConfig:
Type: AWS::Lambda::EventInvokeConfig
Properties:
FunctionName: !Ref UpdateBudgetHandler
Qualifier: $LATEST
MaximumRetryAttempts: 0
UpdateBudgetHandlerAllowInvokeStackMyTopic:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt UpdateBudgetHandler.Arn
Principal: sns.amazonaws.com
SourceArn: !Ref MyTopic
UpdateBudgetHandlerMyTopic:
Type: AWS::SNS::Subscription
Properties:
Protocol: lambda
TopicArn: !Ref MyTopic
Endpoint: !GetAtt UpdateBudgetHandler.Arn
SetBudgetHandlerServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
SetBudgetHandlerServiceRoleDefaultPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action: budgets:*
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: SetBudgetHandlerServiceRoleDefaultPolicy
Roles:
- !Ref SetBudgetHandlerServiceRole
SetBudgetHandler:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import os
import boto3
budget_name = os.environ['BUDGET_NAME']
sns_arn = os.environ['SNS_ARN']
increment = os.environ['INCREMENT']
client = boto3.client("budgets")
def handler(event, context):
account_id = context.invoked_function_arn.split(":")[4]
# Delete if the budget exists
try:
client.delete_budget(AccountId=account_id, BudgetName=budget_name)
print("Deleted the old budget.")
except client.exceptions.NotFoundException:
pass
# Create new budget
client.create_budget(
AccountId=account_id,
Budget={
"BudgetName": budget_name,
"BudgetLimit": {"Amount": "100.0", "Unit": "USD"},
"CostTypes": {
"IncludeTax": True,
"IncludeSubscription": True,
"UseBlended": False,
"IncludeRefund": False,
"IncludeCredit": False,
"IncludeUpfront": True,
"IncludeRecurring": True,
"IncludeOtherSubscription": True,
"IncludeSupport": True,
"IncludeDiscount": True,
"UseAmortized": False,
},
"TimeUnit": "MONTHLY",
"BudgetType": "COST",
},
NotificationsWithSubscribers=[
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": threshold,
"ThresholdType": "ABSOLUTE_VALUE",
"NotificationState": "OK",
},
"Subscribers": [
{"SubscriptionType": "SNS", "Address": sns_arn},
],
}
for threshold in range(1, 1 + 10 * int(increment), int(increment))
],
)
print("Created a new budget.")
Role: !GetAtt SetBudgetHandlerServiceRole.Arn
Environment:
Variables:
BUDGET_NAME: !Ref budgetname
SNS_ARN: !Ref MyTopic
INCREMENT: !Ref increment
Handler: index.handler
Runtime: python3.9
Timeout: 10
DependsOn:
- SetBudgetHandlerServiceRoleDefaultPolicy
- SetBudgetHandlerServiceRole
SetBudgetHandlerEventInvokeConfig:
Type: AWS::Lambda::EventInvokeConfig
Properties:
FunctionName: !Ref SetBudgetHandler
Qualifier: $LATEST
MaximumRetryAttempts: 0
ScheduleRule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: cron(0 0 1 * ? *)
State: ENABLED
Targets:
- Arn: !GetAtt SetBudgetHandler.Arn
Id: Target0
ScheduleRuleAllowEventRuleStackSetBudgetHandler:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt SetBudgetHandler.Arn
Principal: events.amazonaws.com
SourceArn: !GetAtt ScheduleRule.Arn
Summary
I've shared how to update AWS Budgets threshold incrementally.
Posted on December 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.