AWS LambdaでAWS利用費を毎朝Slackに通知する

f:id:ryskit:20200102190802p:plain

明けましておめでとうございます! 今年もよろしくお願いします!

今年は、「個人ブログで技術ネタ50記事書く」という目標も立てたので早速1本目を書きます!

今回作りたいのはAWS Lambdaを使ってAWS利用費を毎朝通知する仕組みです。

イメージとしては、今日が2019年12月25日なら、2019年12月1日から2019年12月24日までのAWS利用費を朝7:00にSlackに通知するというものです。

では、さっそくいってみましょう!

Slack Appを作る

まず、AWS利用費を通知するためにSlack Appを作成して通知先のエンドポイントを発行する必要があります。

以下のURLにアクセスします。

https://api.slack.com/apps

アクセスしたら、 Create New App をクリックします。

f:id:ryskit:20200102172328p:plain

クリックするとモーダルが表示されるので、名前Workspace を指定します。

Workspace は自身のものや会社のSlackのWordspaceを指定すれば良いと思います。

f:id:ryskit:20200102172701p:plain

Create App をしたら以下のように Basic Informationのページに遷移するので、 そこで表示されている、Incoming Webhook をクリックします。

f:id:ryskit:20200102173102p:plain

Incoming Webhook をクリックしたら、以下のページに遷移します。

Activate Incoming webhookをon にしてあげます。

f:id:ryskit:20200102174011p:plain

すると、以下のように表示内容が変わります。

まだ、WorkspaceにWebhookを追加してない状態なので、Add New Webhook to Workspace をクリックします。

f:id:ryskit:20200102174209p:plain

クリックしたら、以下のようにアクセス権限を与えて良いか確認されます。 どのチャンネルにAWS利用費を通知するか決めたら、そのチャンネルを選択して、許可 をクリックします。

f:id:ryskit:20200102173417p:plain

許可したら、Webhookが追加されます。

ここで発行されたWebhook URLが必要になってくるので、コピーしておくなどしておいてください。

f:id:ryskit:20200102174734p:plain

ひとまず、Slack Appの準備は完了です。

AWS Lambdaのコードを書く

次はSlackへ通知するための、AWS Lambdaを作っていきます。 サクッとやってしまいたいのでFramework等は使わず、GoでLambdaを書いてみたかったので言語はGoを使います。

コードはこんな感じ。

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "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"
    "net/http"
    "time"
)

const (
    SlackApi   string = "ここにSlack Appで発行したWebhook URLをコピペする"
    DateLayout string = "2006-01-02"
)

func NewCostExplorerClient() *costexplorer.CostExplorer {
    sess := session.Must(session.NewSessionWithOptions(session.Options{
        Config: aws.Config{
            Region: aws.String("ap-northeast-1"),
        },
        SharedConfigState: session.SharedConfigEnable,
    }))
    return costexplorer.New(sess, aws.NewConfig().WithRegion("ap-northeast-1"))
}

type CostInfo struct {
    Start  string `json:"start"`
    End    string `json:"end"`
    Amount string `json:"amount"`
}

type SlackMessage struct {
    Text string `json:"text"`
    Mrkdwn bool `json:"mrkdwn"`
}

type TimeStringHelper struct {
    Location *time.Location
    Now time.Time
}

func NewTimeStringHelper(locationName string) *TimeStringHelper {
    location, _ := time.LoadLocation(locationName)
    return &TimeStringHelper{
        Location: location,
        Now: time.Now().In(location),
    }
}

