LoginSignup
3
2

More than 3 years have passed since last update.

Lambda (Node.js 8) 等を Terraform で立ててサーバレスにAWSの課金額をSlackに自動通知する

Posted at

背景

AWS を使っていると、単純に忘れたり、認識していないリソースがあったり、リソースの落とし忘れで
いつの間にか課金されていることが多い。 :cry:
意識しなくても把握できるように自動通知したい。 :point_up:

現状、Cloud Watch のアラートは閾値をこえたときに発報されるルールであるなど制限が多い。 :pensive:
利用者からすると、予算のある値を超えたときでは遅くて、
金額の傾向を知りたいので金額ベースでなく日次でほしい。 :grinning:

関連

拙項ですが、GCP 版

AWS 版については、下記記事を参考にさせていただいた。 :bow:

概要

今回は Lambda をメインに使って、 AWS 課金額を日次で Slack に通知できるようにする。

Lambda を採用したのは、 AWS サービス内なので権限管理しやすく、
また無料枠もあってランニングコストを抑えられるからである。 :thumbsup:

さらに、 Terraform と GitHub を構成管理に使うことで、そもそも何のリソースが何に使われているか把握しやすくする。

使用した Lambda のソースコードは
https://github.com/iijimakazuyuki/AWSBillingReport
に、 Terraform のコンフィギュレーションは本文中に記載している。

環境の説明

AWS 内のリソース管理は、今回は Terraform を使う。

GUI や CLI ベースだと手続き的になるので、手順が煩雑になりやすいためである。
Terraform を使うと、宣言的にリソースを記述できる。

Terraform を使う上では、コンフィギュレーションファイルを GitHub で管理し、
Atlantis によるワークフローを適用する。

Atlantis については、今回は深く触れない。 :bow:
GitHub でプルリクエストを作ると Terraform を動かしてくれる人がいる、というイメージ。 :muscle:
その人は、 AWS の強い権限を持っている前提で話を進める。

今回使用するリソースは、次の図のようになる:

+-----------    A     W     S    ------------+
| +-------------------+                      |
| | CloudWatch Events |                      |
| +-------------------+                      |
|    |                                       |
| execute                                    |
|    | +----+                                |
|    | | S3 |                                |
|    | +----+                                |
|    |   ^                                   |
|    |  refer                                |
|    v   |                                   |
| +--------+          +--------------------+ |
| | Lambda |--query-->| CloudWatch Metrics | |
| +--------+          +--------------------+ |
+---------|----------------------------------+
         post          \
          v             \
      +-------+    +-----------------------+   +--------+
      | Slack |    | Terraform    Atlantis |---| GitHub |
      +-------+    +-----------------------+   +--------+
  • Lambda の日次実行には、広く知られている通り CloudWatch Events ルールを用いる。
  • Lambda へのデプロイは S3 を通じて行う。

ステップ0: 必要な環境を準備する

  1. Slack に Incoming Webhook を作る。
  2. GitHub に Terraform 用コンフィギュレーションリポジトリを作る。
  3. Terraform 用コンフィギュレーションをプッシュして、プルリクエストを作成する。

次のようなコンフィギュレーションをプッシュする:

data "aws_iam_policy_document" "billing_report_assume_role_policy" {
  // Lambda の AssumeRole のポリシー。
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "billing_report" {
  // Lambda のロール。
  name               = "billingReport"
  assume_role_policy = "${data.aws_iam_policy_document.billing_report_assume_role_policy.json}"
}

data "aws_iam_policy_document" "billing_report" {
  // Lambda のロールに与えるポリシー。
  statement {
    effect = "Allow"

    // 過不足ありそうだが
    // とりあえず CloudWatch の Read っぽいことと、
    // Lambda の実行ログを出力する権限を与えている。
    actions = [
      "cloudwatch:Describe*",
      "cloudwatch:Get*",
      "cloudwatch:List*",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = ["*"]
  }
}

resource "aws_iam_user" "billing_report_developer" {
  // 開発用に IAM ユーザを作成する。
  name = "billing_report_developer"
}

resource "aws_iam_user_policy" "billing_report_developer" {
  // 開発用 IAM ユーザに権限を付与する。
  name   = "billing_report_developer_policy"
  user   = "${aws_iam_user.billing_report_developer.name}"
  policy = "${data.aws_iam_policy_document.billing_report.json}"
}

resource "aws_iam_role_policy" "billing_report" {
  // Lambda のロールにポリシーを紐づけ。
  name   = "billing_report_policy"
  role   = "${aws_iam_role.billing_report.id}"
  policy = "${data.aws_iam_policy_document.billing_report.json}"
}

resource "aws_cloudwatch_event_rule" "billing_report" {
  // Lambda の日次実行のための CloudWatch Events ルール。
  name                = "billingReport"
  description         = "Send billing report daily"
  schedule_expression = "cron(0 0 * * ? *)"
}

Atlantis はプルリクエストを作成すると terraform plan を実行してくれ、
atlantis apply とコメントするとが実際に適用されて便利だが、
ともかく、 terraform apply まで実行できて、上記リソースが作成済みになった、とする。

Terraform で作成した IAM ユーザ billing_report_developer のアクセスキーが開発に必要なので、
IAM 管理画面 から作成し、メモする。

課金データのエクスポートは Terraform では設定できないようなので、 GUI から操作する。
参考:Amazon Web Services(AWS) で無料枠を超えた時にアラートを飛ばす(日本語メニュー)

ステップ1: ローカルで作ってみる

今回は私の好みで Node.js ランタイムを使用することにした。

まずはディレクトリを切って、

npm init

を実行し、適当に答えつつ Enter を連打する。
AWS SDK を使用するので、インストール:

npm install aws-sdk

このクライアントライブラリは、先ほどメモしたアクセスキーID、シークレットアクセスキーを環境変数で与えれば認証が通る。

export AWS_ACCESS_KEY_ID=<access_key_id>
export AWS_SECRET_ACCESS_KEY=<secret_access_key>

Lambda では、実行する関数を指定する。
開発中はテストを書いて実行するのがよいだろうが、今回は省略する。
Node.js の REPL を立ち上げて、下記のように実行して動作確認はできる。

> const index = require('./index.js');
undefined
> index.handler();
Promise {
  <pending>,
  domain:
   Domain {
     domain: null,
     _events: { error: [Function: debugDomainError] },
     _eventsCount: 1,
     _maxListeners: undefined,
     members: [] } }
>

ステップ2: デプロイする

ZIP で固めて S3 にアップロードする。
AWS SDK 以外を使用する場合、 node_modules 内も固める必要があるが、今回は不要なので、次のようにアップロードする。
なお開発用の IAM ユーザは権限が足りない。
アップロード可能な IAM ユーザを使うよう、環境変数を指定しなおす必要があることに注意。

$ zip v1.0.0.zip index.js
  adding: index.js (deflated 61%)
$ aws s3 cp v1.0.0.zip s3://<bucket_name>/v1.0.0.zip

次に、 Terraform 用コンフィギュレーションをプッシュして、プルリクエストを作成する。

次のようなコンフィギュレーションを追加してプッシュする:

resource "aws_lambda_function" "billing_report" {
  // Lambda 関数を ZIP で固めて S3 にアップロードしたものを参照させてデプロイする。
  s3_bucket     = "<bucket_name>"
  s3_key        = "v1.0.0.zip"
  function_name = "billing_report"
  role          = "${aws_iam_role.billing_report.arn}"
  handler       = "index.handler"
  runtime       = "nodejs8.10"

  environment {
    variables = {
      // Webhook URL を環境変数で与える。
      WEBHOOK_URL = "<webhook_url>"
    }
  }
}

resource "aws_cloudwatch_event_target" "billing_report" {
  // 日次実行の CloudWatch Events ルールで Lambda を実行する。
  target_id = "billingReport"
  arn       = "${aws_lambda_function.billing_report.arn}"
  rule      = "${aws_cloudwatch_event_rule.billing_report.name}"
}

resource "aws_lambda_permission" "billing_report" {
  // 日次実行の CloudWatch Events ルールで Lambda を実行させるのに必要なパーミッションを作成する。
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.billing_report.function_name}"
  principal     = "events.amazonaws.com"
  source_arn    = "${aws_cloudwatch_event_rule.billing_report.arn}"
}

これで、CloudWatch Events ルールが実行される毎日朝9時に Slack に次のようなメッセージが投稿される。

2019/4/30 0.39

CloudWatch Events ルールを手動で実行させることはできないようだが、
Lambda から適当なテストを実行すれば、その時点でメッセージを投稿させられる。

ステップ3: 機能改善

機能を追加したいとか、メッセージをもうちょっと丁寧にしたいとかで、アップデートしたい場合は、次のような手順を踏む。

  1. ソースコードを ZIP に固める。
  2. ZIP ファイルを S3 にアップロードする。バージョン名をつけるとわかりやすい。
  3. Terraform 用コンフィギュレーションをプッシュして、プルリクエストを作成する。

下記 <version> を更新して、 apply すればよい。

resource "aws_lambda_function" "billing_report" {
  // Lambda 関数を ZIP で固めて S3 にアップロードしたものを参照させてデプロイする。
  s3_bucket     = "<bucket_name>"
  s3_key        = "v<version>.zip"

  // (略)

まとめ

AWS の課金額を意識せずとも把握できるよう、日次で Slack に通知されるようにした。

  • その際、広く用いられている構成ではあるが、 Lambda を中心としたサーバレス構成とすることで、通知システム自体のランニングコストを抑えた。
  • Terraform (+ Atlantis) と GitHub を構成管理に使うことで、システムに使用しているリソースを把握しやすくした。

付録:躓いたところ

  • 何のリソースを作ればサービス間が連携できるのかが直ちには分からないし、今後どうしたらよいかもよく分かっていない。 Lambda パーミッションを作り忘れていることに気づかず、 Lambda 関数の GUI (Designer) に CloudWatch Events が表示されないなとしばらく悩んでいた。
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2