AWS CDKに入門し、AWS費用をSlack通知する処理を書き直した

かなり昔にAWS費用をSlack通知する処理を組んでいたのですが、いつの間にか止めており、最近一応入れておきたくなったのでこれを機会にAWS CDKに入門しました。

何度かServerless Frameworkを使ってLambdaとS3と何かしらを組み合わせるような構成は作っていたので、そういう感じかなと思っていましたが、JavaScriptで書くというのもあってCDKの方が分かりやすいなと思いました。

導入

ドキュメントに従い、インストール。ブートストラップも実行しておきます。

$ npm install -g aws-cdk

$ cdk bootstrap aws://ACCOUNT-NUMBER/REGION

プロジェクトの生成

$ cdk init app --language typescript

TypeScript 指定できるのはうれしいですね。

スタックの構築

全部書くと長いので一部のみ

import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

const checkCost = new NodejsFunction(this, 'checkAWSCost', {
      entry: 'lib/lambda/check-cost.ts',
      handler: 'handler',
      runtime: Runtime.NODEJS_18_X,
      timeout: cdk.Duration.seconds(30),

      bundling: {
        forceDockerBundling: false, // use local esbuild
      },
      initialPolicy: [
        new cdk.aws_iam.PolicyStatement({
          actions: ['ce:GetCostAndUsage'],
          resources: ['*']
        })
      ]
    });

AWS CDK v2からは aws-cdk-lib/aws-lambda-nodejs を使うようになったみたいです。なんとなく探しやすい気がします。

NodejsFunction を使うことで、TypeScriptのコードもデプロイ時にコンパイルできるようです。ただ、デフォルトだとDockerの専用イメージでのビルドになるようで、そこまで大掛かりにはしたくないのでオプションでその動作を止め、別途esbuildを入れてそちらを使うようにしています。

// スケジュールの設定
new cdk.aws_events.Rule(this, "cost check handler schedule", {
      // JST で月曜 AM9:00 に定期実行
      // see https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions
      ruleName: 'weekly-cost-check',
      schedule: cdk.aws_events.Schedule.cron({minute: "0", hour: "0", weekDay: 'mon'}),
      targets: [new cdk.aws_events_targets.LambdaFunction(checkCost, { retryAttempts: 2 })],
    });

スケジュール実行はCloudwatch Scheduleを使うのが推奨されているようですが、書き方がイマイチ分からなかったので、一旦Ruleで書いています。

scheduleパラメーターはcron記法で書くとどうにも分かりづらいんですが、Scheduleのcron関数で書くとだいぶ分かりやすくて良いですね。

Lambda関数の実装

LambdaからはCost Explorerから価格情報を取得し、フォーマットを加工した上でSlackに投げています。

Cost Explorer

Cost Explorerからはトータルとサービス別で取得しています。

import { GetCostAndUsageCommand } from "@aws-sdk/client-cost-explorer";

export function totalCostCommand(period: { start: string; end: string }) {
  return new GetCostAndUsageCommand({
    TimePeriod: {
      Start: period.start,
      End: period.end,
    },
    Granularity: 'MONTHLY',
    Metrics: [
      'UnblendedCost'
    ],
  });
}

export function monthlyCostByServiceCommand(period: { start: string; end: string }) {
  return  new GetCostAndUsageCommand({
    TimePeriod: {
      Start: period.start,
      End: period.end,
    },
    Granularity: 'MONTHLY',
    Metrics: [
      'UnblendedCost'
    ],
    GroupBy: [
      {
        'Type': 'DIMENSION',
        'Key': 'SERVICE'
      }
    ]
  });
}

このコマンドをそれぞれ投げて取得しています。

const command = monthlyCostByServiceCommand(period)
const costs = await CostExplorer.send(command);

Slack通知

@slack/web-apiを使うのがいいかなと思ったのですが、どうにも上手く動かず、結局 chat.postMessageへのPOSTリクエストになりました。

メモ
途中API GatewayではなくLambdaの関数URLをSlackのEvent Subscriptionで登録する方法を試みましたが、verify後もスケジューラーで実行した際に上手く動かなかったりで原因が掴めなかった。
AWS Lambda で Slack Bot イベントハンドラを作る - げっとシステムログ

やってみて

Slackへの通知でドハマりしたんですが、それ以外は割りとすんなりいけました。
CDKで書いてみて、そこまで変わったことをしていないというのはあると思いますが、やはり書きやすいというところと、各種サービス周りが @aws-cdk-lib/ 辺りにあるので探しやすかったり扱いやすいのがいいですね。CDK v1もあると思うので注意しないとですが…

また、構成をコードで管理できるので書き慣れると同じようなものをさっと作って構築できるようになるんだろうなと思うとだいぶ便利そうだなと思いました。

その他参考にしたもの