🦅

Cost Optimization Hubの推奨事項をSlackに定期的に通知する方法

2025/02/01に公開

はじめに

re:Invent2023で発表されたCost Optimization Hubは、コスト最適化サービスの推奨事項を集約してダッシュボードに表示してくれる便利なサービスです。

ただ、推奨事項を通知してくれる機能は無く(2025年2月現在だと)、毎度AWSコンソールからCost Optimization Hubのダッシュボードを確認する必要があります。
毎月、Optimization Hubのダッシュボードを確認するといったような、人力の運用もなくはないですが、流石に泥臭いので、推奨事項を定期的にSlackに通知するEventBridgeとLambdaを、Terraformで設定してみました。

設定

Cost Optimization Hubの有効化

デフォルトでは、Cost Optimization Hubは無効化されているので、有効化を行います(無料)。
Terraformのaws providerでは、Cost Optimization Hubの有効化を行うリソースが無いので(2025年2月現在だと)、local-execとaws cliを組み合わせて、有効化を行います。
リージョンはus-east-1を指定します(Cost Optimization Hubはグローバルリソースなので)。

resource "terraform_data" "enable_cost_optimization_hub" {
  provisioner "local-exec" {
    command = <<-EOT
      aws cost-optimization-hub update-enrollment-status --status Active --region us-east-1
    EOT
  }
}

Compute Optimizerの有効化

Cost Optimization Hubは、Compute Optimizerの推奨事項を集約することができるので、コンピュートリソースを使用しているリージョンのCompute Optimizerの有効化も行います(無料)。

resource "aws_computeoptimizer_enrollment_status" "main" {
  status = "Active"
}

SlackのWebhook URLの取得

推奨事項をSlackに通知するのに使用するWebhook URLを取得します。
Webhook URLの取得方法は以下ブログ記事が参考になります。

https://zenn.dev/hotaka_noda/articles/4a6f0ccee73a18

Lambdaの作成

Lambdaを作成するTerraformコードのディレクトリツリーは以下の通りです。
ランタイムにはPythonを使用します。

├ python
    ├ layer
        ├ python
        ├ requirements.txt
    ├ src
        ├ lambda_function.py
├ main.tf

Lambdaレイヤーの作成

SlackのSDKを使用するので、パッケージを含んだLambdaレイヤーを作成します。

まず、requirements.txtを以下のように設定し、python/layer配下でpip install -t ./python -r requirements.txtを実行します。

slack_sdk

python/layer配下にpythonといった名前でパッケージを含んだディレクトリが作成されるので、その後、Lambdaレイヤーを作成します。

module "lambda_layer" {
  # Module source
  source  = "terraform-aws-modules/lambda/aws"
  version = "7.8.1"
  # Module arguments
  layer_name               = "lambda-layer-cost-optimization-hub-recommendations"
  description              = null
  create_layer             = true
  create_package           = true
  recreate_missing_package = false
  source_path              = "/python/layer"
  compatible_runtimes      = ["python3.12"]
  compatible_architectures = ["arm64"]
}

Pythonコードの作成

lambda_function.pyを以下のようにコーディングします。

import boto3
import json
import os
from slack_sdk.webhook import WebhookClient

def lambda_handler(event, context):
    cost_optimization_hub = boto3.client('cost-optimization-hub', region_name='us-east-1')
    slack_webhook = WebhookClient(os.environ["SLACK_WEBHOOK_URL"])

    try:
        response = cost_optimization_hub.list_recommendations()
        items = response['items']
    except Exception as e:
        error_message = f"Error: {str(e)}"
        print(error_message)

    if not items:
        print("No recommendations found.")
        return

    attachments = format_attachments(items)

    try:
        slack_mention_id = os.environ.get("SLACK_MENTION_ID")
        mention_text = f"{slack_mention_id}" if slack_mention_id else ""

        response = slack_webhook.send(
            text=mention_text,
            attachments=attachments
        )
        print("Notify optimization hub recommendations successfully")
    except Exception as e:
        error_message = f"Error: {str(e)}"
        print(error_message)

def format_attachments(items):
    attachments = []
    for item in items:
        attachment = {
            "title": f"CostOptimizationHub Recommendation ",
            "fields": [
                {
                    "title": "アカウントID",
                    "value": item['accountId'],
                    "short": True
                },
                {
                    "title": "リージョン",
                    "value": item['region'],
                    "short": True
                },
                {
                    "title": "リソースタイプ",
                    "value": item['recommendedResourceType'],
                    "short": True
                },
                {
                    "title": "毎月の推定削減額",
                    "value": f"{item['estimatedMonthlySavings']} {item['currencyCode']}",
                    "short": True
                },
                {
                    "title": "推定削減率",
                    "value": f"{item['estimatedSavingsPercentage']}%",
                    "short": True
                },
                {
                    "title": "推定月額コスト",
                    "value": f"{item['estimatedMonthlyCost']} {item['currencyCode']}",
                    "short": True
                },
                {
                    "title": "実装作業",
                    "value": item['implementationEffort'],
                    "short": True
                },
                {
                    "title": "推奨リソースの概要",
                    "value": item['recommendedResourceSummary'],
                    "short": False
                },
            ],
            "color": "#36a64f" # 緑色
        }
        attachments.append(attachment)
    return attachments

