@tlj-blocks/billing-alarm-cdk
v0.1.0
Published
AWS CDK stack and construct that email you when your AWS bill crosses spend thresholds
Readme
@tlj-blocks/billing-alarm-cdk
CloudWatch alarms that email you when your AWS bill passes a spend threshold. The default thresholds are 10 USD and 100 USD.
There is one alarm per threshold, so crossing 10 USD sends an email and crossing 100 USD later in the same month sends a second one.
Install
npm install @tlj-blocks/billing-alarm-cdkaws-cdk-lib and constructs are peer dependencies. Your CDK app already has
them, but if not:
npm install aws-cdk-lib constructsRequires aws-cdk-lib 2.160.0 or later and constructs 10.x.
Quick start
// bin/app.ts
import { App } from "aws-cdk-lib";
import { BillingAlarmStack } from "@tlj-blocks/billing-alarm-cdk";
const app = new App();
new BillingAlarmStack(app, "BillingAlarms", {
notificationEmail: "[email protected]",
// thresholds defaults to [10, 100]
env: { account: process.env.CDK_DEFAULT_ACCOUNT },
});cdk deploy BillingAlarmsYou do not set a region. The stack pins itself to us-east-1, because that is
the only region where AWS publishes the billing metric. See
Why us-east-1.
Two steps you have to do by hand
CloudFormation cannot do either of these, and the alarms report no data until you finish the first one.
1. Turn on billing alerts
Open the billing preferences page, tick "Receive CloudWatch billing alerts", and save. Do this in the payer account if you use AWS Organizations.
Until you do, AWS never publishes the EstimatedCharges metric and every alarm
sits in INSUFFICIENT_DATA. After you switch it on, the first data point can
take up to 24 hours to appear.
2. Confirm the subscription email
The first deploy creates an SNS email subscription, and AWS sends a confirmation
link to notificationEmail. Click it. Until you do, SNS delivers nothing.
Using the construct in a stack you already have
Use BillingAlarm instead of BillingAlarmStack if you would rather not deploy
a separate stack. Put it in a us-east-1 stack yourself.
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { BillingAlarm } from "@tlj-blocks/billing-alarm-cdk";
export class OpsStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, { ...props, env: { ...props?.env, region: "us-east-1" } });
new BillingAlarm(this, "BillingAlarm", {
notificationEmail: "[email protected]",
thresholds: [10, 100, 500],
alarmNamePrefix: "personal",
});
}
}Props
BillingAlarm and BillingAlarmStack both take these. BillingAlarmStack also
takes the usual StackProps, except that it sets env.region itself.
| Prop | Type | Default | Notes |
| ------------------- | ------------- | ----------- | --------------------------------------------------------------- |
| notificationEmail | string | required | Who gets the email. Optional only if you pass topic. |
| thresholds | number[] | [10, 100] | One alarm each. Must be positive and must not repeat. |
| currency | string | "USD" | Must match your billing currency or no data arrives. |
| topic | sns.ITopic | a new topic | Notify a topic you already have. Ignores notificationEmail. |
| topicName | string | generated | Names the created topic. |
| alarmNamePrefix | string | "aws" | Alarms are named prefix-billing-over-threshold-currency. |
| period | Duration | 6 hours | Evaluation period. The metric updates a few times a day. |
What the construct exposes
topic is the sns.ITopic the alarms notify. Add subscribers to it for SMS or
a chatbot.
alarms is the array of cloudwatch.Alarm, one per threshold, in the order you
listed them.
BillingAlarmStack exposes its BillingAlarm as billingAlarm, so you can
reach both of the above from the stack.
To email a second address:
import { Subscription, SubscriptionProtocol } from "aws-cdk-lib/aws-sns";
const billing = new BillingAlarm(this, "BillingAlarm", {
notificationEmail: "[email protected]",
});
new Subscription(this, "Ops", {
topic: billing.topic,
protocol: SubscriptionProtocol.EMAIL,
endpoint: "[email protected]",
});How the alarms behave
AWS/Billing EstimatedCharges is a running total for the current month. An
alarm that fires on the 12th stays in ALARM until charges reset on the 1st,
then returns to OK and can fire again. You therefore get one email per
threshold per month rather than one every evaluation period.
The flip side is that spend climbing from 10 USD to 99 USD sends no further
email, because the 10 USD alarm is already in ALARM. Add more thresholds if
you want finer steps.
The number AWS reports is an estimate. It excludes credits and refunds that get applied later, so it will not match your final invoice.
Missing data counts as notBreaching, so the first days of a month and the
period before you switch on billing alerts do not raise false alarms.
Why us-east-1
AWS publishes billing metrics to us-east-1 only, whatever regions you actually
spend in. An alarm on AWS/Billing created anywhere else receives no data and
never fires. BillingAlarmStack sets the region for you and throws if you pass
a different one, rather than deploying a stack that cannot work.
Alarms or AWS Budgets
This package uses CloudWatch alarms. They are plain CloudFormation, and the first 10 alarms in an account are free.
AWS Budgets is the better fit if you want alerts on a forecast, such as "you are on track to exceed 100 USD", or budgets split by service or tag, or alerts on usage rather than cost. Nothing stops you running both.
Resources created
An AWS::SNS::Topic, an AWS::SNS::Subscription of protocol email, and one
AWS::CloudWatch::Alarm per threshold.
Troubleshooting
If the alarms stay in INSUFFICIENT_DATA, billing alerts are probably still
off. Check step 1 above, then confirm the metric exists:
aws cloudwatch list-metrics --namespace AWS/Billing --region us-east-1An empty result means AWS is not publishing it yet. Allow 24 hours after enabling the preference.
If an alarm fires but no email arrives, the SNS subscription is unconfirmed. Look in the inbox and the spam folder for the confirmation from AWS.
If real spend never trips an alarm, check that currency matches your billing
currency, and that you deployed into the payer account. Member account charges
in AWS Organizations roll up to the payer, not to the member.
