@composurecdk/budgets 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @composurecdk/budgets
2
+
3
+ AWS Budgets builder for [ComposureCDK](../../README.md).
4
+
5
+ This package provides a fluent builder for `AWS::Budgets::Budget` with well-architected defaults, percentage-threshold notification helpers, and automatic `AWS::SNS::TopicPolicy` wiring for SNS subscribers. It wraps the CDK L1 [CfnBudget](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_budgets.CfnBudget.html) construct — there is no L2 for Budgets.
6
+
7
+ ## Budget Builder
8
+
9
+ ```ts
10
+ import { createBudgetBuilder } from "@composurecdk/budgets";
11
+
12
+ const budget = createBudgetBuilder()
13
+ .budgetName("AgentBudget")
14
+ .limit({ amount: 50, unit: "GBP" })
15
+ .notifyOnActual(100, "ops@example.com")
16
+ .build(stack, "AgentBudget");
17
+ ```
18
+
19
+ ### Properties
20
+
21
+ Every field on [CfnBudget.BudgetDataProperty](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_budgets.CfnBudget.BudgetDataProperty.html) that tends to be set by hand is surfaced as a fluent setter:
22
+
23
+ | Setter | Purpose |
24
+ | ------------------- | --------------------------------------------------------------------- |
25
+ | `budgetName(name)` | `BudgetName` — stable identifier in the console and across regions. |
26
+ | `budgetType(type)` | `BudgetType` — `COST`, `USAGE`, `RI_UTILIZATION`, `RI_COVERAGE`, etc. |
27
+ | `timeUnit(unit)` | `TimeUnit` — `DAILY`, `MONTHLY`, `QUARTERLY`, `ANNUALLY`. |
28
+ | `limit({ amount })` | `BudgetLimit` — required for `COST` and `USAGE` budgets. |
29
+ | `costFilters(map)` | `CostFilters` — e.g. `{ Service: ["AmazonEC2"] }`. |
30
+ | `costTypes(types)` | `CostTypes` passthrough. |
31
+
32
+ ### Notifications
33
+
34
+ Percentage-threshold helpers cover the common case; `addNotification` accepts the raw shape when you need absolute-value thresholds or a different comparison operator.
35
+
36
+ ```ts
37
+ createBudgetBuilder()
38
+ .limit({ amount: 100 })
39
+ .notifyOnActual(80, "ops@example.com") // 80% ACTUAL → email
40
+ .notifyOnForecasted(
41
+ 100,
42
+ ref("alerts", (r) => r.topic),
43
+ ) // 100% FORECASTED → SNS topic
44
+ .addNotification({
45
+ notificationType: "ACTUAL",
46
+ threshold: 120,
47
+ thresholdType: "ABSOLUTE_VALUE",
48
+ subscribers: ["oncall@example.com"],
49
+ });
50
+ ```
51
+
52
+ Subscribers may be email strings, `ITopic` instances, or `Resolvable<ITopic>` references to topics owned by sibling components.
53
+
54
+ ### Recommended Thresholds
55
+
56
+ ```ts
57
+ createBudgetBuilder().limit({ amount: 50 }).withRecommendedThresholds("ops@example.com");
58
+ ```
59
+
60
+ Applies the AWS Cost Optimization pillar defaults: `ACTUAL` at 80% and `FORECASTED` at 100%.
61
+
62
+ ## Defaults
63
+
64
+ | Property | Default | Rationale |
65
+ | ----------------------------------------- | ----------- | ----------------------------------------------------------------------------- |
66
+ | `budgetType` | `"COST"` | Cost budgets are the most common; usage/RI/SP budgets are explicit overrides. |
67
+ | `timeUnit` | `"MONTHLY"` | Aligns with AWS billing cycles. |
68
+ | `limitUnit` | `"USD"` | Matches the AWS Billing console default. |
69
+ | `recommendedThresholds.actualPercent` | `80` | Early-warning threshold before breach. |
70
+ | `recommendedThresholds.forecastedPercent` | `100` | Trending-over-budget alert for the period. |
71
+
72
+ Exported as `BUDGET_DEFAULTS`.
73
+
74
+ ## Automatic SNS Topic Policies
75
+
76
+ When at least one notification subscriber is an SNS topic, the builder creates a matching `AWS::SNS::TopicPolicy` granting `SNS:Publish` to the `budgets.amazonaws.com` service principal. Without that policy, budget notifications to SNS silently fail to deliver — one of the most common footguns when wiring Budgets by hand.
77
+
78
+ The created `TopicPolicy` constructs are returned on `result.topicPolicies`, keyed by the topic's fully-qualified node path (unique within the CDK app).
79
+
80
+ ## Recommended Alarms
81
+
82
+ AWS Budgets does not publish per-budget CloudWatch metrics, but the well-architected cost-monitoring pattern combines a budget with a CloudWatch alarm on `AWS/Billing EstimatedCharges`. The builder can create that alarm for you, but it is **off by default** — pass an `estimatedCharges` config to opt in.
83
+
84
+ | Alarm | Metric | Default behaviour |
85
+ | ------------------ | ----------------------------------- | ----------------- |
86
+ | `estimatedCharges` | EstimatedCharges (Maximum, 6 hours) | off |
87
+
88
+ `treatMissingData` defaults to `notBreaching`: missing datapoints from a quiet account are not treated as a breach.
89
+
90
+ ```ts
91
+ const stack = new Stack(app, "BillingStack", { env: { region: "us-east-1" } });
92
+
93
+ createBudgetBuilder()
94
+ .limit({ amount: 50 })
95
+ .recommendedAlarms({
96
+ estimatedCharges: { threshold: 50, currency: "USD" },
97
+ })
98
+ .build(stack, "AccountBudget");
99
+ ```
100
+
101
+ The Budget itself is a global service and can be created from any region; only the alarm requires `us-east-1` (see below).
102
+
103
+ ### Customising thresholds
104
+
105
+ ```ts
106
+ createBudgetBuilder()
107
+ .limit({ amount: 1000 })
108
+ .recommendedAlarms({
109
+ estimatedCharges: {
110
+ threshold: 1000,
111
+ currency: "USD",
112
+ evaluationPeriods: 2,
113
+ datapointsToAlarm: 2,
114
+ },
115
+ });
116
+ ```
117
+
118
+ ### Disabling alarms
119
+
120
+ Disable the recommended alarm with `recommendedAlarms({ estimatedCharges: false })`, or disable all recommended alarms with `recommendedAlarms(false)`. Custom alarms attached via `addAlarm` are unaffected by either form.
121
+
122
+ ### Custom alarms
123
+
124
+ ```ts
125
+ import { Metric } from "aws-cdk-lib/aws-cloudwatch";
126
+
127
+ createBudgetBuilder()
128
+ .limit({ amount: 1000 })
129
+ .addAlarm("ec2EstimatedCharges", (a) =>
130
+ a
131
+ .metric(
132
+ () =>
133
+ new Metric({
134
+ namespace: "AWS/Billing",
135
+ metricName: "EstimatedCharges",
136
+ dimensionsMap: { Currency: "USD", ServiceName: "AmazonEC2" },
137
+ statistic: "Maximum",
138
+ }),
139
+ )
140
+ .threshold(500)
141
+ .greaterThan()
142
+ .description("EC2 estimated charges exceeded $500."),
143
+ );
144
+ ```
145
+
146
+ ### Applying alarm actions
147
+
148
+ No alarm actions are configured by default. Wire SNS or other actions via [`alarmActionsPolicy`](../cloudwatch/README.md#alarm-actions-policy) (or an `afterBuild` hook) — for cross-region deployments, the policy applied to the `us-east-1` monitoring stack covers both recommended and custom alarms.
149
+
150
+ ### Cross-region: `AWS/Billing EstimatedCharges` lives in `us-east-1` only
151
+
152
+ The `AWS/Billing EstimatedCharges` metric is emitted in `us-east-1` only, regardless of where your budgets and resources live. CloudWatch alarms are regional, so an alarm in any other region will never receive data. The combined builder emits a synth-time warning (`@composurecdk/budgets:alarm-region`) when used outside `us-east-1`, but the better approach is to route the alarm into a `us-east-1` stack via `createBudgetAlarmBuilder` and `compose().withStacks()`:
153
+
154
+ ```ts
155
+ import { compose, ref } from "@composurecdk/core";
156
+ import {
157
+ createBudgetBuilder,
158
+ createBudgetAlarmBuilder,
159
+ type BudgetBuilderResult,
160
+ } from "@composurecdk/budgets";
161
+
162
+ compose(
163
+ {
164
+ account: createBudgetBuilder()
165
+ .budgetName("Account")
166
+ .limit({ amount: 1000 })
167
+ .recommendedAlarms(false), // suppress alarms in the budget's own stack
168
+
169
+ accountAlarms: createBudgetAlarmBuilder()
170
+ .budget(ref<BudgetBuilderResult>("account"))
171
+ .recommendedAlarms({ estimatedCharges: { threshold: 1000, currency: "USD" } }),
172
+ },
173
+ { account: [], accountAlarms: ["account"] },
174
+ )
175
+ .withStacks({
176
+ account: appStack, // any region — AWS::Budgets::Budget is global
177
+ accountAlarms: monitoringStack, // us-east-1 — where AWS/Billing metrics live
178
+ })
179
+ .build(app, "App");
180
+ ```
181
+
182
+ If your custom `addAlarm` definitions reference the budget construct, set `crossRegionReferences: true` on both stacks so CDK can export the budget's properties from the app stack and import them in the alarm stack. The same pattern is documented for CloudFront and Route 53 alarms, and codified in [ADR-0004](../../docs/adr/0004-split-alarm-builder-for-fixed-region-metrics.md).
183
+
184
+ ## Build Result
185
+
186
+ ```ts
187
+ interface BudgetBuilderResult {
188
+ budget: CfnBudget;
189
+ topicPolicies: Record<string, TopicPolicy>;
190
+ alarms: Record<string, Alarm>;
191
+ }
192
+ ```
@@ -0,0 +1,61 @@
1
+ import type { AlarmConfig } from "@composurecdk/cloudwatch";
2
+ /**
3
+ * Extended {@link AlarmConfig} for the account-level `EstimatedCharges`
4
+ * billing alarm.
5
+ *
6
+ * The `EstimatedCharges` metric lives in the `AWS/Billing` namespace and
7
+ * is **only emitted in the `us-east-1` region**, regardless of where
8
+ * your resources run. The builder emits a synth-time warning when the
9
+ * surrounding stack is not in `us-east-1`; for non-`us-east-1` stacks,
10
+ * suppress this alarm with `recommendedAlarms: false` and create the
11
+ * alarm in a `us-east-1` stack via `createBudgetAlarmBuilder` (see
12
+ * ADR-0004).
13
+ *
14
+ * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html
15
+ */
16
+ export interface EstimatedChargesAlarmConfig extends AlarmConfig {
17
+ /**
18
+ * The absolute cost threshold (in `currency`) at which the alarm fires.
19
+ * This is a hard monetary value, **not** a percentage of the budget.
20
+ */
21
+ threshold: number;
22
+ /**
23
+ * ISO 4217 currency code that the `EstimatedCharges` metric is
24
+ * emitted in. AWS emits the metric with a `Currency` dimension that
25
+ * must match your billing currency.
26
+ *
27
+ * @default "USD"
28
+ */
29
+ currency?: string;
30
+ }
31
+ /**
32
+ * Controls which recommended CloudWatch alarms are created for an AWS
33
+ * Budget.
34
+ *
35
+ * AWS Budgets itself does not publish per-budget CloudWatch metrics.
36
+ * The recommended-alarm surface therefore mirrors the well-architected
37
+ * cost-monitoring pattern: a CloudWatch alarm on the account-level
38
+ * `AWS/Billing EstimatedCharges` metric that fires when total estimated
39
+ * charges cross a hard threshold.
40
+ *
41
+ * Off by default — the alarm only emits data when its stack is deployed
42
+ * to `us-east-1`, so callers must opt in explicitly.
43
+ *
44
+ * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html
45
+ */
46
+ export interface BudgetAlarmConfig {
47
+ /**
48
+ * Master switch.
49
+ * @default false
50
+ */
51
+ enabled?: boolean;
52
+ /**
53
+ * Configuration for the `EstimatedCharges` billing alarm.
54
+ *
55
+ * Pass `false` to disable, or supply an
56
+ * {@link EstimatedChargesAlarmConfig} to enable with a specific
57
+ * monetary threshold.
58
+ */
59
+ estimatedCharges?: EstimatedChargesAlarmConfig | false;
60
+ }
61
+ //# sourceMappingURL=alarm-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-config.d.ts","sourceRoot":"","sources":["../src/alarm-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,2BAA4B,SAAQ,WAAW;IAC9D;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;CACxD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=alarm-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-config.js","sourceRoot":"","sources":["../src/alarm-config.ts"],"names":[],"mappings":""}
@@ -0,0 +1,147 @@
1
+ import { type CfnBudget } from "aws-cdk-lib/aws-budgets";
2
+ import { type Alarm } from "aws-cdk-lib/aws-cloudwatch";
3
+ import { type IConstruct } from "constructs";
4
+ import { type IBuilder, type Lifecycle, type Resolvable } from "@composurecdk/core";
5
+ import { AlarmDefinitionBuilder } from "@composurecdk/cloudwatch";
6
+ import type { BudgetAlarmConfig } from "./alarm-config.js";
7
+ import type { BudgetBuilderResult } from "./budget-builder.js";
8
+ /**
9
+ * Configuration properties for {@link createBudgetAlarmBuilder}.
10
+ *
11
+ * The standalone alarm builder mirrors the alarm surface that
12
+ * {@link createBudgetBuilder} creates by default. It exists so that
13
+ * billing alarms can be created in a different stack from the budget
14
+ * itself — specifically a `us-east-1` stack, since the
15
+ * `AWS/Billing EstimatedCharges` metric is only emitted in `us-east-1`
16
+ * regardless of where the budget is deployed.
17
+ *
18
+ * @see ADR-0004 — Split-alarm builder pattern for fixed-region metrics
19
+ */
20
+ export interface BudgetAlarmBuilderProps {
21
+ /**
22
+ * Configuration for AWS-recommended CloudWatch alarms.
23
+ *
24
+ * Mirrors {@link BudgetBuilderProps.recommendedAlarms}. Off by default —
25
+ * pass an {@link BudgetAlarmConfig.estimatedCharges} entry to enable
26
+ * the account-level billing alarm. Set to `false` to suppress recommended
27
+ * alarms entirely; custom alarms added via
28
+ * {@link IBudgetAlarmBuilder.addAlarm} are unaffected.
29
+ *
30
+ * No alarm actions are configured by default. Use `alarmActionsPolicy`
31
+ * (or an `afterBuild` hook) to wire SNS or other actions onto the
32
+ * resulting alarms.
33
+ *
34
+ * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html
35
+ */
36
+ recommendedAlarms?: BudgetAlarmConfig | false;
37
+ }
38
+ /**
39
+ * The build output of an {@link IBudgetAlarmBuilder}.
40
+ */
41
+ export interface BudgetAlarmBuilderResult {
42
+ /**
43
+ * The CloudWatch alarms created by this builder, keyed by alarm name.
44
+ * Uses the same key scheme as {@link BudgetBuilderResult.alarms}.
45
+ */
46
+ alarms: Record<string, Alarm>;
47
+ }
48
+ /**
49
+ * A fluent builder for budget-related CloudWatch alarms, decoupled from
50
+ * the budget itself. Use this when the budget lives in a stack outside
51
+ * `us-east-1` — route this builder's component into a `us-east-1` stack
52
+ * via `compose().withStacks()` so the alarms land where the
53
+ * `AWS/Billing EstimatedCharges` metric actually emits.
54
+ *
55
+ * @see {@link createBudgetAlarmBuilder}
56
+ */
57
+ export type IBudgetAlarmBuilder = IBuilder<BudgetAlarmBuilderProps, BudgetAlarmBuilder>;
58
+ /**
59
+ * Shared alarm-assembly used by both {@link createBudgetBuilder} (in its
60
+ * own stack) and {@link createBudgetAlarmBuilder} (typically in a separate
61
+ * `us-east-1` stack). Materialises the recommended billing alarm and any
62
+ * user-supplied custom alarms, emits the region warning if the resulting
63
+ * scope is not in `us-east-1`, and creates the alarm constructs.
64
+ *
65
+ * The `target.budget` reference is only needed for custom alarms added
66
+ * via `addAlarm()` — the recommended `EstimatedCharges` alarm is
67
+ * account-level and does not key off the budget itself, so `target` may
68
+ * be omitted when only the recommended alarm is being created.
69
+ *
70
+ * @internal
71
+ */
72
+ export declare function buildBudgetAlarms(scope: IConstruct, id: string, target: Pick<BudgetBuilderResult, "budget"> | undefined, options?: {
73
+ recommendedAlarms?: BudgetAlarmConfig | false;
74
+ customAlarms?: AlarmDefinitionBuilder<CfnBudget>[];
75
+ }): Record<string, Alarm>;
76
+ declare class BudgetAlarmBuilder implements Lifecycle<BudgetAlarmBuilderResult> {
77
+ #private;
78
+ props: Partial<BudgetAlarmBuilderProps>;
79
+ /**
80
+ * Sets the budget to alarm on. Pass the result of
81
+ * {@link createBudgetBuilder} (or a {@link Ref} to it). The builder
82
+ * reads the underlying `CfnBudget` from the result so custom alarms
83
+ * added via {@link addAlarm} can reference it.
84
+ *
85
+ * Optional when only the recommended `EstimatedCharges` alarm is being
86
+ * created — that alarm is account-level and does not reference any
87
+ * specific budget. Required as soon as you call {@link addAlarm}.
88
+ *
89
+ * Pair with `compose().withStacks()` to route this component into a
90
+ * `us-east-1` stack while the budget itself lives elsewhere — set
91
+ * `crossRegionReferences: true` on both stacks so CDK can wire any
92
+ * cross-stack references automatically.
93
+ */
94
+ budget(budget: Resolvable<BudgetBuilderResult>): this;
95
+ /**
96
+ * Adds a custom alarm against the budget. The configure callback
97
+ * receives a fresh {@link AlarmDefinitionBuilder} pre-set with the
98
+ * alarm's key; configure metric, threshold, comparison and any other
99
+ * options.
100
+ *
101
+ * The created alarm is materialised in this builder's stack — useful
102
+ * for cross-region setups where you want all billing-related alarms to
103
+ * live with the recommended one.
104
+ */
105
+ addAlarm(key: string, configure: (alarm: AlarmDefinitionBuilder<CfnBudget>) => AlarmDefinitionBuilder<CfnBudget>): this;
106
+ build(scope: IConstruct, id: string, context?: Record<string, object>): BudgetAlarmBuilderResult;
107
+ }
108
+ /**
109
+ * Creates a new {@link IBudgetAlarmBuilder} for materialising AWS Budget
110
+ * alarms in a stack separate from the budget itself.
111
+ *
112
+ * The recommended use is multi-region deployments: the budget lives in
113
+ * the application's stack (in any region — `AWS::Budgets::Budget` is a
114
+ * global resource), and the alarms must live in a `us-east-1` stack so
115
+ * they can read the `AWS/Billing EstimatedCharges` metric AWS emits
116
+ * there.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * compose(
121
+ * {
122
+ * account: createBudgetBuilder()
123
+ * .budgetName("Account")
124
+ * .limit({ amount: 1000 })
125
+ * .recommendedAlarms(false), // suppress alarms in the budget's own stack
126
+ *
127
+ * accountAlarms: createBudgetAlarmBuilder()
128
+ * .budget(ref<BudgetBuilderResult>("account"))
129
+ * .recommendedAlarms({
130
+ * estimatedCharges: { threshold: 1000, currency: "USD" },
131
+ * }),
132
+ * },
133
+ * { account: [], accountAlarms: ["account"] },
134
+ * )
135
+ * .withStacks({
136
+ * account: appStack, // any region — Budgets is a global service
137
+ * accountAlarms: monitoringStack, // us-east-1 — where AWS/Billing metrics live
138
+ * })
139
+ * .build(app, "App");
140
+ * ```
141
+ *
142
+ * Set `crossRegionReferences: true` on both stacks if you reference the
143
+ * budget from custom alarms via `.addAlarm()`.
144
+ */
145
+ export declare function createBudgetAlarmBuilder(): IBudgetAlarmBuilder;
146
+ export {};
147
+ //# sourceMappingURL=budget-alarm-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-alarm-builder.d.ts","sourceRoot":"","sources":["../src/budget-alarm-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,4BAA4B,CAAC;AAExD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,SAAS,EAEd,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,sBAAsB,EAAgB,MAAM,0BAA0B,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;;;;;;;OAcG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC,uBAAuB,EAAE,kBAAkB,CAAC,CAAC;AAsBxF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,QAAQ,CAAC,GAAG,SAAS,EACvD,OAAO,GAAE;IACP,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAC;IAC9C,YAAY,CAAC,EAAE,sBAAsB,CAAC,SAAS,CAAC,EAAE,CAAC;CAC/C,GACL,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAoBvB;AAED,cAAM,kBAAmB,YAAW,SAAS,CAAC,wBAAwB,CAAC;;IACrE,KAAK,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAM;IAI7C;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,mBAAmB,CAAC,GAAG,IAAI;IAKrD;;;;;;;;;OASG;IACH,QAAQ,CACN,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,CAAC,KAAK,EAAE,sBAAsB,CAAC,SAAS,CAAC,KAAK,sBAAsB,CAAC,SAAS,CAAC,GACzF,IAAI;IAKP,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,wBAAwB;CASjG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,wBAAwB,IAAI,mBAAmB,CAE9D"}
@@ -0,0 +1,139 @@
1
+ import { Annotations, Stack, Token } from "aws-cdk-lib";
2
+ import { Builder, resolve, } from "@composurecdk/core";
3
+ import { AlarmDefinitionBuilder, createAlarms } from "@composurecdk/cloudwatch";
4
+ import { resolveBudgetAlarmDefinitions } from "./budget-alarms.js";
5
+ /**
6
+ * The `AWS/Billing EstimatedCharges` metric is emitted in `us-east-1`
7
+ * only. CloudWatch alarms are regional, so alarms created in any other
8
+ * region will never receive data. Warn (don't error) when alarms are
9
+ * being created outside `us-east-1`, unless the region is an unresolved
10
+ * token (env-agnostic stack — user knows best).
11
+ */
12
+ function warnIfNotUsEast1(scope) {
13
+ const region = Stack.of(scope).region;
14
+ if (Token.isUnresolved(region))
15
+ return;
16
+ if (region === "us-east-1")
17
+ return;
18
+ Annotations.of(scope).addWarningV2("@composurecdk/budgets:alarm-region", `AWS/Billing EstimatedCharges is emitted in us-east-1 only, but this stack is ` +
19
+ `deployed in "${region}". CloudWatch alarms created here will not fire. Deploy the ` +
20
+ `stack in us-east-1, or use createBudgetAlarmBuilder() routed to a ` +
21
+ `us-east-1 stack via compose().withStacks().`);
22
+ }
23
+ /**
24
+ * Shared alarm-assembly used by both {@link createBudgetBuilder} (in its
25
+ * own stack) and {@link createBudgetAlarmBuilder} (typically in a separate
26
+ * `us-east-1` stack). Materialises the recommended billing alarm and any
27
+ * user-supplied custom alarms, emits the region warning if the resulting
28
+ * scope is not in `us-east-1`, and creates the alarm constructs.
29
+ *
30
+ * The `target.budget` reference is only needed for custom alarms added
31
+ * via `addAlarm()` — the recommended `EstimatedCharges` alarm is
32
+ * account-level and does not key off the budget itself, so `target` may
33
+ * be omitted when only the recommended alarm is being created.
34
+ *
35
+ * @internal
36
+ */
37
+ export function buildBudgetAlarms(scope, id, target, options = {}) {
38
+ const recommended = options.recommendedAlarms;
39
+ const recommendedDefs = recommended === false ? [] : resolveBudgetAlarmDefinitions(recommended);
40
+ const customAlarms = options.customAlarms ?? [];
41
+ if (customAlarms.length > 0 && !target) {
42
+ throw new Error(`BudgetAlarmBuilder "${id}" was given addAlarm() definitions but no budget. ` +
43
+ `Call .budget() with a BudgetBuilderResult or a Ref to one before adding custom alarms.`);
44
+ }
45
+ const customAlarmDefs = target ? customAlarms.map((b) => b.resolve(target.budget)) : [];
46
+ const allAlarmDefs = [...recommendedDefs, ...customAlarmDefs];
47
+ if (allAlarmDefs.length > 0) {
48
+ warnIfNotUsEast1(scope);
49
+ }
50
+ return createAlarms(scope, id, allAlarmDefs);
51
+ }
52
+ class BudgetAlarmBuilder {
53
+ props = {};
54
+ #budget;
55
+ #customAlarms = [];
56
+ /**
57
+ * Sets the budget to alarm on. Pass the result of
58
+ * {@link createBudgetBuilder} (or a {@link Ref} to it). The builder
59
+ * reads the underlying `CfnBudget` from the result so custom alarms
60
+ * added via {@link addAlarm} can reference it.
61
+ *
62
+ * Optional when only the recommended `EstimatedCharges` alarm is being
63
+ * created — that alarm is account-level and does not reference any
64
+ * specific budget. Required as soon as you call {@link addAlarm}.
65
+ *
66
+ * Pair with `compose().withStacks()` to route this component into a
67
+ * `us-east-1` stack while the budget itself lives elsewhere — set
68
+ * `crossRegionReferences: true` on both stacks so CDK can wire any
69
+ * cross-stack references automatically.
70
+ */
71
+ budget(budget) {
72
+ this.#budget = budget;
73
+ return this;
74
+ }
75
+ /**
76
+ * Adds a custom alarm against the budget. The configure callback
77
+ * receives a fresh {@link AlarmDefinitionBuilder} pre-set with the
78
+ * alarm's key; configure metric, threshold, comparison and any other
79
+ * options.
80
+ *
81
+ * The created alarm is materialised in this builder's stack — useful
82
+ * for cross-region setups where you want all billing-related alarms to
83
+ * live with the recommended one.
84
+ */
85
+ addAlarm(key, configure) {
86
+ this.#customAlarms.push(configure(new AlarmDefinitionBuilder(key)));
87
+ return this;
88
+ }
89
+ build(scope, id, context) {
90
+ const target = this.#budget ? resolve(this.#budget, context ?? {}) : undefined;
91
+ return {
92
+ alarms: buildBudgetAlarms(scope, id, target, {
93
+ recommendedAlarms: this.props.recommendedAlarms,
94
+ customAlarms: this.#customAlarms,
95
+ }),
96
+ };
97
+ }
98
+ }
99
+ /**
100
+ * Creates a new {@link IBudgetAlarmBuilder} for materialising AWS Budget
101
+ * alarms in a stack separate from the budget itself.
102
+ *
103
+ * The recommended use is multi-region deployments: the budget lives in
104
+ * the application's stack (in any region — `AWS::Budgets::Budget` is a
105
+ * global resource), and the alarms must live in a `us-east-1` stack so
106
+ * they can read the `AWS/Billing EstimatedCharges` metric AWS emits
107
+ * there.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * compose(
112
+ * {
113
+ * account: createBudgetBuilder()
114
+ * .budgetName("Account")
115
+ * .limit({ amount: 1000 })
116
+ * .recommendedAlarms(false), // suppress alarms in the budget's own stack
117
+ *
118
+ * accountAlarms: createBudgetAlarmBuilder()
119
+ * .budget(ref<BudgetBuilderResult>("account"))
120
+ * .recommendedAlarms({
121
+ * estimatedCharges: { threshold: 1000, currency: "USD" },
122
+ * }),
123
+ * },
124
+ * { account: [], accountAlarms: ["account"] },
125
+ * )
126
+ * .withStacks({
127
+ * account: appStack, // any region — Budgets is a global service
128
+ * accountAlarms: monitoringStack, // us-east-1 — where AWS/Billing metrics live
129
+ * })
130
+ * .build(app, "App");
131
+ * ```
132
+ *
133
+ * Set `crossRegionReferences: true` on both stacks if you reference the
134
+ * budget from custom alarms via `.addAlarm()`.
135
+ */
136
+ export function createBudgetAlarmBuilder() {
137
+ return Builder(BudgetAlarmBuilder);
138
+ }
139
+ //# sourceMappingURL=budget-alarm-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-alarm-builder.js","sourceRoot":"","sources":["../src/budget-alarm-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { AlarmDefinition } from "@composurecdk/cloudwatch";
2
+ import type { BudgetAlarmConfig } from "./alarm-config.js";
3
+ /**
4
+ * Resolves the recommended alarm configuration into fully-resolved
5
+ * {@link AlarmDefinition}s for an AWS Budget.
6
+ *
7
+ * AWS Budgets does not publish per-budget CloudWatch metrics — the only
8
+ * recommended alarm is the account-level `AWS/Billing EstimatedCharges`
9
+ * billing alarm. Off by default: callers must pass an
10
+ * {@link BudgetAlarmConfig.estimatedCharges} config to opt in.
11
+ *
12
+ * Period and statistic are fixed at the AWS-recommended values
13
+ * (6 hours, Maximum) and not exposed as configuration knobs — billing
14
+ * metrics only update every ~6 hours, so a shorter period would
15
+ * oversample. Threshold, currency, evaluation periods, datapoints and
16
+ * missing-data behaviour remain user-configurable via
17
+ * {@link EstimatedChargesAlarmConfig}.
18
+ *
19
+ * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html
20
+ */
21
+ export declare function resolveBudgetAlarmDefinitions(config: BudgetAlarmConfig | undefined): AlarmDefinition[];
22
+ //# sourceMappingURL=budget-alarms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-alarms.d.ts","sourceRoot":"","sources":["../src/budget-alarms.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,iBAAiB,GAAG,SAAS,GACpC,eAAe,EAAE,CAyBnB"}
@@ -0,0 +1,48 @@
1
+ import { Duration } from "aws-cdk-lib";
2
+ import { ComparisonOperator, Metric, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
3
+ const BILLING_METRIC_PERIOD = Duration.hours(6);
4
+ /**
5
+ * Resolves the recommended alarm configuration into fully-resolved
6
+ * {@link AlarmDefinition}s for an AWS Budget.
7
+ *
8
+ * AWS Budgets does not publish per-budget CloudWatch metrics — the only
9
+ * recommended alarm is the account-level `AWS/Billing EstimatedCharges`
10
+ * billing alarm. Off by default: callers must pass an
11
+ * {@link BudgetAlarmConfig.estimatedCharges} config to opt in.
12
+ *
13
+ * Period and statistic are fixed at the AWS-recommended values
14
+ * (6 hours, Maximum) and not exposed as configuration knobs — billing
15
+ * metrics only update every ~6 hours, so a shorter period would
16
+ * oversample. Threshold, currency, evaluation periods, datapoints and
17
+ * missing-data behaviour remain user-configurable via
18
+ * {@link EstimatedChargesAlarmConfig}.
19
+ *
20
+ * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html
21
+ */
22
+ export function resolveBudgetAlarmDefinitions(config) {
23
+ if (config?.enabled === false)
24
+ return [];
25
+ if (!config?.estimatedCharges)
26
+ return [];
27
+ const cfg = config.estimatedCharges;
28
+ const currency = cfg.currency ?? "USD";
29
+ return [
30
+ {
31
+ key: "estimatedCharges",
32
+ metric: new Metric({
33
+ namespace: "AWS/Billing",
34
+ metricName: "EstimatedCharges",
35
+ dimensionsMap: { Currency: currency },
36
+ statistic: "Maximum",
37
+ period: BILLING_METRIC_PERIOD,
38
+ }),
39
+ threshold: cfg.threshold,
40
+ comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
41
+ evaluationPeriods: cfg.evaluationPeriods ?? 1,
42
+ datapointsToAlarm: cfg.datapointsToAlarm ?? 1,
43
+ treatMissingData: cfg.treatMissingData ?? TreatMissingData.NOT_BREACHING,
44
+ description: `Account-level estimated charges exceeded ${String(cfg.threshold)} ${currency}. Billing metrics are only emitted in us-east-1.`,
45
+ },
46
+ ];
47
+ }
48
+ //# sourceMappingURL=budget-alarms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-alarms.js","sourceRoot":"","sources":["../src/budget-alarms.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAI1F,MAAM,qBAAqB,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,6BAA6B,CAC3C,MAAqC;IAErC,IAAI,MAAM,EAAE,OAAO,KAAK,KAAK;QAAE,OAAO,EAAE,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,gBAAgB;QAAE,OAAO,EAAE,CAAC;IAEzC,MAAM,GAAG,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACpC,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,KAAK,CAAC;IAEvC,OAAO;QACL;YACE,GAAG,EAAE,kBAAkB;YACvB,MAAM,EAAE,IAAI,MAAM,CAAC;gBACjB,SAAS,EAAE,aAAa;gBACxB,UAAU,EAAE,kBAAkB;gBAC9B,aAAa,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;gBACrC,SAAS,EAAE,SAAS;gBACpB,MAAM,EAAE,qBAAqB;aAC9B,CAAC;YACF,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,kBAAkB,EAAE,kBAAkB,CAAC,sBAAsB;YAC7D,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,IAAI,CAAC;YAC7C,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,IAAI,CAAC;YAC7C,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,IAAI,gBAAgB,CAAC,aAAa;YACxE,WAAW,EAAE,4CAA4C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,QAAQ,kDAAkD;SAC7I;KACF,CAAC;AACJ,CAAC"}