AWS Budgets: Update alert thresholds unlimitedly with Lambda
Tags: aws, budget, lambda, cloudformation
Published: 2022-12-27 23:21:29



## Motivation

When I use AWS Budgets, I can receive notifications when the billing is above a threshold. 

The facts are that:
1. We can set only 10 alert thresholds for one budget.
2. [Two budgets are free](https://aws.amazon.com/jp/aws-cost-management/aws-budgets/pricing/) 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

![image of Budgets threshold increment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dsachszs5e9zgm0bscn0.png)

1. 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.

2. 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.

```yml
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.