# CloudPeek setup: a "Top services by spend" CloudWatch dashboard and read-only access for the
# CloudPeek app. The Terraform twin of cloudpeek-setup.yaml.
# Guide: https://cloudpeek.app/docs/spend.html
#
# Before applying, turn on "Receive CloudWatch billing alerts" in the Billing console
# (Billing preferences > Alert preferences). Terraform can't change that setting.
#
# This configuration doesn't create an access key, so no secret ever lands in Terraform state.
# After applying, make one for the user:
#   aws iam create-access-key --user-name cloudpeek
#
# Save this file in an empty directory, then run: terraform init && terraform apply
# To add it to an existing configuration instead, leave out the terraform and provider blocks.

terraform {
  required_version = ">= 1.3"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
    }
  }
}

# Dashboards and IAM are global, but billing metrics only exist in us-east-1.
provider "aws" {
  region = "us-east-1"
}

variable "dashboard_name" {
  description = "Name of the CloudWatch dashboard to create."
  type        = string
  default     = "CloudPeek-Spend"
}

variable "top_services" {
  description = "How many of your biggest services to chart. They're found automatically each time the dashboard loads, and everything else still counts in the Total."
  type        = number
  default     = 5

  validation {
    condition     = var.top_services >= 1 && var.top_services <= 20
    error_message = "Chart between 1 and 20 services."
  }
}

variable "currency" {
  description = "The currency your AWS bill is in (the Currency dimension of the billing metrics)."
  type        = string
  default     = "USD"

  validation {
    condition     = can(regex("^[A-Z]{3}$", var.currency))
    error_message = "Use a three-letter currency code, such as USD."
  }
}

variable "policy_name" {
  description = "Name of the read-only IAM policy. IAM Identity Center permission sets can attach it by this name as a customer managed policy."
  type        = string
  default     = "CloudPeekReadOnly"
}

variable "create_user" {
  description = "Create an IAM user with the policy attached, for signing in to CloudPeek with an access key. Set to false if you sign in with IAM Identity Center."
  type        = bool
  default     = true
}

variable "user_name" {
  description = "Name of the IAM user (used only when create_user is true)."
  type        = string
  default     = "cloudpeek"
}

# The four read-only CloudWatch actions CloudPeek uses. These actions don't support resource-level
# permissions for this use, so the resource is "*".
data "aws_iam_policy_document" "cloudpeek" {
  statement {
    sid    = "CloudPeekReadOnly"
    effect = "Allow"
    actions = [
      "cloudwatch:ListDashboards",
      "cloudwatch:GetDashboard",
      "cloudwatch:GetMetricData",
      "cloudwatch:DescribeAlarms",
    ]
    resources = ["*"]
  }
}

resource "aws_iam_policy" "cloudpeek" {
  name        = var.policy_name
  description = "Read-only CloudWatch access for the CloudPeek app (dashboards, metric data and alarms)."
  policy      = data.aws_iam_policy_document.cloudpeek.json
}

resource "aws_iam_user" "cloudpeek" {
  count = var.create_user ? 1 : 0
  name  = var.user_name
}

resource "aws_iam_user_policy_attachment" "cloudpeek" {
  count      = var.create_user ? 1 : 0
  user       = aws_iam_user.cloudpeek[0].name
  policy_arn = aws_iam_policy.cloudpeek.arn
}

# The first row is the account-wide charge, which CloudPeek shows as the widget's Total. The
# expression finds every service's charge (SEARCH) and keeps the biggest (SORT ... DESC); the
# dynamic label names each bar after its service ($${...} keeps Terraform from interpolating it).
# The bar widget looks at the last day so the ranking follows this month's charges, which go back
# to zero each month; the total keeps the dashboard's week for its sparkline.
resource "aws_cloudwatch_dashboard" "spend" {
  dashboard_name = var.dashboard_name
  dashboard_body = jsonencode({
    start = "-P7D"
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 8
        properties = {
          title  = "Top ${var.top_services} services by spend"
          view   = "bar"
          region = "us-east-1"
          start  = "-P1D"
          stat   = "Maximum"
          period = 21600
          metrics = [
            ["AWS/Billing", "EstimatedCharges", "Currency", var.currency, { label = "Total" }],
            [{
              expression = "SORT(SEARCH('{AWS/Billing,Currency,ServiceName} MetricName=\"EstimatedCharges\" Currency=\"${var.currency}\"', 'Maximum', 21600), MAX, DESC, ${var.top_services})"
              label      = "$${PROP('Dim.ServiceName')}"
              id         = "top"
            }],
          ]
        }
      },
      {
        type   = "metric"
        x      = 12
        y      = 0
        width  = 6
        height = 8
        properties = {
          title     = "Month-to-date spend"
          view      = "singleValue"
          region    = "us-east-1"
          stat      = "Maximum"
          period    = 21600
          sparkline = true
          metrics = [
            ["AWS/Billing", "EstimatedCharges", "Currency", var.currency, { label = "Total" }],
          ]
        }
      },
    ]
  })
}

output "dashboard_url" {
  description = "The dashboard in the CloudWatch console."
  value       = "https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards/dashboard/${aws_cloudwatch_dashboard.spend.dashboard_name}"
}

output "policy_arn" {
  description = "The read-only policy. Attach it to a role, or reference it by name from an IAM Identity Center permission set."
  value       = aws_iam_policy.cloudpeek.arn
}

output "next_step" {
  description = "Create an access key for the user, then enter it in CloudPeek. The secret is shown only once."
  value       = var.create_user ? "aws iam create-access-key --user-name ${aws_iam_user.cloudpeek[0].name}" : "Attach ${aws_iam_policy.cloudpeek.name} to an IAM Identity Center permission set."
}