func (helper TimeStringHelper) GetBeginningOfLastMonth() string {
    return time.Date(helper.Now.Year(), helper.Now.Month() -1, 1, 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) GetBeginningOfMonth() string {
    return time.Date(helper.Now.Year(), helper.Now.Month(), 1, 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) GetYesterday() string {
    return time.Date(helper.Now.Year(), helper.Now.Month(), helper.Now.Day() -1, 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) GetToday() string {
    return time.Date(helper.Now.Year(), helper.Now.Month(), helper.Now.Day(), 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) IsTodayFirst() bool {
    return helper.Now.Day() == 1
}

func (helper TimeStringHelper) GetStartTimePeriod() string {
    if helper.IsTodayFirst() {
        return helper.GetBeginningOfLastMonth()
    } else {
        return helper.GetBeginningOfMonth()
    }
}


func GetCostInfo(helper *TimeStringHelper) *CostInfo {
    start := helper.GetStartTimePeriod()
    end := helper.GetToday()
    costExplorer := NewCostExplorerClient()
    output, err := costExplorer.GetCostAndUsage(&costexplorer.GetCostAndUsageInput{
        Granularity: aws.String("MONTHLY"),
        Metrics: []*string{
            aws.String("AmortizedCost"),
        },
        TimePeriod: &costexplorer.DateInterval{
            Start: aws.String(start),
            End:   aws.String(end),
        },
    })
    if err != nil {
        panic(err)
    }
    total := output.ResultsByTime[0].Total["AmortizedCost"]
    amount := aws.StringValue(total.Amount)

    return &CostInfo{
        Start:  start,
        End:    end,
        Amount: amount,
    }
}

func makeSlackMessage(costInfo *CostInfo, helper *TimeStringHelper) SlackMessage {
    return SlackMessage{
        Text: fmt.Sprintf("*期間*: `%s ~ %s`\n*料金*: `$%s`",
            costInfo.Start,
            helper.GetYesterday(),
            costInfo.Amount),
        Mrkdwn: true}
}

func PostToSlack(message SlackMessage) {
    input, _ := json.Marshal(message)
    fmt.Println(string(input))
    http.Post(SlackApi, "application/json", bytes.NewBuffer(input))
}

type Response struct {
    Message []byte `json:"message"`
}

func BillingNotification(ctx context.Context) (Response, error) {
    helper := NewTimeStringHelper("Asia/Tokyo")
    fmt.Println(helper.Now.Format("2006/01/02 15:04:05"))
    costInfo := GetCostInfo(helper)
    message := makeSlackMessage(costInfo, helper)
    PostToSlack(message)
    json, _ := json.Marshal(message)
    return Response{Message: json}, nil
}

func main() {
    lambda.Start(BillingNotification)
}

Githubにもアップしてるので、試してみたい方はぜひ使ってみてください。

github.com

コードに関して全ては説明しないですが、ピックアップして説明します。

AWS利用費の取得には、Cost Explorer を使います。 以下の部分ですね。

GranularityにはMONTHLY を指定します。 TimePeriodは、利用料の取得期間です。 たとえば、Startに 2019-12-01 、 Endに 2019-12-31を指定すると、 2019-12-01 から 2019-12-30までのAWS利用料を取得できます。 ここが間違えやすところかもしれません。

参考: https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html#awscostmanagement-GetCostAndUsage-request-TimePeriod

output, err := costExplorer.GetCostAndUsage(&costexplorer.GetCostAndUsageInput{
    Granularity: aws.String("MONTHLY"),
    Metrics: []*string{
        aws.String("AmortizedCost"),
    },
    TimePeriod: &costexplorer.DateInterval{
        Start: aws.String(start),
        End:   aws.String(end),
    },
})

Slackへ通知する際のmessageのpayloadも決まっているので、 それに合わせてメッセージをPostする必要があります。

func makeSlackMessage(costInfo *CostInfo, helper *TimeStringHelper) SlackMessage {
    return SlackMessage{
        Text: fmt.Sprintf("*期間*: `%s ~ %s`\n*料金*: `$%s`",
            costInfo.Start,
            helper.GetYesterday(),
            costInfo.Amount),
        Mrkdwn: true}
}

参照: api.slack.com

定数のSlackApi にはSlack Appで発行したWebhookのURLを貼り付けておいてください。

これができたら、ビルドしてzip化します。

ビルドは以下のコマンドで実行してください。

オプションをつけ忘れるとLambdaが実行できないので気をつけましょう!

GOARCH=amd64 GOOS=linux go build

ビルドしたら実行ファイルが生成されるので、以下のコマンドでzip化します。

zip billing-notification.zip ./billing-notification

そうすると、billing-notification.zip というzipファイルが生成されます。 これはあとでAWSコンソールでLambda関数を作るときに使います。

AWSコンソールからLambda実行用IAMサービスロールを作成する

IAMサービスロールにアタッチするポリシーのJSONは以下のように書きます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ce:GetCostAndUsage"
            ],
            "Resource": "*"
        }
    ]
}

IAMサービスロールはLambdaを選択してください。

f:id:ryskit:20200102181522p:plain

あとはよしなにIAMサービスロールを作成します。

AWSコンソールからLambdaの関数を作成する

AWSコンソールからLambdaを開いて作成してきます。

関数名: billing-notification ランタイム: Go 1.x 実行ロール: 先程作成したサービスロールを指定するので、既存のロールを使用する を選択し、セレクトボックスから作成したロールを探して選択します。

f:id:ryskit:20200102182049p:plain

関数を作成できたら、LambdaのトリガーにCloudWatch Events を指定しておいてください。

そして、zip化したLambdaのコードをアップロードします。

ハンドラ名は billing-notification と入力し、保存します。

f:id:ryskit:20200102182941p:plain

もうこれでSlackに通知することができます!

試しにテストしてみましょう。

テストイベントの設定をします。 作成したことがあれば以下のように表示されますし、してなければ テストイベントの設定 と表示されているはず?なので、 テスト をクリックします。

f:id:ryskit:20200102183614p:plain

もし新規で作る場合は以下のようにモーダルが表示されるので、適当な名前をつけて保存してください。

f:id:ryskit:20200102183805p:plain

そして、もう一度テストをクリックすると、Slackに利用料が通知されるはず!!

f:id:ryskit:20200102183930p:plain

ほぼできた🎉🎉🎉

作業もあと少しです!

AWSコンソールからCloudWatch Eventsの設定

Slackに通知する仕組みは作れました。

あとは、毎朝7時にAWS Lambdaを実行するようにしておきたいですよね?

そのためにCloucWatch Eventsを利用します。

Lambdaを作成したときにトリガーにCloudWatch Eventsを指定したのはそのためです。

それでは、AWSコンソールからCloudWatchを開きます。

開いたら以下のように、ルールと書かれた箇所をクリックします。

f:id:ryskit:20200102184644p:plain

クリックしたら以下のようにルールの作成をクリックします。

f:id:ryskit:20200102184907p:plain

そうしたら、以下のように設定してみてください。

Cron式0 22 * * ? * と指定していますが、UTC表記なのでこのままで問題ありません。

設定できたら 設定の詳細 をクリックします。

f:id:ryskit:20200102184801p:plain

ステップ 2: ルールの詳細を設定する 画面では、状態を有効化してルールを作成してください。

これで、毎朝7時にLambdaが実行されてSlackに通知が来るはずです🎉🎉🎉

f:id:ryskit:20200102185242p:plain

最後に

いかがでしたでしょうか?

Lambdaを使えばサクッとAWS利用費を通知できるので、今どれぐらい使っていて請求が来るのか把握しやすくなりました!

もしこの記事が良ければ、Twitterなどでシェアしてくださいね!

読んでくださってありがとうございました!

※ アイキャッチには@tentenのGopher by tenntenn CC BY 3.0を利用させていただいています。