The aws:costexplorer/anomalySubscription:AnomalySubscription resource, part of the Pulumi AWS provider, defines how Cost Explorer anomaly alerts are delivered: notification frequency, delivery channels, and threshold conditions that trigger alerts. This guide focuses on three capabilities: email and SNS notification routing, absolute and percentage-based thresholds, and compound threshold expressions.
Anomaly subscriptions depend on existing Cost Explorer Anomaly Monitors and may route alerts to SNS topics with appropriate IAM policies. The examples are intentionally small. Combine them with your own monitors and notification infrastructure.
Send daily email alerts for absolute cost anomalies
Most cost monitoring starts with daily email notifications when anomalies exceed a fixed dollar threshold, providing baseline visibility without overwhelming teams.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const test = new aws.costexplorer.AnomalyMonitor("test", {
name: "AWSServiceMonitor",
monitorType: "DIMENSIONAL",
monitorDimension: "SERVICE",
});
const testAnomalySubscription = new aws.costexplorer.AnomalySubscription("test", {
name: "DAILYSUBSCRIPTION",
frequency: "DAILY",
monitorArnLists: [test.arn],
subscribers: [{
type: "EMAIL",
address: "abc@example.com",
}],
thresholdExpression: {
dimension: {
key: "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
matchOptions: ["GREATER_THAN_OR_EQUAL"],
values: ["100"],
},
},
});
import pulumi
import pulumi_aws as aws
test = aws.costexplorer.AnomalyMonitor("test",
name="AWSServiceMonitor",
monitor_type="DIMENSIONAL",
monitor_dimension="SERVICE")
test_anomaly_subscription = aws.costexplorer.AnomalySubscription("test",
name="DAILYSUBSCRIPTION",
frequency="DAILY",
monitor_arn_lists=[test.arn],
subscribers=[{
"type": "EMAIL",
"address": "abc@example.com",
}],
threshold_expression={
"dimension": {
"key": "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
"match_options": ["GREATER_THAN_OR_EQUAL"],
"values": ["100"],
},
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/costexplorer"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
test, err := costexplorer.NewAnomalyMonitor(ctx, "test", &costexplorer.AnomalyMonitorArgs{
Name: pulumi.String("AWSServiceMonitor"),
MonitorType: pulumi.String("DIMENSIONAL"),
MonitorDimension: pulumi.String("SERVICE"),
})
if err != nil {
return err
}
_, err = costexplorer.NewAnomalySubscription(ctx, "test", &costexplorer.AnomalySubscriptionArgs{
Name: pulumi.String("DAILYSUBSCRIPTION"),
Frequency: pulumi.String("DAILY"),
MonitorArnLists: pulumi.StringArray{
test.Arn,
},
Subscribers: costexplorer.AnomalySubscriptionSubscriberArray{
&costexplorer.AnomalySubscriptionSubscriberArgs{
Type: pulumi.String("EMAIL"),
Address: pulumi.String("abc@example.com"),
},
},
ThresholdExpression: &costexplorer.AnomalySubscriptionThresholdExpressionArgs{
Dimension: &costexplorer.AnomalySubscriptionThresholdExpressionDimensionArgs{
Key: pulumi.String("ANOMALY_TOTAL_IMPACT_ABSOLUTE"),
MatchOptions: pulumi.StringArray{
pulumi.String("GREATER_THAN_OR_EQUAL"),
},
Values: pulumi.StringArray{
pulumi.String("100"),
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var test = new Aws.CostExplorer.AnomalyMonitor("test", new()
{
Name = "AWSServiceMonitor",
MonitorType = "DIMENSIONAL",
MonitorDimension = "SERVICE",
});
var testAnomalySubscription = new Aws.CostExplorer.AnomalySubscription("test", new()
{
Name = "DAILYSUBSCRIPTION",
Frequency = "DAILY",
MonitorArnLists = new[]
{
test.Arn,
},
Subscribers = new[]
{
new Aws.CostExplorer.Inputs.AnomalySubscriptionSubscriberArgs
{
Type = "EMAIL",
Address = "abc@example.com",
},
},
ThresholdExpression = new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionArgs
{
Dimension = new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionDimensionArgs
{
Key = "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
MatchOptions = new[]
{
"GREATER_THAN_OR_EQUAL",
},
Values = new[]
{
"100",
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.costexplorer.AnomalyMonitor;
import com.pulumi.aws.costexplorer.AnomalyMonitorArgs;
import com.pulumi.aws.costexplorer.AnomalySubscription;
import com.pulumi.aws.costexplorer.AnomalySubscriptionArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionSubscriberArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionThresholdExpressionArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionThresholdExpressionDimensionArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var test = new AnomalyMonitor("test", AnomalyMonitorArgs.builder()
.name("AWSServiceMonitor")
.monitorType("DIMENSIONAL")
.monitorDimension("SERVICE")
.build());
var testAnomalySubscription = new AnomalySubscription("testAnomalySubscription", AnomalySubscriptionArgs.builder()
.name("DAILYSUBSCRIPTION")
.frequency("DAILY")
.monitorArnLists(test.arn())
.subscribers(AnomalySubscriptionSubscriberArgs.builder()
.type("EMAIL")
.address("abc@example.com")
.build())
.thresholdExpression(AnomalySubscriptionThresholdExpressionArgs.builder()
.dimension(AnomalySubscriptionThresholdExpressionDimensionArgs.builder()
.key("ANOMALY_TOTAL_IMPACT_ABSOLUTE")
.matchOptions("GREATER_THAN_OR_EQUAL")
.values("100")
.build())
.build())
.build());
}
}
resources:
test:
type: aws:costexplorer:AnomalyMonitor
properties:
name: AWSServiceMonitor
monitorType: DIMENSIONAL
monitorDimension: SERVICE
testAnomalySubscription:
type: aws:costexplorer:AnomalySubscription
name: test
properties:
name: DAILYSUBSCRIPTION
frequency: DAILY
monitorArnLists:
- ${test.arn}
subscribers:
- type: EMAIL
address: abc@example.com
thresholdExpression:
dimension:
key: ANOMALY_TOTAL_IMPACT_ABSOLUTE
matchOptions:
- GREATER_THAN_OR_EQUAL
values:
- '100'
The subscription monitors the specified anomaly monitor and sends daily reports to the email address. The thresholdExpression uses ANOMALY_TOTAL_IMPACT_ABSOLUTE to trigger alerts when cost anomalies exceed $100 in absolute terms. The frequency property controls how often alerts are sent; DAILY batches anomalies into a single daily report.
Alert on percentage-based cost increases
Some teams prefer percentage-based thresholds to catch proportional spending changes that absolute dollar amounts might miss, especially for services with variable baselines.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const test = new aws.costexplorer.AnomalySubscription("test", {
name: "AWSServiceMonitor",
frequency: "DAILY",
monitorArnLists: [testAwsCeAnomalyMonitor.arn],
subscribers: [{
type: "EMAIL",
address: "abc@example.com",
}],
thresholdExpression: {
dimension: {
key: "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
matchOptions: ["GREATER_THAN_OR_EQUAL"],
values: ["100"],
},
},
});
import pulumi
import pulumi_aws as aws
test = aws.costexplorer.AnomalySubscription("test",
name="AWSServiceMonitor",
frequency="DAILY",
monitor_arn_lists=[test_aws_ce_anomaly_monitor["arn"]],
subscribers=[{
"type": "EMAIL",
"address": "abc@example.com",
}],
threshold_expression={
"dimension": {
"key": "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
"match_options": ["GREATER_THAN_OR_EQUAL"],
"values": ["100"],
},
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/costexplorer"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := costexplorer.NewAnomalySubscription(ctx, "test", &costexplorer.AnomalySubscriptionArgs{
Name: pulumi.String("AWSServiceMonitor"),
Frequency: pulumi.String("DAILY"),
MonitorArnLists: pulumi.StringArray{
testAwsCeAnomalyMonitor.Arn,
},
Subscribers: costexplorer.AnomalySubscriptionSubscriberArray{
&costexplorer.AnomalySubscriptionSubscriberArgs{
Type: pulumi.String("EMAIL"),
Address: pulumi.String("abc@example.com"),
},
},
ThresholdExpression: &costexplorer.AnomalySubscriptionThresholdExpressionArgs{
Dimension: &costexplorer.AnomalySubscriptionThresholdExpressionDimensionArgs{
Key: pulumi.String("ANOMALY_TOTAL_IMPACT_PERCENTAGE"),
MatchOptions: pulumi.StringArray{
pulumi.String("GREATER_THAN_OR_EQUAL"),
},
Values: pulumi.StringArray{
pulumi.String("100"),
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var test = new Aws.CostExplorer.AnomalySubscription("test", new()
{
Name = "AWSServiceMonitor",
Frequency = "DAILY",
MonitorArnLists = new[]
{
testAwsCeAnomalyMonitor.Arn,
},
Subscribers = new[]
{
new Aws.CostExplorer.Inputs.AnomalySubscriptionSubscriberArgs
{
Type = "EMAIL",
Address = "abc@example.com",
},
},
ThresholdExpression = new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionArgs
{
Dimension = new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionDimensionArgs
{
Key = "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
MatchOptions = new[]
{
"GREATER_THAN_OR_EQUAL",
},
Values = new[]
{
"100",
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.costexplorer.AnomalySubscription;
import com.pulumi.aws.costexplorer.AnomalySubscriptionArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionSubscriberArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionThresholdExpressionArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionThresholdExpressionDimensionArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var test = new AnomalySubscription("test", AnomalySubscriptionArgs.builder()
.name("AWSServiceMonitor")
.frequency("DAILY")
.monitorArnLists(testAwsCeAnomalyMonitor.arn())
.subscribers(AnomalySubscriptionSubscriberArgs.builder()
.type("EMAIL")
.address("abc@example.com")
.build())
.thresholdExpression(AnomalySubscriptionThresholdExpressionArgs.builder()
.dimension(AnomalySubscriptionThresholdExpressionDimensionArgs.builder()
.key("ANOMALY_TOTAL_IMPACT_PERCENTAGE")
.matchOptions("GREATER_THAN_OR_EQUAL")
.values("100")
.build())
.build())
.build());
}
}
resources:
test:
type: aws:costexplorer:AnomalySubscription
properties:
name: AWSServiceMonitor
frequency: DAILY
monitorArnLists:
- ${testAwsCeAnomalyMonitor.arn}
subscribers:
- type: EMAIL
address: abc@example.com
thresholdExpression:
dimension:
key: ANOMALY_TOTAL_IMPACT_PERCENTAGE
matchOptions:
- GREATER_THAN_OR_EQUAL
values:
- '100'
The thresholdExpression switches to ANOMALY_TOTAL_IMPACT_PERCENTAGE, triggering alerts when costs increase by 100% or more relative to the learned baseline. This catches proportional changes regardless of absolute dollar amounts.
Combine multiple threshold conditions with AND logic
Complex alerting rules often require multiple conditions to reduce false positives, such as requiring both a minimum dollar amount and a percentage increase.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const test = new aws.costexplorer.AnomalySubscription("test", {
name: "AWSServiceMonitor",
frequency: "DAILY",
monitorArnLists: [testAwsCeAnomalyMonitor.arn],
subscribers: [{
type: "EMAIL",
address: "abc@example.com",
}],
thresholdExpression: {
ands: [
{
dimension: {
key: "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
matchOptions: ["GREATER_THAN_OR_EQUAL"],
values: ["100"],
},
},
{
dimension: {
key: "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
matchOptions: ["GREATER_THAN_OR_EQUAL"],
values: ["50"],
},
},
],
},
});
import pulumi
import pulumi_aws as aws
test = aws.costexplorer.AnomalySubscription("test",
name="AWSServiceMonitor",
frequency="DAILY",
monitor_arn_lists=[test_aws_ce_anomaly_monitor["arn"]],
subscribers=[{
"type": "EMAIL",
"address": "abc@example.com",
}],
threshold_expression={
"ands": [
{
"dimension": {
"key": "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
"match_options": ["GREATER_THAN_OR_EQUAL"],
"values": ["100"],
},
},
{
"dimension": {
"key": "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
"match_options": ["GREATER_THAN_OR_EQUAL"],
"values": ["50"],
},
},
],
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/costexplorer"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := costexplorer.NewAnomalySubscription(ctx, "test", &costexplorer.AnomalySubscriptionArgs{
Name: pulumi.String("AWSServiceMonitor"),
Frequency: pulumi.String("DAILY"),
MonitorArnLists: pulumi.StringArray{
testAwsCeAnomalyMonitor.Arn,
},
Subscribers: costexplorer.AnomalySubscriptionSubscriberArray{
&costexplorer.AnomalySubscriptionSubscriberArgs{
Type: pulumi.String("EMAIL"),
Address: pulumi.String("abc@example.com"),
},
},
ThresholdExpression: &costexplorer.AnomalySubscriptionThresholdExpressionArgs{
Ands: costexplorer.AnomalySubscriptionThresholdExpressionAndArray{
&costexplorer.AnomalySubscriptionThresholdExpressionAndArgs{
Dimension: &costexplorer.AnomalySubscriptionThresholdExpressionAndDimensionArgs{
Key: pulumi.String("ANOMALY_TOTAL_IMPACT_ABSOLUTE"),
MatchOptions: pulumi.StringArray{
pulumi.String("GREATER_THAN_OR_EQUAL"),
},
Values: pulumi.StringArray{
pulumi.String("100"),
},
},
},
&costexplorer.AnomalySubscriptionThresholdExpressionAndArgs{
Dimension: &costexplorer.AnomalySubscriptionThresholdExpressionAndDimensionArgs{
Key: pulumi.String("ANOMALY_TOTAL_IMPACT_PERCENTAGE"),
MatchOptions: pulumi.StringArray{
pulumi.String("GREATER_THAN_OR_EQUAL"),
},
Values: pulumi.StringArray{
pulumi.String("50"),
},
},
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var test = new Aws.CostExplorer.AnomalySubscription("test", new()
{
Name = "AWSServiceMonitor",
Frequency = "DAILY",
MonitorArnLists = new[]
{
testAwsCeAnomalyMonitor.Arn,
},
Subscribers = new[]
{
new Aws.CostExplorer.Inputs.AnomalySubscriptionSubscriberArgs
{
Type = "EMAIL",
Address = "abc@example.com",
},
},
ThresholdExpression = new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionArgs
{
Ands = new[]
{
new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionAndArgs
{
Dimension = new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionAndDimensionArgs
{
Key = "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
MatchOptions = new[]
{
"GREATER_THAN_OR_EQUAL",
},
Values = new[]
{
"100",
},
},
},
new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionAndArgs
{
Dimension = new Aws.CostExplorer.Inputs.AnomalySubscriptionThresholdExpressionAndDimensionArgs
{
Key = "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
MatchOptions = new[]
{
"GREATER_THAN_OR_EQUAL",
},
Values = new[]
{
"50",
},
},
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.costexplorer.AnomalySubscription;
import com.pulumi.aws.costexplorer.AnomalySubscriptionArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionSubscriberArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionThresholdExpressionArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var test = new AnomalySubscription("test", AnomalySubscriptionArgs.builder()
.name("AWSServiceMonitor")
.frequency("DAILY")
.monitorArnLists(testAwsCeAnomalyMonitor.arn())
.subscribers(AnomalySubscriptionSubscriberArgs.builder()
.type("EMAIL")
.address("abc@example.com")
.build())
.thresholdExpression(AnomalySubscriptionThresholdExpressionArgs.builder()
.ands(
AnomalySubscriptionThresholdExpressionAndArgs.builder()
.dimension(AnomalySubscriptionThresholdExpressionAndDimensionArgs.builder()
.key("ANOMALY_TOTAL_IMPACT_ABSOLUTE")
.matchOptions("GREATER_THAN_OR_EQUAL")
.values("100")
.build())
.build(),
AnomalySubscriptionThresholdExpressionAndArgs.builder()
.dimension(AnomalySubscriptionThresholdExpressionAndDimensionArgs.builder()
.key("ANOMALY_TOTAL_IMPACT_PERCENTAGE")
.matchOptions("GREATER_THAN_OR_EQUAL")
.values("50")
.build())
.build())
.build())
.build());
}
}
resources:
test:
type: aws:costexplorer:AnomalySubscription
properties:
name: AWSServiceMonitor
frequency: DAILY
monitorArnLists:
- ${testAwsCeAnomalyMonitor.arn}
subscribers:
- type: EMAIL
address: abc@example.com
thresholdExpression:
ands:
- dimension:
key: ANOMALY_TOTAL_IMPACT_ABSOLUTE
matchOptions:
- GREATER_THAN_OR_EQUAL
values:
- '100'
- dimension:
key: ANOMALY_TOTAL_IMPACT_PERCENTAGE
matchOptions:
- GREATER_THAN_OR_EQUAL
values:
- '50'
The thresholdExpression uses an ands array to combine multiple conditions. Alerts trigger only when anomalies meet both the $100 absolute threshold and the 50% percentage threshold. This reduces noise from small absolute changes or low-percentage increases on high-cost services.
Route alerts to SNS for programmatic handling
Teams building automated response workflows route anomaly alerts to SNS topics, enabling Lambda functions or other subscribers to process alerts programmatically.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const costAnomalyUpdates = new aws.sns.Topic("cost_anomaly_updates", {name: "CostAnomalyUpdates"});
const snsTopicPolicy = pulumi.all([costAnomalyUpdates.arn, costAnomalyUpdates.arn]).apply(([costAnomalyUpdatesArn, costAnomalyUpdatesArn1]) => aws.iam.getPolicyDocumentOutput({
policyId: "__default_policy_ID",
statements: [
{
sid: "AWSAnomalyDetectionSNSPublishingPermissions",
actions: ["SNS:Publish"],
effect: "Allow",
principals: [{
type: "Service",
identifiers: ["costalerts.amazonaws.com"],
}],
resources: [costAnomalyUpdatesArn],
},
{
sid: "__default_statement_ID",
actions: [
"SNS:Subscribe",
"SNS:SetTopicAttributes",
"SNS:RemovePermission",
"SNS:Receive",
"SNS:Publish",
"SNS:ListSubscriptionsByTopic",
"SNS:GetTopicAttributes",
"SNS:DeleteTopic",
"SNS:AddPermission",
],
conditions: [{
test: "StringEquals",
variable: "AWS:SourceOwner",
values: [accountId],
}],
effect: "Allow",
principals: [{
type: "AWS",
identifiers: ["*"],
}],
resources: [costAnomalyUpdatesArn1],
},
],
}));
const _default = new aws.sns.TopicPolicy("default", {
arn: costAnomalyUpdates.arn,
policy: snsTopicPolicy.apply(snsTopicPolicy => snsTopicPolicy.json),
});
const anomalyMonitor = new aws.costexplorer.AnomalyMonitor("anomaly_monitor", {
name: "AWSServiceMonitor",
monitorType: "DIMENSIONAL",
monitorDimension: "SERVICE",
});
const realtimeSubscription = new aws.costexplorer.AnomalySubscription("realtime_subscription", {
name: "RealtimeAnomalySubscription",
frequency: "IMMEDIATE",
monitorArnLists: [anomalyMonitor.arn],
subscribers: [{
type: "SNS",
address: costAnomalyUpdates.arn,
}],
}, {
dependsOn: [_default],
});
import pulumi
import pulumi_aws as aws
cost_anomaly_updates = aws.sns.Topic("cost_anomaly_updates", name="CostAnomalyUpdates")
sns_topic_policy = pulumi.Output.all(
costAnomalyUpdatesArn=cost_anomaly_updates.arn,
costAnomalyUpdatesArn1=cost_anomaly_updates.arn
).apply(lambda resolved_outputs: aws.iam.get_policy_document_output(policy_id="__default_policy_ID",
statements=[
{
"sid": "AWSAnomalyDetectionSNSPublishingPermissions",
"actions": ["SNS:Publish"],
"effect": "Allow",
"principals": [{
"type": "Service",
"identifiers": ["costalerts.amazonaws.com"],
}],
"resources": [resolved_outputs['costAnomalyUpdatesArn']],
},
{
"sid": "__default_statement_ID",
"actions": [
"SNS:Subscribe",
"SNS:SetTopicAttributes",
"SNS:RemovePermission",
"SNS:Receive",
"SNS:Publish",
"SNS:ListSubscriptionsByTopic",
"SNS:GetTopicAttributes",
"SNS:DeleteTopic",
"SNS:AddPermission",
],
"conditions": [{
"test": "StringEquals",
"variable": "AWS:SourceOwner",
"values": [account_id],
}],
"effect": "Allow",
"principals": [{
"type": "AWS",
"identifiers": ["*"],
}],
"resources": [resolved_outputs['costAnomalyUpdatesArn1']],
},
]))
default = aws.sns.TopicPolicy("default",
arn=cost_anomaly_updates.arn,
policy=sns_topic_policy.json)
anomaly_monitor = aws.costexplorer.AnomalyMonitor("anomaly_monitor",
name="AWSServiceMonitor",
monitor_type="DIMENSIONAL",
monitor_dimension="SERVICE")
realtime_subscription = aws.costexplorer.AnomalySubscription("realtime_subscription",
name="RealtimeAnomalySubscription",
frequency="IMMEDIATE",
monitor_arn_lists=[anomaly_monitor.arn],
subscribers=[{
"type": "SNS",
"address": cost_anomaly_updates.arn,
}],
opts = pulumi.ResourceOptions(depends_on=[default]))
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/costexplorer"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/sns"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
costAnomalyUpdates, err := sns.NewTopic(ctx, "cost_anomaly_updates", &sns.TopicArgs{
Name: pulumi.String("CostAnomalyUpdates"),
})
if err != nil {
return err
}
snsTopicPolicy := pulumi.All(costAnomalyUpdates.Arn,costAnomalyUpdates.Arn).ApplyT(func(_args []interface{}) (iam.GetPolicyDocumentResult, error) {
costAnomalyUpdatesArn := _args[0].(string)
costAnomalyUpdatesArn1 := _args[1].(string)
return iam.GetPolicyDocumentResult(interface{}(iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
PolicyId: pulumi.StringRef(pulumi.StringRef("__default_policy_ID")),
Statements: []iam.GetPolicyDocumentStatement([]iam.GetPolicyDocumentStatement{
{
Sid: pulumi.StringRef(pulumi.String(pulumi.StringRef("AWSAnomalyDetectionSNSPublishingPermissions"))),
Actions: []string{
"SNS:Publish",
},
Effect: pulumi.StringRef(pulumi.String(pulumi.StringRef("Allow"))),
Principals: []iam.GetPolicyDocumentStatementPrincipal{
{
Type: "Service",
Identifiers: []string{
"costalerts.amazonaws.com",
},
},
},
Resources: []string{
costAnomalyUpdatesArn,
},
},
{
Sid: pulumi.StringRef(pulumi.String(pulumi.StringRef("__default_statement_ID"))),
Actions: []string{
"SNS:Subscribe",
"SNS:SetTopicAttributes",
"SNS:RemovePermission",
"SNS:Receive",
"SNS:Publish",
"SNS:ListSubscriptionsByTopic",
"SNS:GetTopicAttributes",
"SNS:DeleteTopic",
"SNS:AddPermission",
},
Conditions: []iam.GetPolicyDocumentStatementCondition{
{
Test: "StringEquals",
Variable: "AWS:SourceOwner",
Values: interface{}{
accountId,
},
},
},
Effect: pulumi.StringRef(pulumi.String(pulumi.StringRef("Allow"))),
Principals: []iam.GetPolicyDocumentStatementPrincipal{
{
Type: "AWS",
Identifiers: []string{
"*",
},
},
},
Resources: []string{
costAnomalyUpdatesArn1,
},
},
}),
}, nil))), nil
}).(iam.GetPolicyDocumentResultOutput)
_default, err := sns.NewTopicPolicy(ctx, "default", &sns.TopicPolicyArgs{
Arn: costAnomalyUpdates.Arn,
Policy: pulumi.String(snsTopicPolicy.ApplyT(func(snsTopicPolicy iam.GetPolicyDocumentResult) (*string, error) {
return &snsTopicPolicy.Json, nil
}).(pulumi.StringPtrOutput)),
})
if err != nil {
return err
}
anomalyMonitor, err := costexplorer.NewAnomalyMonitor(ctx, "anomaly_monitor", &costexplorer.AnomalyMonitorArgs{
Name: pulumi.String("AWSServiceMonitor"),
MonitorType: pulumi.String("DIMENSIONAL"),
MonitorDimension: pulumi.String("SERVICE"),
})
if err != nil {
return err
}
_, err = costexplorer.NewAnomalySubscription(ctx, "realtime_subscription", &costexplorer.AnomalySubscriptionArgs{
Name: pulumi.String("RealtimeAnomalySubscription"),
Frequency: pulumi.String("IMMEDIATE"),
MonitorArnLists: pulumi.StringArray{
anomalyMonitor.Arn,
},
Subscribers: costexplorer.AnomalySubscriptionSubscriberArray{
&costexplorer.AnomalySubscriptionSubscriberArgs{
Type: pulumi.String("SNS"),
Address: costAnomalyUpdates.Arn,
},
},
}, pulumi.DependsOn([]pulumi.Resource{
_default,
}))
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var costAnomalyUpdates = new Aws.Sns.Topic("cost_anomaly_updates", new()
{
Name = "CostAnomalyUpdates",
});
var snsTopicPolicy = Aws.Iam.GetPolicyDocument.Invoke(new()
{
PolicyId = "__default_policy_ID",
Statements = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementArgs
{
Sid = "AWSAnomalyDetectionSNSPublishingPermissions",
Actions = new[]
{
"SNS:Publish",
},
Effect = "Allow",
Principals = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalArgs
{
Type = "Service",
Identifiers = new[]
{
"costalerts.amazonaws.com",
},
},
},
Resources = new[]
{
costAnomalyUpdates.Arn,
},
},
new Aws.Iam.Inputs.GetPolicyDocumentStatementArgs
{
Sid = "__default_statement_ID",
Actions = new[]
{
"SNS:Subscribe",
"SNS:SetTopicAttributes",
"SNS:RemovePermission",
"SNS:Receive",
"SNS:Publish",
"SNS:ListSubscriptionsByTopic",
"SNS:GetTopicAttributes",
"SNS:DeleteTopic",
"SNS:AddPermission",
},
Conditions = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementConditionArgs
{
Test = "StringEquals",
Variable = "AWS:SourceOwner",
Values = new[]
{
accountId,
},
},
},
Effect = "Allow",
Principals = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalArgs
{
Type = "AWS",
Identifiers = new[]
{
"*",
},
},
},
Resources = new[]
{
costAnomalyUpdates.Arn,
},
},
},
});
var @default = new Aws.Sns.TopicPolicy("default", new()
{
Arn = costAnomalyUpdates.Arn,
Policy = snsTopicPolicy.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
});
var anomalyMonitor = new Aws.CostExplorer.AnomalyMonitor("anomaly_monitor", new()
{
Name = "AWSServiceMonitor",
MonitorType = "DIMENSIONAL",
MonitorDimension = "SERVICE",
});
var realtimeSubscription = new Aws.CostExplorer.AnomalySubscription("realtime_subscription", new()
{
Name = "RealtimeAnomalySubscription",
Frequency = "IMMEDIATE",
MonitorArnLists = new[]
{
anomalyMonitor.Arn,
},
Subscribers = new[]
{
new Aws.CostExplorer.Inputs.AnomalySubscriptionSubscriberArgs
{
Type = "SNS",
Address = costAnomalyUpdates.Arn,
},
},
}, new CustomResourceOptions
{
DependsOn =
{
@default,
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.sns.Topic;
import com.pulumi.aws.sns.TopicArgs;
import com.pulumi.aws.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.sns.TopicPolicy;
import com.pulumi.aws.sns.TopicPolicyArgs;
import com.pulumi.aws.costexplorer.AnomalyMonitor;
import com.pulumi.aws.costexplorer.AnomalyMonitorArgs;
import com.pulumi.aws.costexplorer.AnomalySubscription;
import com.pulumi.aws.costexplorer.AnomalySubscriptionArgs;
import com.pulumi.aws.costexplorer.inputs.AnomalySubscriptionSubscriberArgs;
import com.pulumi.resources.CustomResourceOptions;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var costAnomalyUpdates = new Topic("costAnomalyUpdates", TopicArgs.builder()
.name("CostAnomalyUpdates")
.build());
final var snsTopicPolicy = Output.tuple(costAnomalyUpdates.arn(), costAnomalyUpdates.arn()).applyValue(values -> {
var costAnomalyUpdatesArn = values.t1;
var costAnomalyUpdatesArn1 = values.t2;
return IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
.policyId("__default_policy_ID")
.statements(
GetPolicyDocumentStatementArgs.builder()
.sid("AWSAnomalyDetectionSNSPublishingPermissions")
.actions("SNS:Publish")
.effect("Allow")
.principals(GetPolicyDocumentStatementPrincipalArgs.builder()
.type("Service")
.identifiers("costalerts.amazonaws.com")
.build())
.resources(costAnomalyUpdatesArn)
.build(),
GetPolicyDocumentStatementArgs.builder()
.sid("__default_statement_ID")
.actions(
"SNS:Subscribe",
"SNS:SetTopicAttributes",
"SNS:RemovePermission",
"SNS:Receive",
"SNS:Publish",
"SNS:ListSubscriptionsByTopic",
"SNS:GetTopicAttributes",
"SNS:DeleteTopic",
"SNS:AddPermission")
.conditions(GetPolicyDocumentStatementConditionArgs.builder()
.test("StringEquals")
.variable("AWS:SourceOwner")
.values(accountId)
.build())
.effect("Allow")
.principals(GetPolicyDocumentStatementPrincipalArgs.builder()
.type("AWS")
.identifiers("*")
.build())
.resources(costAnomalyUpdatesArn1)
.build())
.build());
});
var default_ = new TopicPolicy("default", TopicPolicyArgs.builder()
.arn(costAnomalyUpdates.arn())
.policy(snsTopicPolicy.applyValue(_snsTopicPolicy -> _snsTopicPolicy.json()))
.build());
var anomalyMonitor = new AnomalyMonitor("anomalyMonitor", AnomalyMonitorArgs.builder()
.name("AWSServiceMonitor")
.monitorType("DIMENSIONAL")
.monitorDimension("SERVICE")
.build());
var realtimeSubscription = new AnomalySubscription("realtimeSubscription", AnomalySubscriptionArgs.builder()
.name("RealtimeAnomalySubscription")
.frequency("IMMEDIATE")
.monitorArnLists(anomalyMonitor.arn())
.subscribers(AnomalySubscriptionSubscriberArgs.builder()
.type("SNS")
.address(costAnomalyUpdates.arn())
.build())
.build(), CustomResourceOptions.builder()
.dependsOn(default_)
.build());
}
}
resources:
costAnomalyUpdates:
type: aws:sns:Topic
name: cost_anomaly_updates
properties:
name: CostAnomalyUpdates
default:
type: aws:sns:TopicPolicy
properties:
arn: ${costAnomalyUpdates.arn}
policy: ${snsTopicPolicy.json}
anomalyMonitor:
type: aws:costexplorer:AnomalyMonitor
name: anomaly_monitor
properties:
name: AWSServiceMonitor
monitorType: DIMENSIONAL
monitorDimension: SERVICE
realtimeSubscription:
type: aws:costexplorer:AnomalySubscription
name: realtime_subscription
properties:
name: RealtimeAnomalySubscription
frequency: IMMEDIATE
monitorArnLists:
- ${anomalyMonitor.arn}
subscribers:
- type: SNS
address: ${costAnomalyUpdates.arn}
options:
dependsOn:
- ${default}
variables:
snsTopicPolicy:
fn::invoke:
function: aws:iam:getPolicyDocument
arguments:
policyId: __default_policy_ID
statements:
- sid: AWSAnomalyDetectionSNSPublishingPermissions
actions:
- SNS:Publish
effect: Allow
principals:
- type: Service
identifiers:
- costalerts.amazonaws.com
resources:
- ${costAnomalyUpdates.arn}
- sid: __default_statement_ID
actions:
- SNS:Subscribe
- SNS:SetTopicAttributes
- SNS:RemovePermission
- SNS:Receive
- SNS:Publish
- SNS:ListSubscriptionsByTopic
- SNS:GetTopicAttributes
- SNS:DeleteTopic
- SNS:AddPermission
conditions:
- test: StringEquals
variable: AWS:SourceOwner
values:
- ${accountId}
effect: Allow
principals:
- type: AWS
identifiers:
- '*'
resources:
- ${costAnomalyUpdates.arn}
The subscriber type changes to SNS, and the address points to an SNS topic ARN. The frequency switches to IMMEDIATE for real-time alerting. The example includes the required SNS topic policy that grants the costalerts.amazonaws.com service principal permission to publish messages. The dependsOn ensures the policy is in place before creating the subscription.
Beyond these examples
These snippets focus on specific subscription-level features: email and SNS notification routing, absolute and percentage-based thresholds, and compound threshold expressions with AND logic. They’re intentionally minimal rather than full cost monitoring solutions.
The examples reference pre-existing infrastructure such as Cost Explorer Anomaly Monitors (monitorArnLists) and IAM permissions for the Cost Anomaly Detection service. They focus on configuring the subscription rather than provisioning the monitoring infrastructure.
To keep things focused, common subscription patterns are omitted, including:
- OR and NOT threshold expressions
- Multiple subscriber configurations (email + SNS)
- Weekly frequency option
- Account-specific subscriptions (accountId)
These omissions are intentional: the goal is to illustrate how each subscription feature is wired, not provide drop-in cost monitoring modules. See the Cost Explorer Anomaly Subscription resource reference for all available configuration options.
Let's configure AWS Cost Explorer Anomaly Subscriptions
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Try Pulumi Cloud for FREEFrequently Asked Questions
Configuration & Setup
accountId and name properties are immutable and cannot be modified after creation. Plan these values carefully, as changing them requires recreating the resource.monitorArnLists property is required and must reference existing aws.costexplorer.AnomalyMonitor resources.subscribers array supports multiple subscriber configurations, allowing you to send alerts to multiple email addresses or SNS topics.Threshold Expressions
ANOMALY_TOTAL_IMPACT_ABSOLUTE for dollar amount thresholds (e.g., alert when anomaly exceeds $100), or ANOMALY_TOTAL_IMPACT_PERCENTAGE for percentage-based thresholds (e.g., alert when anomaly exceeds 50% increase).ands array in thresholdExpression to require multiple conditions. For example, you can alert only when an anomaly meets both a minimum dollar amount AND a minimum percentage threshold.Notifications
DAILY, IMMEDIATE, or WEEKLY for the frequency property. Use IMMEDIATE for real-time alerts or DAILY/WEEKLY for batched reports.type: "SNS" and the SNS topic ARN as the address. The SNS topic policy must grant SNS:Publish permission to the costalerts.amazonaws.com service principal.Using a different cloud?
Explore monitoring guides for other cloud providers: