1.この記事を書こうと思った背景
- AWSの利用料金が想定以上に高くてびっくりしたことがあった
- AWSの利用料金が想定より高くなったら検知するようにしようと決意したい
- ↓こういったことが起きないためにも、、
- AWSでやらかして3桁万円請求された話 - Qiita
- 無事できたので、この記事でどうやったか?またいくつか検知する方法があるなかでどういった選定基準を持ったか?書いていきたい
※ サンプルコードはこちら
https://github.com/gkzz/serverless-aws-budget-alerts-to-slack
2.前提
AWSの利用料金が想定より高くなったら検知する方法は2022/02/24時点で3種類あり、この記事ではbudget alertsを使った方法を採用している。ただし、各サービスの仕様変更やアップデートなどにより今後は他2つの方法を採用した方がいいということも。
3.環境情報
- Serverless Frameworkのバージョンは3.2
$ grep VERSION= /etc/os-release
VERSION="20.04.3 LTS (Focal Fossa)"
$ sls --version
Framework Core: 3.2.0
Plugin: 6.0.0
SDK: 4.3.1
$ npm --version
6.14.16
$ nodejs --version
v10.19.0
- Serverless Frameworkでは環境変数としてAWSのRegionとSlackのincoming webhook urlを使っている
- 環境変数の使い方や適用方法については以前書いたのでそちらを参照してほしい
# .env
# .envはserverless.ymlと同じディレクトリに配置
# AWS
REGION="***"
# slack incoming webhook
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/***"
- serverless.ymlを配置したディレクトリをルートディレクトリとみたときのディレクトリ構造
- serverless.ymlにLambdaの実行ファイルのパスなどを書くので、treeコマンドの結果を貼っておく
- treeコマンドの結果からnode_modulesディレクトリは見やすさを考慮して除外した
$ tree -L 3 -I node_modules
.
## Lambdaの実行環境でnpmパッケージを使うことが出来るようにLambda Layersを使っている
# ※ serverless.ymlでLambda Layersをどうやってdeployするのか?については後述
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html
├── lambdaLayer
│ └── nodejs
│ ├── package.json
│ └── package-lock.json
├── LICENSE
├── package.json
├── package-lock.json
├── README.md
├── serverless.yml ## serverless deployコマンドでdeployするリソースなどを定義
└── src
└── slack-notify.js ## Lambdaの実行ファイル
3 directories, 8 files
$ find ./ -type d -name node_modules | grep -Ev "./node_modules/.+"
./node_modules ## Serverless Frameworkの実行環境であるローカルで使う
./lambdaLayer/nodejs/node_modules ## Lambdaの実行環境で使う
4.AWSの利用料金が想定より高くなったら検知する3つの方法
冒頭でお話ししたとおり、AWSの利用料金が想定より高くなったら検知する方法は、今回採用するbudget alerts以外に2種類ある。billing alertsとAWS Cost Anomaly Detectionだ。
- budget alerts
- billing alerts
- AWS Cost Anomaly Detection
それではbudget alertsを採用した理由はなにか?また、残り2つのサービスの採用をどうして見送ったか?こちらについて書いていこう。
5.選定理由
budget alertsを使って検知することにした理由
- アラートを飛ばすかどうか判断するための閾値はコストの実測値だけではなくその予測値も指定することが出来る
billing alertsの採用を見送った理由
- アラートを飛ばすかどうか判断するための閾値はコストの実測値だけしか指定できない(予測値を指定することができない)
- アカウント単位でしか設定できない
AWS Cost Anomaly Detectionの採用を見送った理由
- TerraformやServerless Frameworkの管理下とすることができない
- 検知の仕組みがブラックボックスなので「検知してほしい」ときに検知してくれないことがあった
- 検知してまた検知するようにする設定が不明(一度削除して再作成するしかない、、?)
以下のドキュメントや記事を踏まえてを選んだ
- amazon web services - What’s the difference between Alarm Budget and Cloudwatch alarms Billing? - Stack Overflow
- What is the difference between Billing Alarms and Budgets? : aws
- AWSの料金アラートの違い
6.どうやるか?
6-1.budget alertsを使ったAWSの利用料金が想定より高くなったら検知する仕組み(全体像)
- budget alertsでコスト予算と閾値(Budget thresholds)を設定
- 閾値を超えたらAmazon SNSからメッセージが発行される
- メッセージが発行されるとLambdaがSlackへincoming webhook urlを使ってメッセージを送信
6-2.serverless.ymlの書き方ポイント
Serverless Frameworkは内部的にはCloudFormationのスタックを作成しているのでServerless Frameworkでの書きっぷりに困ったら、Cfnのドキュメントや技術ブログを読むとヒントがあるかも!?
たとえば、、。
- AWS::Budgets::Budget Notification - AWS CloudFormation
- CloudFormationでAWS Budgets の予算超過アラートを Slack へ通知してみた | DevelopersIO
- Create AWS billing alerts with the Serverless framework. | Medium
Every stage you deploy to with serverless.yml using the aws provider is a single AWS CloudFormation stack.
出所: Serverless Framework - AWS Lambda Guide - AWS Infrastructure Resources
- serverless.yml(providerブロックとfunctionsブロックの書き方など)
service: src
frameworkVersion: '3'
useDotenv: true
provider:
name: aws
region: ${env:REGION}
runtime: nodejs14.x
memorySize: 128
timeout: 300
# Lambdaの実行環境でnpmパッケージを使うことが出来るようにpackage.jsonなどを転送
# 転送しているファイル
# $ ls lambdaLayer/nodejs/
# node_modules package.json package-lock.json
#
#
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html
#
layers:
layerName:
path: lambdaLayer
description: npm package layers
plugins:
- serverless-dotenv-plugin
functions:
slack-notify:
# Lambdaのエントリーポイント
handler: src/slack-notify.handler
name: slack-notify
events:
- sns:
arn: !Ref snsTopic
topicName: snsTopic
environment:
TZ: Asia/Tokyo
SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL}
layers:
# Refでlayersでデプロイしているnpmパッケージを参照するようにし、Lambdaの実行環境にpackage.jsonとpackage-lock.jsonを配置する
- { Ref: LayerNameLambdaLayer }
resources:
略
snsTopic:
略
snsTopicPolicy:
略
Outputs:
略
- serverless.yml(resourcesブロックとOutputsブロックの書き方。AWS BudgetsとAmazon SNSなどの設定をする。)
service: src
frameworkVersion: '3'
useDotenv: true
provider:
略
layers:
略
plugins:
略
functions:
略
resources:
Description: "Set billing/budget alerts for the AWS account."
Resources:
# GUIでは https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-create.html#create-cost-budget
dailyBudget:
Type: "AWS::Budgets::Budget"
Properties:
# https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-budgets-budget-budgetdata.html
Budget:
BudgetName: Daily
BudgetType: COST
# 今回は日次で予算管理したいので。他に月次、四半期、年次も指定可能。
# 月次はMONTHLY、 四半期はQUARTERLY、年次ANNUALLY
TimeUnit: DAILY
BudgetLimit:
# "TimeUnit"で指定したスパンで管理したい予算金額。
# なお、ここでのアラートの閾値は"Amount"のN%といった具合に決めていく
Amount: 20
Unit: USD
# https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-budgets-budget-notification.html
NotificationsWithSubscribers:
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-budgets-budget-notification.html
- Notification:
NotificationType: ACTUAL # FORECASTEDも指定可能
ComparisonOperator: GREATER_THAN
Threshold: 50 # "Amount"の50%に到達したら通知
ThresholdType: PERCENTAGE
Subscribers:
- SubscriptionType: SNS
Address: { Ref: snsTopic }
- Notification:
NotificationType: ACTUAL
ComparisonOperator: GREATER_THAN # 他にEQUAL_TOとLESS_THANも指定可能
Threshold: 90 # "Amount"の90%に到達したら通知
ThresholdType: PERCENTAGE # 他にABSOLUTE_VALUE、絶対値も指定可能
Subscribers:
- SubscriptionType: SNS
Address: { Ref: snsTopic }
# "NotificationsWithSubscribers"で作成したアラートをAmazon SNSで受け取る
snsTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: aws-budget-alert-to-slack-topic
TopicName: aws-budget-alert-to-slack-topic
# "snsTopic"を定義するだけでは以下のエラーを引いてしまう。"snsTopicPolicy"も必要!
# How to fix ERORR "Your budget must have permissions to send a notification to your topic " on AWS
# https://stackoverflow.com/questions/70021257/how-to-fix-erorr-your-budget-must-have-permissions-to-send-a-notification-to-yo
snsTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
PolicyDocument:
{
"Version": "2008-10-17",
"Id": "__default_policy_ID",
"Statement": [
{
"Sid": "AWSBudgetsSNSPublishingPermissions",
"Effect": "Allow",
"Principal": {
"Service": "budgets.amazonaws.com"
},
"Action": "SNS:Publish",
# Fn::Subは変数と文字列を結合するために使っている。ここではRegionとAccountId
# https://dev.classmethod.jp/articles/cloud%E2%80%8Bformation-intrinsic-function-memorandum/
# RegionとAccountIdはAWS CloudFormation によって事前定義されたパラメータ、Pseudo parameters(疑似パラメータ)であり、AWS::Regionなどと使う
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html
"Resource": !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:aws-budget-alerts-to-slack-topic"
},
{
"Sid": "__default_statement_ID",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"SNS:GetTopicAttributes",
"SNS:SetTopicAttributes",
"SNS:AddPermission",
"SNS:RemovePermission",
"SNS:DeleteTopic",
"SNS:Subscribe",
"SNS:ListSubscriptionsByTopic",
"SNS:Publish",
"SNS:Receive"
],
"Resource": !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:aws-budget-alerts-to-slack-topic",
"Condition": {
"StringEquals": {
"AWS:SourceOwner": !Ref "AWS::AccountId"
}
}
}
]
}
Topics:
- { Ref: snsTopic }
# sls deployが完了してから出力したいもの
Outputs:
DailyBudgetId:
Value:
Ref: dailyBudget
snsTopicArn:
Description: "ARN of SNS Topic"
Value:
Ref: snsTopic
上で書いた、serverless.ymlでデプロイする"functions"(Lambda)の実体である、src/slack-notify.jsについては、書きっぷりはご自身のニーズによって変わってしまうので参考までに。
- request-promise-nativeパッケージを使って、メッセージの送信を同期処理のように書くようにした
const request = require('request-promise-native');
const sendMessage = (message) => {
return request({
method: 'POST',
url: process.env.SLACK_WEBHOOK_URL,
body: message,
json: true,
})
.then((body) => {
if (body === 'ok') {
return {};
} else {
throw new Error(body);
}
});
};
const processRecord = (record) => {
console.log(`record: ${JSON.stringify(record)}`);
return sendMessage({
// @here AWS Budgets: Daily has exceeded your alert threshold
text: '<!here> ' + record.Sns.Subject,
attachments: [{
fields: [{
title: 'Type',
value: record.Sns.Type,
short: true,
}, {
title: 'Time',
value: record.Sns.Timestamp,
short: true,
}, {
title: 'MessageId',
value: record.Sns.MessageId,
short: false,
}, {
title: 'Message',
value: record.Sns.Message,
short: false,
}],
}],
});
};
/*
example event:
{
"EventSource": "aws:sns",
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:<REGION>:<xxxxxxxxx>:src-dev-snsTopic-<xxxxxxxxx>:<xxxxxxxxx>",
"Sns": {
"Type": "Notification",
"MessageId": "<xxxxxxxxx>",
"TopicArn": "arn:aws:sns:<REGION>:<xxxxxxxxx>:src-dev-snsTopic-<xxxxxxxxx>",
"Subject": "<SUBJECT>",
"Message": "<MESSAGE>",
"Timestamp": "2022-02-17T17:33:07.585Z",
"SignatureVersion": "1",
"Signature": "<xxxxxxxxx>",
"SigningCertUrl": "<xxxxxxxxx>",
"UnsubscribeUrl": "<xxxxxxxxx>",
"MessageAttributes": {}
}
}
*/
module.exports.handler = (event, context, callback) => {
console.log(`event received: ${JSON.stringify(event)}`);
Promise.all(event.Records.map(processRecord))
.then(() => callback(null))
.catch((err) => callback(err));
};
あとはdeployするだけ!!
$ sls deploy
デバッグしたいときは --verbose オプションを付ける
$ sls deploy --verbose
リソースを削除する場合
$ sls remove
参考
- budget alerts
- billing alerts
- AWS Cost Anomaly Detection
- AWSの料金アラートの違い
- budget alertsの書き方、オプションの使い方
- Fn::Subの使い方
- Pseudo parameters(疑似パラメータ)の使い方
- budget alertsをCloudformationで設定する方法
- Lambda layersの使い方
- Lambdaで使っているrequest-promise-nativeパッケージ