From 54ea04dce7057433a1964019ca2a248bf2cafdf7 Mon Sep 17 00:00:00 2001 From: Robert Sturla Date: Sun, 8 Sep 2024 09:33:24 +0100 Subject: [PATCH 1/2] feat(cost-control): create budgets module --- examples/aws/cost-control/budgets/main.tf | 30 ++++ modules/aws/cost-control/budgets/main.tf | 135 ++++++++++++++++++ modules/aws/cost-control/budgets/outputs.tf | 4 + modules/aws/cost-control/budgets/variables.tf | 65 +++++++++ modules/aws/cost-control/budgets/versions.tf | 10 ++ 5 files changed, 244 insertions(+) create mode 100644 examples/aws/cost-control/budgets/main.tf create mode 100644 modules/aws/cost-control/budgets/main.tf create mode 100644 modules/aws/cost-control/budgets/outputs.tf create mode 100644 modules/aws/cost-control/budgets/variables.tf create mode 100644 modules/aws/cost-control/budgets/versions.tf diff --git a/examples/aws/cost-control/budgets/main.tf b/examples/aws/cost-control/budgets/main.tf new file mode 100644 index 0000000..63cf115 --- /dev/null +++ b/examples/aws/cost-control/budgets/main.tf @@ -0,0 +1,30 @@ +module "budgets" { + source = "../../../../modules/aws/cost-control/budgets" + + budgets = { + RDSMonthly = { + limit_amount = 100 + cost_filter = [{ + name = "Service" + values = ["Amazon Relational Database Service"] + }] + } + EC2Monthly = { + limit_amount = 150 + cost_filter = [{ + name = "Service" + values = ["Amazon Elastic Compute Cloud - Compute"] + }] + } + } + + notifications = [ + { + comparison_operator = "GREATER_THAN" + notification_type = "ACTUAL" + threshold = 100 + threshold_type = "PERCENTAGE" + subscriber_email_addresses = [] + } + ] +} diff --git a/modules/aws/cost-control/budgets/main.tf b/modules/aws/cost-control/budgets/main.tf new file mode 100644 index 0000000..b18ae8d --- /dev/null +++ b/modules/aws/cost-control/budgets/main.tf @@ -0,0 +1,135 @@ +data "aws_caller_identity" "current" {} + +resource "aws_budgets_budget" "this" { + for_each = var.budgets + + name = each.key + budget_type = each.value.budget_type + limit_amount = each.value.limit_amount + limit_unit = lookup(each.value, "limit_unit", "USD") + time_period_start = lookup(each.value, "time_period_start", null) + time_period_end = lookup(each.value, "time_period_end", null) + time_unit = lookup(each.value, "time_unit", "MONTHLY") + + dynamic "cost_types" { + for_each = lookup(each.value, "cost_types", null) != null ? [each.value.cost_types] : [] + + content { + include_credit = lookup(cost_types.value, "include_credit", null) + include_discount = lookup(cost_types.value, "include_discount", null) + include_other_subscription = lookup(cost_types.value, "include_other_subscription", null) + include_recurring = lookup(cost_types.value, "include_recurring", null) + include_refund = lookup(cost_types.value, "include_refund", null) + include_subscription = lookup(cost_types.value, "include_subscription", null) + include_support = lookup(cost_types.value, "include_support", null) + include_tax = lookup(cost_types.value, "include_tax", null) + include_upfront = lookup(cost_types.value, "include_upfront", null) + use_blended = lookup(cost_types.value, "use_blended", null) + } + } + + dynamic "cost_filter" { + for_each = toset(each.value.cost_filter) + + content { + name = cost_filter.value.name + values = cost_filter.value.values + } + } + + dynamic "notification" { + for_each = var.notifications != null ? toset(var.notifications) : [] + + content { + comparison_operator = notification.value.comparison_operator + notification_type = notification.value.notification_type + threshold = notification.value.threshold + threshold_type = notification.value.threshold_type + subscriber_email_addresses = notification.value.subscriber_email_addresses + subscriber_sns_topic_arns = concat( + var.sns_topic_name != null && length(var.notifications) > 0 ? [aws_sns_topic.notifications[0].arn] : [], + notification.value.subscriber_sns_topic_arns, + ) + } + } + + tags = merge( + var.tags_all, + each.value.tags + ) +} + +resource "aws_sns_topic" "notifications" { + count = var.sns_topic_name != null && length(var.notifications) > 0 ? 1 : 0 + name = var.sns_topic_name +} + +resource "aws_sns_topic_policy" "notifications" { + count = var.sns_topic_name != null && length(var.notifications) > 0 ? 1 : 0 + + arn = aws_sns_topic.notifications[0].arn + policy = data.aws_iam_policy_document.notifications[0].json +} + +data "aws_iam_policy_document" "notifications" { + count = var.sns_topic_name != null && length(var.notifications) > 0 ? 1 : 0 + + statement { + sid = "__default_statement_ID" + effect = "Allow" + actions = [ + "SNS:GetTopicAttributes", + "SNS:SetTopicAttributes", + "SNS:AddPermission", + "SNS:RemovePermission", + "SNS:DeleteTopic", + "SNS:Subscribe", + "SNS:ListSubscriptionsByTopic", + "SNS:Publish", + ] + resources = [ + aws_sns_topic.notifications[0].arn, + ] + principals { + type = "AWS" + identifiers = ["*"] + } + + condition { + test = "StringEquals" + variable = "AWS:SourceOwner" + values = [ + data.aws_caller_identity.current.account_id, + ] + } + } + + statement { + sid = "AllowAWSBudgetsPublish" + effect = "Allow" + actions = [ + "sns:Publish", + ] + resources = [ + aws_sns_topic.notifications[0].arn, + ] + principals { + type = "Service" + identifiers = ["budgets.amazonaws.com"] + } + condition { + test = "StringEquals" + variable = "AWS:SourceAccount" + values = [ + data.aws_caller_identity.current.account_id, + ] + } + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = [ + "arn:aws:budgets::${data.aws_caller_identity.current.account_id}:*", + ] + } + } +} diff --git a/modules/aws/cost-control/budgets/outputs.tf b/modules/aws/cost-control/budgets/outputs.tf new file mode 100644 index 0000000..e9a2598 --- /dev/null +++ b/modules/aws/cost-control/budgets/outputs.tf @@ -0,0 +1,4 @@ +output "sns_topic_arn" { + description = "The ARN of the SNS topic to use for budget notifications" + value = var.sns_topic_name != null && length(var.notifications) > 0 ? aws_sns_topic.notifications[0].arn : null +} diff --git a/modules/aws/cost-control/budgets/variables.tf b/modules/aws/cost-control/budgets/variables.tf new file mode 100644 index 0000000..f9ec886 --- /dev/null +++ b/modules/aws/cost-control/budgets/variables.tf @@ -0,0 +1,65 @@ +variable "budgets" { + description = "A map of budgets to create" + type = map(object({ + budget_type = optional(string, "COST") + limit_amount = number + limit_unit = optional(string, "USD") + time_period_start = optional(string, null) + time_period_end = optional(string, null) + time_unit = optional(string, "MONTHLY") + cost_types = optional(object({ + include_credit = optional(bool, false) + include_discount = optional(bool, false) + include_other_subscription = optional(bool, false) + include_recurring = optional(bool, false) + include_refund = optional(bool, false) + include_subscription = optional(bool, false) + include_support = optional(bool, false) + include_tax = optional(bool, false) + include_upfront = optional(bool, false) + use_blended = optional(bool, false) + }), { + include_credit = false + include_discount = false + include_other_subscription = false + include_recurring = false + include_refund = false + include_subscription = true + include_support = false + include_tax = false + include_upfront = false + use_blended = false + }) + cost_filter = list(object({ + name = string + values = list(string) + })) + tags = optional(map(string), {}) + })) + default = {} +} + +variable "sns_topic_name" { + description = "The name of the SNS topic to create for budget notifications" + type = string + default = "budget-notifications" +} + +variable "notifications" { + description = "A list of notifications to create for budgets" + type = list(object({ + comparison_operator = optional(string, "GREATER_THAN") + notification_type = optional(string, "ACTUAL") + threshold = optional(number, 100) + threshold_type = optional(string, "PERCENTAGE") + subscriber_email_addresses = optional(list(string), []) + subscriber_sns_topic_arns = optional(list(string), []) + })) + default = [] +} + +variable "tags_all" { + type = map(string) + description = "A map of tags to assign to all resources" + default = {} +} diff --git a/modules/aws/cost-control/budgets/versions.tf b/modules/aws/cost-control/budgets/versions.tf new file mode 100644 index 0000000..9416453 --- /dev/null +++ b/modules/aws/cost-control/budgets/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">=1.3" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">=4.0" + } + } +} From ecd096bfeca8b7c484a8ed886cbd0cc90b52f028 Mon Sep 17 00:00:00 2001 From: Robert Sturla Date: Sun, 8 Sep 2024 09:34:06 +0100 Subject: [PATCH 2/2] chore: add tags to sns topic --- modules/aws/cost-control/budgets/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/aws/cost-control/budgets/main.tf b/modules/aws/cost-control/budgets/main.tf index b18ae8d..2bf0d1f 100644 --- a/modules/aws/cost-control/budgets/main.tf +++ b/modules/aws/cost-control/budgets/main.tf @@ -62,6 +62,7 @@ resource "aws_budgets_budget" "this" { resource "aws_sns_topic" "notifications" { count = var.sns_topic_name != null && length(var.notifications) > 0 ? 1 : 0 name = var.sns_topic_name + tags = var.tags_all } resource "aws_sns_topic_policy" "notifications" {