LoginSignup
25
6

More than 3 years have passed since last update.

AWSの利用料金をChatworkに通知してくれるバッチをGoで作った

Last updated at Posted at 2019-12-24

はじめに

AWSの料金を毎日確認しにログインしてコンソールを開くのは面倒…。でも、気がついたら設定が間違っててすごい料金を請求されるなーんてことも。

そんなことが起こる前に、毎日使用料金を通知して把握しておこう!

ということで、Cost ExplorerのSDKを使ってGoでバッチを作成してみました。

AWSの構成

CostExplorer.png

当記事はLambdaにアップするバッチにフォーカスしてます。

実装

通知したいこと

  • 昨日の利用料金
  • 今月の利用料金(今月1日から昨日までの合計金額) or 今日が1日のときは先月の利用料金

必要なもの

  • Chatworkのルームid(トークルームURL:chatwork.com/#!rid********* ←9桁の数字)
  • ChatworkのAPIトークン(画面右上の自分の名前 → API設定で発行できます)

気をつけるところ

料金を知るのにもお金がかかります。
Cost Explorerに対して1リクエストあたり 0.01USD かかります。

AWSユーザーガイド

ソースコード

package main

import (
    "bytes"
    "fmt"
    "github.com/pkg/errors"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "os"
    "time"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/costexplorer"
    "github.com/aws/aws-sdk-go/service/costexplorer/costexploreriface"
)

var chatworkClient = ChatworkClient{
    APIURL:   "https://api.chatwork.com/",
    Resource: "/v2/rooms/<ルームid>/messages",
}

// ChatworkClient はチャットワークへ通知するためのクライアントに相当する構造体
type ChatworkClient struct {
    APIURL   string
    Resource string
}

// メッセージの通知
func (chatwork ChatworkClient) postMessage(msg string) error {
    u, _ := url.ParseRequestURI(chatwork.APIURL)
    u.Path = chatwork.Resource
    urlStr := fmt.Sprintf("%v", u)

    data := url.Values{}
    data.Set("body", msg)
    fmt.Printf(data.Encode())

    client := &http.Client{}
    r, err := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode()))
    if err != nil {
        fmt.Println("HTTPリクエストの生成に失敗しました。date:" + fmt.Sprint(data) + ", urlStr:" + urlStr + ", err:" + fmt.Sprint(err))
        return errors.WithStack(err)
    }

    r.Header.Add("X-ChatWorkToken", "<APIトークン>")
    r.Header.Add("Content-Type", "application/x-www-form-urlencoded")

    resp, err := client.Do(r)
    if err != nil {
        fmt.Println("HTTPリクエストに失敗しました。date:" + fmt.Sprint(data) + ", urlStr:" + urlStr + ", err:" + fmt.Sprint(err))
        return errors.WithStack(err)
    }

    defer resp.Body.Close()
    contents, _ := ioutil.ReadAll(resp.Body)
    fmt.Printf("Http Status:%s, result: %s\n", resp.Status, contents)

    return nil
}

// コストの取得
func GetCost(svc costexploreriface.CostExplorerAPI, period string) (result *costexplorer.GetCostAndUsageOutput) {
    // 現在時刻の取得
    jst, _ := time.LoadLocation("Asia/Tokyo")
    now := time.Now().UTC().In(jst)
    dayBefore := now.AddDate(0, 0, -1)
    first := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, jst)
    if now.Day() == 1 { // 月初のときは先月分
        first = first.AddDate(0, -1, 0)
    }
    nowDate := now.Format("2006-01-02")
    nowDateP := &nowDate
    dateBefore := dayBefore.Format("2006-01-02")
    dateBeforeP := &dateBefore
    firstDate := first.Format("2006-01-02")
    firstDateP := &firstDate

    start := dateBeforeP
    if period == "Monthly" {
        start = firstDateP
    }

    granularity := aws.String("DAILY")
    if period == "Monthly" {
        granularity = aws.String("MONTHLY")
    }

    metric := "NetUnblendedCost" // 非ブレンド純コスト
    metrics := []*string{&metric}

    timePeriod := costexplorer.DateInterval{
        Start: start,
        End:   nowDateP,
    }

    // Inputの作成
    input := &costexplorer.GetCostAndUsageInput{
        Granularity: granularity,
        Metrics:     metrics,
        TimePeriod:  &timePeriod,
    }

    // 処理実行
    result, err := svc.GetCostAndUsage(input)
    if err != nil {
        log.Println(err.Error())
    }

    // 処理結果を出力
    log.Println(result)

    return result
}

// 処理実行
func run() error {
    log.Println("--- コスト取得バッチ 開始")
    log.Println("----- セッション作成")
    svc := costexplorer.New(session.Must(session.NewSession()))

    log.Println("----- コスト取得 実行")
    costDaily := GetCost(svc, "Daily")
    costMonthly := GetCost(svc, "Monthly")
    log.Println("----- コスト取得 完了")

    log.Println("----- メッセージの通知 実行")
    err := chatworkClient.postMessage(costDaily.String())
    if err != nil{
        fmt.Printf("%+v\n", err)
        return err
    }
    err = chatworkClient.postMessage(costMonthly.String())
    if err != nil{
        fmt.Printf("%+v\n", err)
        return err
    }

    log.Println("--- コスト取得バッチ 完了")
    return nil
}

