はじめに
AWSの料金を毎日確認しにログインしてコンソールを開くのは面倒…。でも、気がついたら設定が間違っててすごい料金を請求されるなーんてことも。
そんなことが起こる前に、毎日使用料金を通知して把握しておこう!
ということで、Cost ExplorerのSDKを使ってGoでバッチを作成してみました。
AWSの構成
当記事はLambdaにアップするバッチにフォーカスしてます。
実装
通知したいこと
- 昨日の利用料金
- 今月の利用料金(今月1日から昨日までの合計金額) or 今日が1日のときは先月の利用料金
必要なもの
- Chatworkのルームid(トークルームURL:chatwork.com/#!rid********* ←9桁の数字)
- ChatworkのAPIトークン(画面右上の自分の名前 → API設定で発行できます)
気をつけるところ
料金を知るのにもお金がかかります。
Cost Explorerに対して1リクエストあたり 0.01USD かかります。
ソースコード
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となっています。
こちらは当月(2019/12/01~2019/12/15)の合計金額です。指定した期間は2019/12/01から2019/12/16で、費用は約1.35USDという結果が得られました。
解説
コストの取得
今回は、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: コストと使用状況のページの右下、詳細オプション → コストの表示方法 → 非ブレンド純コスト と同額になります。
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)
実行結果
おわりに
コンソール上だと下2桁しか表示されないですけど、Cost Explorer APIで取得すると下10桁まで表示されるので、コンソール上では0.00USD。でも実際は0.0012345678USDってことも分かるのでいいですよね。
大変だったとこととしては、はじめましてのGo。C言語を昔授業でちょっとやっていたけどポインタから逃げたので、今となってポインタに苦しめられました。
あとは、AWSのSDKを使う時に公式ドキュメントを読んでコードを書くことが大変でした。あまりGoでCostExplorerを使っている記事がなくて、自力では完成させることができず…。SDKの使い方、Goの書き方、とても勉強になりました。
次は藤原さんのQiitaの記事を見ながら、slackに通知できるようにしてみようと思っています!