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, 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 FREE

Frequently Asked Questions

Configuration & Setup
What properties can't I change after creating an anomaly subscription?
The accountId and name properties are immutable and cannot be modified after creation. Plan these values carefully, as changing them requires recreating the resource.
Do I need to create an anomaly monitor before creating a subscription?
Yes, the monitorArnLists property is required and must reference existing aws.costexplorer.AnomalyMonitor resources.
Can I have multiple subscribers for one anomaly subscription?
Yes, the subscribers array supports multiple subscriber configurations, allowing you to send alerts to multiple email addresses or SNS topics.
Threshold Expressions
What's the difference between absolute and percentage thresholds?
Use 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).
Can I combine multiple threshold conditions?
Yes, use the 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
What notification frequencies are available?
You can choose DAILY, IMMEDIATE, or WEEKLY for the frequency property. Use IMMEDIATE for real-time alerts or DAILY/WEEKLY for batched reports.
How do I set up SNS notifications for anomaly alerts?
Configure a subscriber with 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.
What's the difference between EMAIL and SNS subscriber types?
EMAIL subscribers receive alerts directly at the specified email address, while SNS subscribers publish to an SNS topic (allowing integration with Lambda, SQS, or other AWS services). SNS requires additional topic policy configuration.

Using a different cloud?

Explore monitoring guides for other cloud providers: