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 を立てて実際に通知まで実装してデプロイする。
Discussion