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がなかったから作れなかったのか、作ろうとして断念したことがあったので、簡単に作れて良かったですね。