// メイン
func main() {
    lambda.Start(run)
}

実行結果

2019/12/16に実行してみました。

こちらが1日前(12/15)の利用料金を取得した結果です。期間(TimePeriod)を2019/12/15から2019/12/16で指定しています。料金はTotalの中にあるAmountで、今回だと約0.06USDとなっています。
cost_explorer_json_daily.png

こちらは当月(2019/12/01~2019/12/15)の合計金額です。指定した期間は2019/12/01から2019/12/16で、費用は約1.35USDという結果が得られました。
cost_explorer_json_monthly.png

解説

コストの取得

今回は、API「GetCostAndUsage」のInputを構成する要素として以下の3つを使用しています。

  • Granularity
  • Metrics
  • TimePeriod

Granularity

Granularityは「粒度」という意味で、どの粒度の期間で取得するのか指定できます。選択肢は以下の3つがあります。

  • MONTHLY
  • DAILY
  • HOURLY

今回はMONTHLYとDAILYを使いました。

Metrics

Metricsは「指標」という意味で、コストをどう計算するかを指定できます。選択肢は以下の7つがあります。

  • AmortizedCost
  • BlendedCost
  • NetAmortizedCost
  • NetUnblendedCost
  • NormalizedUsageAmount
  • UnblendedCost
  • UsageQuantity

それぞれのメトリクスの内容はこの記事を参考にしました。
AWS Cost Explorerに渡す、Metricsの値の意味

今回は「NetUnblendedCost = ディスカウント適用後(EDP割引等)のコスト」を使っています。
ちなみに、コンソール上で見る場合は、Cost Explorer: コストと使用状況のページの右下、詳細オプション → コストの表示方法 → 非ブレンド純コスト と同額になります。
cost-explorer-console.png

TimePeriod

Start(集計開始日時)とEnd(集計終了日時)を指定します。
フォーマットは " YYYY-MM-DD "です。

なお、Endに指定した日の利用料金は加算されません。
例えば、
Start : 2019-12-01
End : 2019-12-16
とした場合は1日から15日までの料金を取得することになります。

通知するベスト時間

最初、朝9時に前日の利用料金を通知するようにしていましたが、ある日、コスト通知用のトークルームを見ると「$ 0」。

昨日は無料キャンペーンだったのかなあ(わくわく)という話になりましたが、そんなことは…なかったです。

明確なコストエクスプローラーの更新時間は書かれていませんが、公式ページでは、「24 時間ごとに少なくとも一度コストデータを更新します。」とのことだったので、夜に通知するほうが正確な金額になると思われます。

ちなみに、私は朝9時に一昨日の利用料金を取得するようにしています。今のところ、それ以降の料金更新はないっぽいです。

おまけ

かわいく通知してみた

上記のままだと取得してきたJSONのまま表示されるので、お花をつけてかわいく整えてみました🌼

func MakeMassage(costMonthly, costDaily *costexplorer.GetCostAndUsageOutput) string {
    var msg bytes.Buffer
    msg.WriteString("(F)" + "AWS使用料金" + "(F)" + "\n\n")
    msg.WriteString("◆MONTHLY")
    msg.WriteString(" (" + *costMonthly.ResultsByTime[0].TimePeriod.Start + "~" +
        *costDaily.ResultsByTime[0].TimePeriod.Start + ")\n")
    msg.WriteString(" $ " + *costMonthly.ResultsByTime[0].Total["NetUnblendedCost"].Amount + "\n\n")
    msg.WriteString("◆DAILY")
    msg.WriteString(" (" + *costDaily.ResultsByTime[0].TimePeriod.Start + ")\n")
    msg.WriteString(" $ " + *costDaily.ResultsByTime[0].Total["NetUnblendedCost"].Amount + "\n")

    return msg.String()
}

処理実行箇所にメッセージ作成を追加し、実行コードを少し変更

log.Println("----- メッセージの作成 実行")
    msg := MakeMassage(costMonthly, costDaily)
log.Println("----- メッセージの作成 完了")

log.Printf("----- メッセージの通知 実行")
    err := chatworkClient.postMessage(msg)

実行結果

かわいくなりました🌼
cost_explorer_msg.png

おわりに

コンソール上だと下2桁しか表示されないですけど、Cost Explorer APIで取得すると下10桁まで表示されるので、コンソール上では0.00USD。でも実際は0.0012345678USDってことも分かるのでいいですよね。

大変だったとこととしては、はじめましてのGo。C言語を昔授業でちょっとやっていたけどポインタから逃げたので、今となってポインタに苦しめられました。

あとは、AWSのSDKを使う時に公式ドキュメントを読んでコードを書くことが大変でした。あまりGoでCostExplorerを使っている記事がなくて、自力では完成させることができず…。SDKの使い方、Goの書き方、とても勉強になりました。

次は藤原さんのQiitaの記事を見ながら、slackに通知できるようにしてみようと思っています!

25
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
6