awsの料金の通知機能が標準にもあるが、使いづらいため作ってみた。
使いづらい点としては、
- コストの閾値設定
- 通知が合計
- メッセージのカスタマイズができない
標準の通知系はメッセージで欲しいものがあまりないのが困りますね。
今回は、lambda + golangで作成
料金取得
今回は月額取得したいためMONTHLYを指定
type CostExplorer struct {
session costexploreriface.CostExplorerAPI
}
func NewCostExplorer(session costexploreriface.CostExplorerAPI) CostExplorer {
return CostExplorer{
session: session,
}
}
func (c CostExplorer) GetCostForDaily(time_start string, time_end string, metrics []string) (*costexplorer.GetCostAndUsageOutput, error) {
granularity := aws.String("MONTHLY")
metric := aws.StringSlice(metrics)
resp, err := c.session.GetCostAndUsage(
&costexplorer.GetCostAndUsageInput{
Metrics: metric,
Granularity: granularity,
TimePeriod: &costexplorer.DateInterval{Start: aws.String(time_start), End: aws.String(time_end)},
})
if err != nil {
return nil, err
}
return resp, nil
}
func (c CostExplorer) GetCostDetail(time_start string, time_end string, metrics []string) (*costexplorer.GetCostAndUsageOutput, error) {
granularity := aws.String("MONTHLY")
metric := aws.StringSlice(metrics)
group := costexplorer.GroupDefinition{Key: aws.String("SERVICE"), Type: aws.String("DIMENSION")}
resp, err := c.session.GetCostAndUsage(
&costexplorer.GetCostAndUsageInput{
GroupBy: []*costexplorer.GroupDefinition{&group},
Metrics: metric,
Granularity: granularity,
TimePeriod: &costexplorer.DateInterval{
Start: aws.String(time_start),
End: aws.String(time_end)},
})
if err != nil {
return nil, err
}
return resp, nil
}
// Handler is our lambda handler invoked by the `lambda.Start` function call
func Handler() (string, error) {
svc := costexplorer.New(session.Must(session.NewSession()))
logic := NewCostExplorer(svc)
// TimePeriod
// 現在時刻の取得
jst, _ := time.LoadLocation("Asia/Tokyo")
now := time.Now().UTC().In(jst)
startDate := now.AddDate(0, -1, 0)
endDate := getLastDate(startDate.Year(), int(startDate.Month()))
startDateStr := startDate.Format("2006-01") + "-01"
endDateStr := endDate.Format("2006-01-02")
_ , err := logic.GetCostDetail(startDateStr, endDateStr, []string{"UnblendedCost"})
if err != nil {
log.Fatal(err)
}
return "", nil
}
取得した内容を通知用に文字列に変換
func (c CostExplorer) message(response *costexplorer.GetCostAndUsageOutput) ([]string, error) {
if len(response.ResultsByTime) == 0 {
return []string{}, nil
}
ret := []string{}
total := 0.0
ret = append(ret, config.ServiceName)
for _, v := range response.ResultsByTime[0].Groups {
msg := *v.Keys[0] + " " + *v.Metrics["UnblendedCost"].Amount
ret = append(ret, msg)
amount, _ := strconv.ParseFloat(*v.Metrics["UnblendedCost"].Amount, 64)
total += amount
}
ret = append(ret, fmt.Sprintf("Total: %f",total))
return ret, nil
}
アクセス権限付与
lambdaの実行ロールに「GetCostAndUsage」の権限を付与する
参考
権限がない場合、 「 is not authorized to perform」エラーが発生する
2021/06/28 16:20:58 AccessDeniedException: User: arn:aws:sts::***:assumed-role/awspricenotification-dev-ap-northeast-1-lambdaRole/awspricenotification-dev-billing is not authorized to perform: ce:GetCostAndUsage on resource: arn:aws:ce:us-east-1:***:/GetCostAndUsage
結果

Serverless Framework使用してdeploy
cronで日時指定で実行したい。
serverless.ymlのeventsを変更
functions:
billing:
handler: bin/billing
events:
- schedule: cron(0 0 1 * ? *)
cronの?の部分はaws eventsの仕様
The * (asterisk) wildcard includes all values in the field. In the Hours field, * would include every hour. You cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other.
曜日と日付の両方の設定ができないらしい。
サーバーがなくてもcronで動かせるの便利。
まとめ
料金の通知は前からやりたかったので、今回作れて良かった。
最近canaryで予想以上の料金が発生してしまったので、かなり凹んでいたところ作りたい欲が高まりました。
以前はcost exploreがなかったから作れなかったのか、作ろうとして断念したことがあったので、簡単に作れて良かったですね。