kazu22002の技術覚書

PHPer, Golang, AWS エンジニアの日々

lambda + golangでawsの料金をサービス別に通知

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
}

techblog.zozo.com

アクセス権限付与

lambdaの実行ロールに「GetCostAndUsage」の権限を付与する

参考

qiita.com

権限がない場合、 「 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

結果

f:id:kazu22002:20210629024435p:plain

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