「AWS無料相談会」をオンラインで開催中

プログラムからAWS月額利用料金を日本円で取得する方法

エンジニアの内山です。最近は暑すぎて、ずっと汗をかいてます。

今回は、Go言語とHeadless Chromeを使って、
AWSコンソールをスクレイピングし、
AWS月額利用料金を日本円で取得する方法をご紹介します。

目次

取得方法の検討

プログラムでAWS月額利用料金を取得する場合、以下の方法があります。

  • CloudWatch経由で取得する
  • S3に利用料金CSVを出力するよう設定し、ダウンロードする

これらの方法で、取得できる利用料金はUSDのみとなっています(2018年7月現在)。

アカウントへの請求に使用する通貨。このディメンションは必須です。

単位: USD

AWS Billing and Cost Management のディメンションおよびメトリクス
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/billing-metricscollected.html

USDからJPYを計算するにも、AWSが使っている為替レートが必要になってきてしまい、正確な値が出せません。

AWS内部ではドルで計算され、月末の利用料集計時に、その時点でのAWSが定めるレートをもとに円に自動計算されます

AWS支払の円払いへの切り替え方法について
https://aws.amazon.com/jp/blogs/news/how-to-pay-aws-by-yen/

そこで、AWSコンソールをスクレイピングして日本円を取得するという方法を取ることにしました。

スクレイピング用のアカウントを設定

スクレイピングで使用するアカウントについては、セキュリティを考慮する必要があります。
方針としては、権限を最小限に設定したスクレイピング専用のアカウントを作成します。
このアカウントのログインIDとパスワード自体を厳重に管理する必要もありますが、
万一漏れた場合でも被害を最小限になるようにしておきます。

今回は、利用料金を取得するだけなので、以下のようなポリシーにしておきます。

使用するツール

今回は、プロジェクトの状況などから以下のツールを使用しました。

  • Docker
  • docker-compose
  • Go言語
  • Headless Chrome

これらのツールを使用してスクレイピングする方法をご紹介します。

Dockerfileの設定

Dockerfileでは、Headless ChromeとGoで使用するモジュールをインストールしています。
イメージはGoがすでに入っているgolang:1.10.2-alpineを使用します。

Dockerfile

FROM golang:1.10.2-alpine

RUN apk add --no-cache chromium chromium-chromedriver git bash && 
    go get -u github.com/golang/dep/cmd/dep && 
    go get -u github.com/golang/lint/golint

WORKDIR /go/src/chromium-test
COPY . /go/src/chromium-test

docker-compose.ymlの設定

いろいろと楽するため、docker-composeを使用します。
Dockerfileの設定とGoのビルド&実行スクリプトの設定をしています。

docker-compose.yml

version: '3'
services:
  go:
    command: ./run.sh
    build:
      context: ./
      dockerfile: ./Dockerfile
    volumes:
      - .:/go/src/chromium-test

run.shは、Goのビルド&実行スクリプトです。
depで依存関係モジュールをインストールしています。

#!/bin/sh

dep ensure
CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix cgo -o ./main ./main.go
./main

Goで使用するモジュールの設定

Gopkg.tomlは、depコマンドで読み込まれる依存モジュールの設定ファイルです。
Headless ChromeをGoから使用するためのモジュール(agouti)と.envを読み込むためのモジュール(godotenv)を記述しています。

[prune]
    go-tests = true
    unused-packages = true

[[constraint]]
    name = "github.com/sclevine/agouti"
    version = "3.0.0"

[[constraint]]
    name = "github.com/joho/godotenv"
    version = "1.2.0"

スクレイピングスクリプト

スクリプトでは、コンソール上の以下の部分を取得します(数値は改変しています)。

アカウント情報などは.envファイルに記述します。
Git管理に含めないように注意します。

ACCOUNT_ID=xxxxxxxxxxxxxxxx
USER_NAME=xxxxxxxxxxxxxxxx
PASSWORD=xxxxxxxxxxxxxxx
YEAR=2018
MONTH=6

スクレイピングするプログラムは以下のようになりました。

main.go

package main

import (
    "github.com/sclevine/agouti"
    "github.com/joho/godotenv"
    "log"
    "fmt"
    "time"
    "strconv"
    "strings"
    "os"
)

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    accountID := os.Getenv("ACCOUNT_ID")
    userName := os.Getenv("USER_NAME")
    password := os.Getenv("PASSWORD")
    year := os.Getenv("YEAR")
    month := os.Getenv("MONTH")

    billingURL := fmt.Sprintf("https://console.aws.amazon.com/billing/home?region=ap-northeast-1#/bills?year=%s&month=%s", year, month)

    log.Println("--- Create driver")
    driver := agouti.ChromeDriver(
        agouti.ChromeOptions("args", []string{
            "--headless",
            "--disable-gpu",
            "--window-size=1280,800",
            "--disable-application-cache",
            "--disable-infobars",
            "--no-sandbox",
            "--hide-scrollbars",
            "--enable-logging",
            "--log-level=0",
            "--v=99",
            "--single-process",
            "--homedir=/tmp",
            "--allow-insecure-localhost",
        }),
        agouti.Debug,
    )
    if err := driver.Start(); err != nil {
        log.Fatal()
    }
    defer driver.Stop()

    page, err := driver.NewPage()
    if err != nil {
        log.Fatal(err)
    }

    log.Println("--- Access login page")
    err = page.Navigate(fmt.Sprintf("https://%s.signin.aws.amazon.com/console", accountID))
    if err != nil {
        log.Fatalf("Failed to navigate: %v", err)
    }
    html, err := page.HTML()
    if err != nil {
        log.Fatalf("Failed to login:%v", err)
    }

    identityElm := page.FindByID("username")
    passwordElm := page.FindByID("password")
    identityElm.Fill(userName)
    passwordElm.Fill(password)
    log.Println("--- Start login")
    if err := page.FindByID("signin_button").Click(); err != nil {
        log.Fatalf("Failed to login:%v", err)
    }
    time.Sleep(3 * time.Second)
    //page.Screenshot("Screen2.png")


    log.Println("--- Access billing page")
    page.Navigate(billingURL)
    time.Sleep(10 * time.Second)

    billFxAmount, err := page.Find("#totalAmount #billFxAmount").Text()
    if err != nil {
        log.Fatal(err)
    }

    billStr := strings.Replace(billFxAmount, " JPY", "", 1)
    billStr = strings.Replace(billStr, ",", "", -1)
    billF, err := strconv.ParseFloat(billStr, 64)
    if err != nil {
        log.Fatal(err)
    }

    println(int(billF))
}

HTMLを解析してデータ取得を行っているため、なにかしら更新があった場合にうまく動作しない可能性があります。
上手く動作しない場合は、page.Screenshot("Screen.png")などを使用して、
スクリーンショットを確認しながらデバッグすると良いです。

以下のコマンドで、スクリプトを実行します。

$ docker-compose run go ./run.sh

処理がうまくいけば、料金が表示されます。

まとめ

AWSコンソールをスクレイピングして日本円を取得する方法をご紹介しました。
この方法はかなり無理やりなので、APIが対応したら即廃止にしたいですね。
それまでは注意して運用して行こうと思います。