話題の記事

AWS利用料金を毎日Slackに通知する仕組みをCDKで作りたくてやってみた

いろんな記事でよく見かける処理ですが、本件はCDK(Typescript)、Lambda(Python3)という組み合わせで作りました
2023.01.16

どーも、データアナリティクス事業本部コンサルティングチームのsutoです。

最近仕事が忙しくなると、AWSにて検証で作ったリソースを削除し忘れたことで余計な課金を発生させてしまうことが増えてきました。

自分の個人検証アカウントではAWS Budgetsを使って予算とアラートを設定していましたが、上限近くになってから気づくより毎日通知で気づくほうが良いと思ったので、今回はAWS CDKを使って作ってみました。

※CDKをTypescriptで書く練習をしたかったという思いもあり、CDKスタックはTypescript、中のLambdaはPythonという個人的趣向に沿った組み合わせとなっています。

作るもの

以下の図のとおりです。

毎日AM9時10分(JST)にAWS料金を特定のSlackチャンネルに通知します。

作業環境は以下となります。(Python、AWS CDKの環境はすでにインストール済みの状態です)

% sw_vers
ProductName:	macOS
ProductVersion:	12.6.2
BuildVersion:	21G320

% python --version
Python 3.9.13

% cdk --version
2.60.0 (build 2d40d77)

Slackの設定

  • LambdaによるSlack通知はIncoming Webhook URLを使いますのでまずはこちらを生成しておきます。

  • Slackアプリでワークスペースにサインインして通知先とするチャンネル名を控えた後、チャンネル一覧から「その他」→ 「App」をクリックします。

  • 右上の「App ディレクトリ」をクリックします。

  • ブラウザで以下の画面が表示されるので、右上の「ビルド」をクリックします。

  • 「Create an app」をクリックします。

  • 以下のポップアップが表示されたら、「From scratch」をクリックします。

  • App Name に任意の名前を入力します。
  • 今回使用するワークスペースを選択します。
  • 「Create App」をクリックします。

  • Basic Information の中から「Incoming Webhooks」をクリックします。

  • デフォルトでは右上のトグルが「Off」になっているので、クリックして「On」にします。

  • 「Add New Webhook to Workspace」をクリックし、通知先とするチャンネル名を入力して「許可する」をクリックします。

  • Webhook URL が払い出されるのでコピーしておきます。

CDKプロジェクト作成

  • プロジェクト用フォルダとCDKプロジェクトを作成します。
% mkdir cdk-billing-alarm && cd cdk-billing-alarm

% cdk init --language=typescript

% cdk ls                                                      
CdkBillingAlarmStack

Lambda Function作成

  • 「lambda」というフォルダを作成し、その配下のファイル「app.py」にコードを書いて保存します。
% mkdir lambda
# lambda/app.py

# encoding: utf-8
import json
import datetime
import requests
import boto3
import os
import logging

TODAY = datetime.datetime.utcnow()
BEGINING_OF_THE_MONTH = TODAY - datetime.timedelta(days=TODAY.day - 1)
START_DATE = BEGINING_OF_THE_MONTH.strftime('%Y/%m/%d').replace('/', '-')
END_DATE = TODAY.strftime('%Y/%m/%d').replace('/', '-')

SLACK_POST_URL = os.environ['SLACK_POST_URL']
SLACK_CHANNEL = os.environ['SLACK_CHANNEL']

logger = logging.getLogger()
logger.setLevel(logging.INFO)

client = boto3.client('ce')
sts = boto3.client('sts')
id_info = sts.get_caller_identity()

def get_total_cost():
    response = client.get_cost_and_usage(
        TimePeriod={
            'Start': START_DATE,
            'End': END_DATE
        },
        Granularity='MONTHLY',
        Metrics=[
            'UnblendedCost',
        ],
    )

    total_cost = response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"]
    return total_cost

def handler(event, context):
    text = "ID:{} の {}までのAWS合計料金 : ${}".format(id_info['Account'], END_DATE, get_total_cost())
    content = {"text": text}

    slack_message = {
        'channel': SLACK_CHANNEL,
        "attachments": [content],
    }

    try:
        requests.post(SLACK_POST_URL, data=json.dumps(slack_message))
    except requests.exceptions.RequestException as e:
        logger.error("Request failed: %s", e)

Lambda Layer作成

  • コード内のモジュール「requests」は外部モジュールなので、そのためのLambda Layerを作ります。

  • 以下のようにLambda Layer 用のディレクトリを作成し、その配下にpythonフォルダを作ってインポートします。

% mkdir lambda_layer  && cd lambda_layer

% mkdir python
% pip install -t python requests

CDKスタック作成

  • cdk-billing-alarm-stack.tsを編集します。
  • コード内の「SLACK_POST_URLの値」と「SLACK_CHANNELの値」を控えておいた情報に書き換えて保存します。
// lib/cdk-billing-alarm-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as events from "aws-cdk-lib/aws-events";
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as iam from 'aws-cdk-lib/aws-iam';


export class CdkBillingAlarmStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // lambda-layer
    const layer = new lambda.LayerVersion(this, 'MyLayer', {
      code: lambda.Code.fromAsset("lambda_layer"),
      compatibleRuntimes: [lambda.Runtime.PYTHON_3_9],
    });
    
    // lambda
    const sampleLambda = new lambda.Function(this, 'NptifyPriceHandler', {
      runtime: lambda.Runtime.PYTHON_3_9,    // execution environment
      code: lambda.Code.fromAsset('lambda'),  // code loaded from "lambda" directory
      handler: 'app.handler',                // file is "hello", function is "handler"
      environment: {
        TZ: 'Asia/Tokyo',
        SLACK_POST_URL: '生成したSlackのWebhook URL',
        SLACK_CHANNEL: '通知先のチャンネル名',
      },
      layers: [layer],
      initialPolicy: [new iam.PolicyStatement({
        actions: ['ce:GetCostAndUsage'],
        resources: ['*'],
      })],
    });

    // EventBridge
    new events.Rule(this, "sampleRule", {
      // JST で毎日 AM9:10 に定期実行
      // 参考 https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions
      schedule: events.Schedule.cron({minute: "10", hour: "0"}),
      targets: [new targets.LambdaFunction(sampleLambda, {retryAttempts: 3})],
  });
  }
}
  • 最終的にフォルダ構成はこのようなかたちとなります。

デプロイ

  • 初めてCDKデプロイをする方のみbootstrap を実行します。
% cdk bootstrap
  • 作成したスタックをデプロイします。
% cdk deploy CdkBillingAlarmStack
  • デプロイ完了後、Lambdaの動作を確認してみると、以下のようにSlackチャンネルに通知することができました。