AWS CDKでAWS Budgetsの請求アラートとAWS コスト異常検出(Cost Anomaly Detection)アラートを設定する

2022.07.07

「請求アラートとコスト異常検出アラート設定を自動化したい」

請求アラートとコスト異常検出アラートを設定する機会がたまにあります。

やり方忘れて調べたり、手動でポチポチするのが大変と感じていました。

今後楽するために、AWS CDKで設定してみました。

下記が参考になりました。

作成するアラート

以下3つのアラートを作成します。

  • AWS Budgets 予算アラート(実績)
  • AWS Budgets 予算アラート(予測)
  • AWSコスト異常検出(Cost Anomaly Detection)アラート

1つ目は、AWS Budgets 予算アラート(実績) です。 実際のコストが閾値を超えたらアラートを飛ばします。

「これだけでいいじゃん」と思うかもしれませんが、他2つも合わせて設定しておくことでより早くコスト増を検知できます。

2つ目のAWS Budgets 予算アラート(予測)を過去のデータから予測したコストに対してアラートを設定できます。 (予測のためには、過去5週間分の使用量データが必要です)

3つ目は、AWSコスト異常検出(Cost Anomaly Detection)アラートです。機械学習を使用して異常なコストを検出することができます。

このアラートを設定することで、予算内でも突発的に金額が上がった際に検知することができます。

各アラートに関しては、AWS ChatBotを使用してSlackに通知します。

やってみた

事前準備 AWS Chatbot Slackワークスペースへの追加(既に実施済みの場合は不要)

CDK上でできないためマネジメントコンソールでの操作が必要になります。(2022年7月時点)

以下の記事の「AWS Chatbot の Workspace Configuration を GUI から作成」の手順を実施します。

コード

コードは以下になります。

msato0731/aws-cost-alarm

エントリポイント

AWSアカウント毎に変えたい値は、main.tsで渡すようにしています。 STG環境予算:100USD、開発環境予算:50USDなど環境毎に予算が異なることがあるかと思います。

コスト異常検出のしきい値に関しても、予算額次第で異常とみなす金額を変える必要があります。

予算額100USDでしきい値1USDにするのと、1,000USDで1USDにするのは誤検知の発生率が変わってくるはずです。

コスト異常検出のしきい値は、予算額の何割とかに設定するのも良いかもしれません。(設定値としては、現時点ではUSD単位のみ設定可能です。)

src/main.ts

import { App } from 'aws-cdk-lib';
import { BillingAlarmStack } from './billing_alarm_stack';
import 'dotenv/config';

const env = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: 'us-east-1', # コスト異常検出の関係でus-east-1にする必要あり
};


const app = new App();

new BillingAlarmStack(app, 'billing-alarm', {
  env,
  slackWorkspaceId: process.env.SLACK_WORKSPACE_ID!,
  slackChannelConfigurationName: process.env.SLACK_CHANNEL_CONFIGURATION_NAME!,
  slackChannelId: process.env.SLACK_CHANNEL_ID!,
  budgetLimitAmountUsd: 50, # 予算額 しきい値80%でアラート通知
  costAnomaryThresholdUsd: 1, # コスト異常検出のしきい値
});

app.synth();

アラームスタック

リソース設定を定義している部分です。

各種アラームと通知用のSNSやChatBotを作成しています。

Budgetsのしきい値に関しては、とりあえず予算額の80%としています。

src/billing_alarm_stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import * as budgets from 'aws-cdk-lib/aws-budgets';
import * as ce from 'aws-cdk-lib/aws-ce';
import * as chatbot from 'aws-cdk-lib/aws-chatbot';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';

export interface BillingAlarmStackProps extends StackProps {
  slackChannelConfigurationName: string;
  slackWorkspaceId: string;
  slackChannelId: string;
  budgetLimitAmountUsd: number;
  costAnomaryThresholdUsd: number;
}

export class BillingAlarmStack extends Stack {
  constructor(scope: Construct, id: string, props: BillingAlarmStackProps ) {
    super(scope, id, props);
    /**
     * SNS
     */
    const topic = new sns.Topic(this, 'BillingAlarmTopic');

    /**
     * Chatbot
     */
    const slackChannel = new chatbot.SlackChannelConfiguration(this, 'SlackChannel', {
      slackWorkspaceId: props.slackWorkspaceId,
      slackChannelConfigurationName: props.slackChannelConfigurationName,
      slackChannelId: props.slackChannelId,
      loggingLevel: chatbot.LoggingLevel.ERROR,
    });

    slackChannel.addNotificationTopic(topic);

    /**
     * Budgets
     */
    new budgets.CfnBudget(this, 'CfnBudgetCost', {
      budget: {
        budgetType: 'COST',
        timeUnit: 'MONTHLY',
        budgetLimit: {
          amount: props.budgetLimitAmountUsd,
          unit: 'USD',
        },
      },
      notificationsWithSubscribers: [
        {
          notification: {
            comparisonOperator: 'GREATER_THAN',
            notificationType: 'ACTUAL',
            threshold: 80,
            thresholdType: 'PERCENTAGE',
          },
          subscribers: [{
            subscriptionType: 'SNS',
            address: topic.topicArn,
          }],
        },
        {
          notification: {
            comparisonOperator: 'GREATER_THAN',
            notificationType: 'FORECASTED',
            threshold: 80,
            thresholdType: 'PERCENTAGE',
          },
          subscribers: [{
            subscriptionType: 'SNS',
            address: topic.topicArn,
          }],
        },
      ],
    });

    /**
     * Cost Anomaly Detection
     */
    const cfnAnomalyMonitor = new ce.CfnAnomalyMonitor(this, 'CfnAnomalyMonitor', {
      monitorName: 'AWS_Services-Recommended',
      monitorType: 'DIMENSIONAL',
      monitorDimension: 'SERVICE',

    });
    new ce.CfnAnomalySubscription(this, 'CfnAnomalySubscription', {
      frequency: 'IMMEDIATE',
      monitorArnList: [cfnAnomalyMonitor.attrMonitorArn],
      subscribers: [{
        address: topic.topicArn,
        type: 'SNS',
      }],
      subscriptionName: 'AWS_Services-Recommended-AlertSubscription',
      threshold: props.costAnomaryThresholdUsd,
    });
  }
}

環境変数

GithubをPublicにしたかったため、Slackの情報は.envに入れてます。

Privateリポジトリだったらcdk.jsonとかに書いてGit管理に含めてもいいかと思います。

.env

SLACK_WORKSPACE_ID=T8XXXXXXX
SLACK_CHANNEL_CONFIGURATION_NAME=chYYYYYYY
SLACK_CHANNEL_ID= C01ZZZZZZZ

ハマったところ

環境構築時に以下のエラーが出ました。

Template format error: Unrecognized resource types: [AWS::CE::AnomalyMonitor]

エラー内容で、調べたところus-east-1以外のリージョンを使用していからでした。

AWS::CE::AnomalyMonitorus-east-1で作成する必要があるみたいです。

おわりに

AWSコスト関連アラートのざっくりとした紹介と、AWS CDKを使った設定方法についてでした。

AWS CDKを使用することで、短い記述で簡単にリソース作成ができました。

BudgetsCost Anomaly Detectionに関しては、現時点ではL2 Constructのものためコントリビュートチャンスですね。

今回紹介したコードでは、Cost Anomaly Detectionの関係でus-east-1に全てのリソースを作成しています。

もし、SNSトピック等を別リージョンで作成したい場合はスタックの分割が必要です。

以上、AWS事業本部の佐藤(@chari7311)でした。