いくつかポイントを抜粋して解説します。

  • リージョンはus-east-1
    • Cost Optimization Hubはグローバルリソースなので
  • SlackのWebhook URLは環境変数経由で設定
    • センシティブなパラメーターなので
  • bot3でCost Optimization Hubの推奨事項の一覧を取得
    • 推奨事項が空の場合は、終了
  • format_attachments関数で取得した推奨事項をリストから取り出し、Slack通知に適した形式に整形
  • SlackのメンションIDは環境変数経由で設定
    • メンション先の変更を容易にするため、ハードコーディングを避ける
  • Slack SDKを使って、推奨事項をSlackに通知

Lambda関数の作成

先ほど作成したLambdaレイヤーとPythonコードを含める形で、Lambda関数を作成します。

data "aws_iam_policy_document" "lambda" {
  statement {
    effect    = "Allow"
    actions   = ["cost-optimization-hub:ListRecommendations"]
    resources = ["*"]
  }
}

module "lambda_function" {
  # Module source
  source  = "terraform-aws-modules/lambda/aws"
  version = "7.8.1"
  # Module arguments
  ## Basic information
  function_name                           = "lambda-cost-optimization-hub-recommendations"
  description                             = ""
  package_type                            = "Zip"
  create_package                          = true
  recreate_missing_package                = false
  create_current_version_allowed_triggers = false
  runtime                                 = "python3.12"
  handler                                 = "lambda_function.lambda_handler"
  source_path                             = "/python/src"
  layers = [
    module.lambda_layer.lambda_layer_arn
  ]
  architectures             = ["arm64"]
  create_role               = true
  role_name                 = "lambda-role-cost-optimization-hub-recommendation"
  policy_name               = "lambda-policy-cost-optimization-hub-recommendation"
  attach_policy_json        = true
  policy_json               = data.aws_iam_policy_document.lambda.json
  ## Advanced Setting
  tags = {
    Name                   = "lambda-cost-optimization-hub-recommendations"
  }
  cloudwatch_logs_retention_in_days = var.lambda_logs_retention_in_days
  ## General Settings
  timeout = 60
  ## Permission
  # allowed_triggers = {}
  ## Environment Variables
  environment_variables = {
    SLACK_WEBHOOK_URL = var.slack_webhook_url
    SLACK_MENTION_ID  = var.notify_slack_mention_id
  }
}

EventBridgeの作成

先ほど作成したLambdaを定期的に実行するEventBridgeを作成します。
以下の例では、毎月1日の12時にLambdaが実行されます。

resource "aws_iam_role" "scheduler" {
  name               = "optimization-hub-recommendation-schedule-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role["scheduler"].json
  tags = {
    Name = "optimization-hub-recommendation-schedule-role"
  }
}

data "aws_iam_policy_document" "scheduler" {
  statement {
    effect    = "Allow"
    actions   = ["lambda:InvokeFunction"]
    resources = [module.lambda_function.lambda_function_arn]
  }
}

resource "aws_iam_policy" "scheduler" {
  name   = "optimization-hub-recommendation-schedule-policy"
  policy = data.aws_iam_policy_document.scheduler.json
  tags = {
    Name = "optimization-hub-recommendation-schedule-policy"
  }
}

resource "aws_iam_role_policy_attachment" "scheduler" {
  role       = aws_iam_role.scheduler.name
  policy_arn = aws_iam_policy.scheduler.arn
}

resource "aws_scheduler_schedule" "lambda" {
  # Resource arguments
  name        = "optimization-hub-recommendation-schedule"
  description = "Notify optimization hub recommendations"
  group_name  = "default"
  state       = "ENABLED"
  flexible_time_window {
    mode = "OFF"
  }
  schedule_expression          = "cron(0 12 1 * ? *)"
  schedule_expression_timezone = "Asia/Tokyo"
  target {
    arn      = module.lambda_function.lambda_function_arn
    role_arn = aws_iam_role.scheduler.arn
    retry_policy {
      maximum_event_age_in_seconds = "60"
      maximum_retry_attempts       = "3"
    }
  }
}

通知例

さいごに

Cost Optimization Hubの推奨事項の定期通知は、コスト削減対応の足掛かりになるかもなので、是非試して頂けたらと!

Discussion