Configure AWS Cost Explorer Anomaly Subscriptions

The aws:costexplorer/anomalySubscription:AnomalySubscription resource, part of the Pulumi AWS provider, defines how Cost Explorer anomaly alerts are delivered: notification frequency, recipient configuration, and threshold rules that determine which anomalies trigger notifications. This guide focuses on three capabilities: email and SNS notification delivery, threshold expressions for filtering alerts, and delivery frequency configuration.

Anomaly subscriptions depend on existing Cost Explorer Anomaly Monitors and may reference 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 anomalies detected by the referenced AnomalyMonitor and sends daily summaries to the specified email address. The thresholdExpression filters alerts using ANOMALY_TOTAL_IMPACT_ABSOLUTE, which measures the dollar amount of unexpected spending. Here, only anomalies exceeding $100 trigger notifications.

Alert on percentage-based cost increases

Some teams prefer percentage-based thresholds to catch proportional spending changes that absolute dollar limits might miss.

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 uses ANOMALY_TOTAL_IMPACT_PERCENTAGE instead of absolute dollars. This catches anomalies that represent a 100% or greater cost increase, regardless of the actual dollar amount. A $10 service doubling to $20 triggers the same alert as a $1000 service reaching $2000.

Combine multiple threshold conditions with AND logic

Complex alerting rules often require multiple conditions to reduce false positives, such as requiring both absolute and percentage thresholds.

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 ands array within thresholdExpression combines multiple conditions. Both the absolute impact ($100+) and percentage impact (50%+) must be met for an alert to fire. This reduces noise from small percentage increases on low-cost services or large absolute increases that represent normal scaling.

Deliver immediate alerts to SNS topics

Production monitoring often routes alerts through SNS to integrate with incident management systems or trigger automated responses.

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 frequency changes to IMMEDIATE for real-time delivery, and the subscriber type switches to SNS. The SNS topic requires a policy granting costalerts.amazonaws.com publish permissions. The dependsOn ensures the policy is applied before the subscription attempts to use the topic.

Beyond these examples

These snippets focus on specific subscription-level features: email and SNS notification delivery, threshold expressions (absolute, percentage, compound logic), and delivery frequency (daily vs immediate). They’re intentionally minimal rather than full cost monitoring solutions.

The examples reference pre-existing infrastructure such as Cost Explorer Anomaly Monitors (monitorArnLists) and SNS topics with appropriate IAM policies. They focus on configuring the subscription rather than provisioning the monitoring infrastructure.

To keep things focused, common subscription patterns are omitted, including:

  • Account-specific subscriptions (accountId)
  • Resource tagging (tags)
  • OR and NOT threshold expressions
  • Multiple subscriber configurations

These omissions are intentional: the goal is to illustrate how each subscription feature is wired, not provide drop-in 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 FREE

Frequently Asked Questions

Threshold Configuration
What threshold types can I use for anomaly detection?
You can use ANOMALY_TOTAL_IMPACT_ABSOLUTE for dollar amounts (e.g., >= $100) or ANOMALY_TOTAL_IMPACT_PERCENTAGE for percentage changes (e.g., >= 50%).
How do I combine multiple threshold conditions?
Use the ands array within thresholdExpression to require multiple conditions. For example, you can trigger alerts when both absolute impact >= $100 AND percentage impact >= 50%.
Notification & Subscribers
What subscriber types are supported?
Both EMAIL and SNS subscriber types are supported. Configure the type field in the subscribers array with the appropriate address (email or SNS topic ARN).
What IAM permissions does my SNS topic need for anomaly subscriptions?
The SNS topic policy must allow SNS:Publish action for the costalerts.amazonaws.com service principal.
Why should I use dependsOn with SNS subscribers?
Use dependsOn to ensure the SNS topic policy is created before the anomaly subscription, preventing permission errors during subscription creation.
Frequency & Monitoring
What notification frequencies are available?
Three frequencies are supported: DAILY (once per day), IMMEDIATE (real-time alerts), and WEEKLY (once per week).
Can I subscribe to multiple anomaly monitors?
Yes, monitorArnLists accepts an array of cost anomaly monitor ARNs, allowing you to aggregate alerts from multiple monitors.
Resource Management
What properties can't I change after creating the subscription?
Both accountId and name are immutable after creation. Changing either requires recreating the subscription.
How do I import an existing anomaly subscription?
Use pulumi import aws:costexplorer/anomalySubscription:AnomalySubscription with the subscription ARN as the identifier.

Using a different cloud?

Explore monitoring guides for other cloud providers: