背景
AWS を使っていると、単純に忘れたり、認識していないリソースがあったり、リソースの落とし忘れで
いつの間にか課金されていることが多い。
意識しなくても把握できるように自動通知したい。
現状、Cloud Watch のアラートは閾値をこえたときに発報されるルールであるなど制限が多い。
利用者からすると、予算のある値を超えたときでは遅くて、
金額の傾向を知りたいので金額ベースでなく日次でほしい。
関連
拙項ですが、GCP 版。
AWS 版については、下記記事を参考にさせていただいた。
- LambdaでAWSの料金を毎日Slackに通知する(Python3)
- AWSの料金をLambdaのcronで定期チェックしてSlackに通知する
- Amazon Web Services(AWS) で無料枠を超えた時にアラートを飛ばす(日本語メニュー)
概要
今回は Lambda をメインに使って、 AWS 課金額を日次で Slack に通知できるようにする。
Lambda を採用したのは、 AWS サービス内なので権限管理しやすく、
また無料枠もあってランニングコストを抑えられるからである。
さらに、 Terraform と GitHub を構成管理に使うことで、そもそも何のリソースが何に使われているか把握しやすくする。
使用した Lambda のソースコードは
https://github.com/iijimakazuyuki/AWSBillingReport
に、 Terraform のコンフィギュレーションは本文中に記載している。
環境の説明
AWS 内のリソース管理は、今回は Terraform を使う。
GUI や CLI ベースだと手続き的になるので、手順が煩雑になりやすいためである。
Terraform を使うと、宣言的にリソースを記述できる。
Terraform を使う上では、コンフィギュレーションファイルを GitHub で管理し、
Atlantis によるワークフローを適用する。
Atlantis については、今回は深く触れない。
GitHub でプルリクエストを作ると Terraform を動かしてくれる人がいる、というイメージ。
その人は、 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: 必要な環境を準備する
- Slack に Incoming Webhook を作る。
- GitHub に Terraform 用コンフィギュレーションリポジトリを作る。
- 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: 機能改善
機能を追加したいとか、メッセージをもうちょっと丁寧にしたいとかで、アップデートしたい場合は、次のような手順を踏む。
- ソースコードを ZIP に固める。
- ZIP ファイルを S3 にアップロードする。バージョン名をつけるとわかりやすい。
- 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 が表示されないなとしばらく悩んでいた。