AWS 請求通知 BOT を AWS Lambda + AWS SDK + Go で作る~ SDK調査編 ~
AWS 請求通知 BOT を AWS Lambda + AWS SDK + Go で作る ~ SDK調査編 ~
あらまし
AWS 請求額が設定ミスで跳ね上がる事例が多々発生しがちです、その対策として、現在の請求額を通知する BOT を作るのも一つです。
今回 Cost Explorer の SDK に関して個人的に調査を行った。
Cost Explorer とは
AWSのコストを可視化できます。
料金について
Cost Explorer のユーザーインターフェイスを使用したコストと使用状況の表示は無料。
Cost Explorer API を使用して、プログラムでデータにアクセスすることもできます。
API リクエストごとに
今日(2022/04/07)のレート
API リクエストごとに
なので、毎日リクエストを一回行ったとして、
一ヶ月(30日と仮定)約
Cost Explorer の SDK
Go で実装する際のドキュメント
USD しか取れない問題
USD ベースの rate がとれる API 「OpenExchangeRates」 を利用してUSDをJPYに変換する。
フリープランでは 1000 リクエスト/月 まで利用できる。
取得した様子
使い方は非常にシンプルで、app_id をクエリパラメータに与えて Get リクエストを投げるだけ。
Go言語で実装した例
app_id は ssm パラメータストア等の SecureString を利用。
func GetOpenexchangeratesJpy(sess *session.Session) float64 {
// Base URL: https://docs.openexchangerates.org
base := "https://openexchangerates.org/api/latest.json?app_id=%s"
// Application id: https://docs.openexchangerates.org/docs/authentication
// Using ssm parametor store: https://ap-northeast-1.console.aws.amazon.com/systems-manager/parameters
svc := ssm.New(sess)
app_id, err := svc.GetParameter(&ssm.GetParameterInput{
Name: aws.String("<YOUR_OPENEXCHANGERATES_APP_ID_PARAMSNAME>"),
WithDecryption: aws.Bool(true),
})
if err != nil {
log.Println(err.Error())
}
// Create Request url
url := fmt.Sprintf(base, *app_id.Parameter.Value)
// Start Request
resp, err := http.Get(url)
if err != nil {
log.Println(err.Error())
}
defer resp.Body.Close()
source, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err.Error())
}
// Parse json
desc := Schema{}
json.Unmarshal(source, &desc)
return desc.Rates.JPY
}
Cost Explorer SDK のパラメータまとめ
今回は GetCostAndUsage
を利用する。
Filter
さまざまなディメンションで AWS コストをフィルタリングできる。
たとえば、SERVICE と LINKED_ACCOUNT を指定して、そのアカウントのサービスの使用に関連付けられているコストを取得できる。
このパラメータは必須ではない。
Granularity
AWS コストの粒度を MONTHLY、DAILY、または HOURLY で設定できる。
有効な値:DAILY|MONTHLY|HOURLY
このパラメータは必須。
GroupBy
AWSコストは、最大2つの異なるグループ (ディメンション、タグキー、コストカテゴリ、またはタイプ別の2つのグループ) を使用してグループ化できる。
DIMENSIONタイプの有効な値は、
AZ、
INSTANCE_TYPE、
LEGAL_ENTITY_NAME、
INVOICING_ENTITY、
LINKED_ACCOUNT、
OPERATION、
PLATFORM、
PURCHASE_TYPE、
SERVICE、
TENANCY、
RECORD_TYPE、
USAGE_TYPE
また、タグの種類でグループ化し、有効なタグキーを含めると、空の文字列を含むすべてのタグ値が取得される。
もう少し詳しく書くと、
AZ に関しては アベイラビリティーゾーン毎の情報
INSTANCE_TYPE に関しては インスタンスタイプ(例えば、 t2.micro など)
LEGAL_ENTITY_NAME, INVOICING_ENTITY に関しては 例えば、Amazon Web Services Japan G.K. など
LINKED_ACCOUNT に関しては アカウントに紐づく情報
OPERATION に関しては オペレーション毎の情報(例えば RunInstances など)
PLATFORM に関しては プラットフォーム毎の情報
PURCHASE_TYPE に関しては オンデマンド、リザーブド、saving 等のプラン毎の情報
SERVICE に関しては サービス毎の情報
TENANCY に関しては (例えば、Sharedなど)
RECORD_TYPE に関しては (例えば、DiscountedUsage,Taxなど)
USAGE_TYPE に関しては (例えば、APS1-EUN1-AWS-Out-Bytesなど)
等のキーを指定可能で1回の呼び出しで2つまで指定可能。
このパラメータは必須ではない。
Metrics
有効な値は AmortizedCost、BlendedCost、NetAmortizedCost、NetUnblendedCost、NormalizedUsageAmount、UnblendedCost、UsageQuantity
こちらの記事が参考になる。
今回は UnblendedCost
を 利用する。
ちなみにこのパラメータは必須。
NextPageToken
次の結果セットを取得するトークン。
前回の呼び出しからの応答の結果が最大ページサイズを超える場合にトークンが提供される。
今回の調査の実装例に関しては簡単のためにページングは考慮しない。
結果オブジェクトに NextPageToken が含まれていたら後続を読み込む処理が必要になる。
TimePeriod
AWS コストを取得するための開始日と終了日を設定できる。
開始日は含みますが、終了日は含まない。
たとえば、start が 2017-01-01 で end が 2017-05-01 の場合、コストと使用状況のデータは 2017-01-01 から 2017-04-30 まで取得されるが、2017-05-01 は含まれない。
このパラメータももちろん必須。
Goでの実装
通知予定の部分
package main
import (
"aws-billing-notify/pkg/aws/profile"
"aws-billing-notify/pkg/aws/sdk/ce/costandusage/calc"
"aws-billing-notify/pkg/aws/sdk/ce/costandusage/granularity"
"aws-billing-notify/pkg/aws/sdk/ce/costandusage/group"
"aws-billing-notify/pkg/aws/sdk/ce/costandusage/metric"
"aws-billing-notify/pkg/aws/sdk/ce/costandusage/term"
"fmt"
"log"
"strconv"
"time"
"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"
"github.com/aws/aws-sdk-go/service/ssm"
)
type costParams struct {
Granularity *string
Term *costexplorer.DateInterval
Metrics []*string
Groups []*costexplorer.GroupDefinition
}
func (c costParams) getCost(svc costexploreriface.CostExplorerAPI, start *string, end *string) (result *costexplorer.GetCostAndUsageOutput) {
c.Granularity = granularity.Monthly.String()
c.Metrics = []*string{
metric.UnblendedCost.String(),
}
c.Term = &costexplorer.DateInterval{
Start: start,
End: end,
}
service := costexplorer.GroupDefinition{
Key: group.Service.Key(),
Type: group.Dimention.Type(),
}
c.Groups = append(c.Groups, &service)
input := costexplorer.GetCostAndUsageInput{
Granularity: c.Granularity,
TimePeriod: c.Term,
Metrics: c.Metrics,
GroupBy: c.Groups,
}
result, err := svc.GetCostAndUsage(&input)
if err != nil {
log.Println(err.Error())
}
return result
}
func main() {
sess, err := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
Profile: profile.Name,
}) // todo: change lambda
if err != nil {
log.Println(err.Error())
}
c := costParams{}
svce := costexplorer.New(sess)
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Println(err.Error())
}
start, end := term.CreateThisMonthRange(jst)
cost := c.getCost(svce, start, end)
svc := ssm.New(sess)
rawjpy, err := svc.GetParameter(&ssm.GetParameterInput{
Name: aws.String("<YOUR_OPENEXCHANGERATES_JPY_RATE_PARAMSNAME>"),
WithDecryption: aws.Bool(false),
})
if err != nil {
log.Println(err.Error())
}
jpy, err := strconv.ParseFloat(*rawjpy.Parameter.Value, 64)
if err != nil {
log.Println(err.Error())
}
for _, group := range cost.ResultsByTime[0].Groups {
amount, err := strconv.ParseFloat(*group.Metrics[*metric.UnblendedCost.String()].Amount, 64)
if err != nil {
log.Println(err.Error())
}
// Set openexchangerates jpy
val := fmt.Sprintf("- %s: %f 円", *group.Keys[0], amount*jpy)
fmt.Println(val)
}
// Set openexchangerates jpy
fmt.Println(calc.Sum(cost, jpy))
}
レート更新部
package main
import (
"aws-billing-notify/pkg/aws/profile"
"aws-billing-notify/pkg/openexchangerates"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
)
func main() {
sess, err := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
Profile: profile.Name,
})
if err != nil {
log.Println(err.Error())
}
openexchangerates.PutOpenexchangeratesJpy(sess)
}
まとめ
今回は SDK の検証まででした。
AWS を利用したサービス設計時、ある程度正しい見積もりができないと大きな損害を生む可能性があるので、信用を失うことになりかねないです。
AWS の料金体系に関して、今後も継続して勉強を続けていこうと思いました。
次回は、Lambda を立てて実際に通知まで行きたいと思います。
Author And Source
この問題について(AWS 請求通知 BOT を AWS Lambda + AWS SDK + Go で作る~ SDK調査編 ~), 我々は、より多くの情報をここで見つけました https://zenn.dev/_kazuya/articles/aeabc23cd5df2a著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol