diff --git a/infrastructure/terraform/components/acct/README.md b/infrastructure/terraform/components/acct/README.md index ade163fd3..041f69a1b 100644 --- a/infrastructure/terraform/components/acct/README.md +++ b/infrastructure/terraform/components/acct/README.md @@ -13,7 +13,10 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes | +| [budget\_amount](#input\_budget\_amount) | The budget amount in USD for the account | `number` | `500` | no | | [component](#input\_component) | The variable encapsulating the name of this component | `string` | `"acct"` | no | +| [cost\_alarm\_recipients](#input\_cost\_alarm\_recipients) | A list of email addresses to receive alarm notifications | `list(string)` | `[]` | no | +| [cost\_anomaly\_threshold](#input\_cost\_anomaly\_threshold) | The threshold percentage for cost anomaly detection | `number` | `10` | no | | [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no | | [environment](#input\_environment) | The name of the tfscaffold environment | `string` | n/a | yes | | [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes | diff --git a/infrastructure/terraform/components/acct/budgets_budget.tf b/infrastructure/terraform/components/acct/budgets_budget.tf new file mode 100644 index 000000000..6a253fa5c --- /dev/null +++ b/infrastructure/terraform/components/acct/budgets_budget.tf @@ -0,0 +1,31 @@ +resource "aws_budgets_budget" "main" { + name = "${local.csi}-monthly-budget" + budget_type = "COST" + limit_amount = var.budget_amount + limit_unit = "USD" + time_unit = "MONTHLY" + + notification { + comparison_operator = "GREATER_THAN" + notification_type = "FORECASTED" + threshold = 100 + threshold_type = "PERCENTAGE" + subscriber_sns_topic_arns = [aws_sns_topic.costs.arn] + } + + notification { + comparison_operator = "GREATER_THAN" + notification_type = "ACTUAL" + threshold = 100 + threshold_type = "PERCENTAGE" + subscriber_sns_topic_arns = [aws_sns_topic.costs.arn] + } + + notification { + comparison_operator = "GREATER_THAN" + notification_type = "ACTUAL" + threshold = 85 + threshold_type = "PERCENTAGE" + subscriber_sns_topic_arns = [aws_sns_topic.costs.arn] + } +} diff --git a/infrastructure/terraform/components/acct/cost_anomaly_monitor.tf b/infrastructure/terraform/components/acct/cost_anomaly_monitor.tf new file mode 100644 index 000000000..986336a91 --- /dev/null +++ b/infrastructure/terraform/components/acct/cost_anomaly_monitor.tf @@ -0,0 +1,28 @@ +resource "aws_ce_anomaly_monitor" "anomaly_monitor" { + name = "${local.csi}-anomaly-monitor" + monitor_type = "DIMENSIONAL" + monitor_dimension = "SERVICE" +} + +resource "aws_ce_anomaly_subscription" "realtime_subscription" { + name = "${local.csi}-realtime-subscription" + frequency = "IMMEDIATE" + threshold_expression { + dimension { + key = "ANOMALY_TOTAL_IMPACT_PERCENTAGE" + values = [var.cost_anomaly_threshold] + match_options = ["GREATER_THAN_OR_EQUAL"] + } + } + monitor_arn_list = [ + aws_ce_anomaly_monitor.anomaly_monitor.arn, + ] + + subscriber { + type = "SNS" + address = aws_sns_topic.costs.arn + } + depends_on = [ + aws_sns_topic_policy.costs, + ] +} diff --git a/infrastructure/terraform/components/acct/sns_topic_costs.tf b/infrastructure/terraform/components/acct/sns_topic_costs.tf new file mode 100644 index 000000000..50b544d1c --- /dev/null +++ b/infrastructure/terraform/components/acct/sns_topic_costs.tf @@ -0,0 +1,37 @@ +resource "aws_sns_topic" "costs" { + name = "${local.csi}-costs" + kms_master_key_id = module.kms.key_id +} + +resource "aws_sns_topic_policy" "costs" { + arn = aws_sns_topic.costs.arn + + policy = data.aws_iam_policy_document.sns_costs.json +} + +data "aws_iam_policy_document" "sns_costs" { + statement { + sid = "AllowSNSCosts" + effect = "Allow" + + actions = [ + "SNS:Publish", + ] + + resources = [ + aws_sns_topic.costs.arn, + ] + + principals { + type = "Service" + identifiers = ["budgets.amazonaws.com", "costalerts.amazonaws.com"] + } + } +} + +resource "aws_sns_topic_subscription" "costs" { + for_each = toset(var.cost_alarm_recipients) + topic_arn = aws_sns_topic.costs.arn + protocol = "email" + endpoint = each.value +} diff --git a/infrastructure/terraform/components/acct/variables.tf b/infrastructure/terraform/components/acct/variables.tf index 92bbdb840..1a9f5be07 100644 --- a/infrastructure/terraform/components/acct/variables.tf +++ b/infrastructure/terraform/components/acct/variables.tf @@ -120,3 +120,21 @@ variable "oam_sink_id" { type = string default = "" } + +variable "cost_alarm_recipients" { + type = list(string) + description = "A list of email addresses to receive alarm notifications" + default = [] +} + +variable "budget_amount" { + type = number + description = "The budget amount in USD for the account" + default = 500 +} + +variable "cost_anomaly_threshold" { + type = number + description = "The threshold percentage for cost anomaly detection" + default = 10 +}