ブログ

ryuzeeによるブログ記事。不定期更新
アジャイル開発に取り組むチーム向けのコーチングや、技術顧問、認定スクラムマスター研修などの各種トレーニングを提供しています。ぜひお気軽にご相談ください(初回相談無料)

Azureでコマンドラインから利用料金を取得する

みなさんこんにちは。@ryuzeeです。

クラウドを使っていると利用料金を常時監視したり通知するのは当然の行動の1つです。 そこで、Azureの利用料金をいちいちポータルにログインしなくても見られるようにスクリプト化してみたので、やり方を紹介します。 今回は、コマンドラインツール(AzureCLI)とスクリプトの開発にはRubyを使っています。

AzureCLIを用意する

AzureCLIはNode.jsを使うので、環境設定が済んでいない場合は、nvmでもなんでもいいので用意しておきます。

それが終わったらAzureCLIをグローバルにインストールします。いろいろなパッケージをインストールするので3分ぐらいかかります。

npm install -g azure-cli

インストールが完了したら設定に入ります。

Azureにはリソースマネージャー(ARM)デプロイモデルとクラシック(ASM)デプロイモデルがありますが、現在はリソースマネージャー推奨なので、そちらに切り替えます。

azure config mode arm

データを収集して送ってよいか聞かれますので、適当にyかnを選択してください。以下のように表示されれば成功です。

info:    Executing command config mode
info:    New mode is arm
info:    config mode command OK

次にAzureにログインします。

azure login

すると以下のように表示されるので、リンクをブラウザで開きます。

info:    Executing command login
/info:    To sign in, use a web browser to open the page https://aka.ms/devicelogin. Enter the code AB12CDE34 to authenticate.

以下のようにコードを入力する欄があるので、上記の出力のコードを入力して先に進みます。認証画面を経て、最後の画面で「デバイスの Microsoft Azure Cross-platform Command Line Interface アプリケーションにサインインしました。このウィンドウは閉じてかまいません。」と表示されたらブラウザを閉じて構いません。 その時点でコマンドラインでのログインが完了しています。

Azure AD アプリケーションの作成

自前のスクリプトからの認証や権限を管理するために、Azure ADアプリケーションを作成します。 ここでパスワードを設定しますが、これは後ほど自前スクリプトで使いますので控えておいてください。

azure ad app create --name "usage_collection" \
 --home-page "https://www.ryuzee.com/" \
 --identifier-uris "https://www.ryuzee.com" \
 --password sushikuitai

実行すると以下のようなログが表示されます(値はダミーになっています。念のため)。 この中で重要なのは、AppIdの項目で、このデータも後ほど使います。

info:    Executing command ad app create
+ Creating application usage_collection
data:    AppId:                   b7b17624-5536-3888-3328-1231ba716c02
data:    ObjectId:                12ba3f8a-1222-3e5d-a77e-0699b3f2d503
data:    DisplayName:             usage_collection
data:    IdentifierUris:          0=https://www.ryuzee.com
data:    ReplyUrls:
data:    AvailableToOtherTenants: False
data:    HomePage:                https://www.ryuzee.com/
info:    ad app create command OK

次にサービスプリンシパルを作成します。引数は先ほど作成したアプリケーションのAppIdを指定します。

azure ad sp create --applicationId b7b17624-5536-3888-3328-1231ba716c02

以下のように作成が完了しました。ここで表示されたObject Idはあとで使いますので控えておいてください。

info:    Executing command ad sp create
+ Creating service principal for application b6b07346-3719-4177-8618-3661ba716c02
data:    Object Id:               aa0f5bc5-e5d8-4e30-a9cb-999a49343bb1
data:    Display Name:            usage_collection
data:    Service Principal Names:
data:                             b6b07346-3719-4177-8618-3661ba716c02
data:                             https://www.ryuzee.com
info:    ad sp create command OK

ここで対象とするサブスクリプションを確認します。

azure account list

以下のように表示されます。ここで表示されるIdはサブスクリプションIDで後で使います。

data:    Name                    Id                                    Current  State  
data:    ----------------------  ------------------------------------  -------  -------
data:    従量課金                1abc8ad2-2cc5-9876-a0ee-0f76be829d3a  true    Enabled
(以下略。複数のサブスクリプションがある場合は複数表示されます)

サービスプリンシパルに権限を割り当てます。最初のobjectIdには先ほどサービスプリンシパルの作成で表示された値を指定します。 第2引数では読み取り権限を示すReaderを設定し、最後の引数に、サブスクリプションのIDを指定します。 複数のサブスクリプションから利用データを取得する場合は、以下の作業を繰り返し実施してください。

azure role assignment create \
  --objectId aa0f5bc5-e5d8-4e30-a9cb-999a49343bb1 \
  -o Reader \
 -c /subscriptions/1abc8ad2-2cc5-9876-a0ee-0f76be829d3a/

以下のように結果が表示されます。

info:    Executing command role assignment create
+ Finding role with specified name                                             
/data:    RoleAssignmentId     : /subscriptions/1abc8ad2-2cc5-9876-a0ee-0f76be829d3a/providers/Microsoft.Authorization/roleAssignments/69738901-2cb7-46a5-8179-c81431ca6046
data:    RoleDefinitionName   : Reader
data:    RoleDefinitionId     : acdd72a7-9876-55ef-bd42-f606fba81ae7
data:    Scope                : /subscriptions/1abc8ad2-2cc5-9876-a0ee-0f76be829d3a
data:    Display Name         : usage_collection
data:    SignInName           : undefined
data:    ObjectId             : aa0f5bc3-e5d8-4e30-a7cb-977a55555bb1
data:    ObjectType           : ServicePrincipal
data:    
+
info:    role assignment create command OK

次にテナント情報を取得します。以下を実行してください。

azure accout show

画面に以下が出力されます。ここで出力されるTenant IDを控えておいてください。

info:    Executing command account show
data:    Name                        : 従量課金
data:    ID                          : 7bdf8ad2-2cc5-3986-a0ee-0f76be829d3a
data:    State                       : Enabled
data:    Tenant ID                   : 80ed711a-71af-9123-82fa-57d0464c288e
data:    Is Default                  : false
data:    Environment                 : AzureCloud
data:    Has Certificate             : No
data:    Has Access Token            : Yes
data:    User name                   : hoge@example.com

コードによるデータ取得

この時点で揃っているデータは以下の通りです。

  • Application ID
  • パスワード
  • Tenant ID
  • Subscription ID

これらを使って使用量と料金表を取得します。処理の流れは次のようになります。

  1. Azureにログインして、APIのヘッダーに埋め込むTokenを取得する
  2. 使用量をAzure Resource Usage APIを使って取得する
  3. 料金表をAzure RateCard APIを使って取得する
  4. 2で取得した使用量と3で取得した料金表をマージする

1. Azureにログインして、APIのヘッダーに埋め込むTokenを取得

2や3でAPIを投げるためにはリクエストヘッダーにTokenを埋める必要があるので、まずそれを取得します。 取得のためには、https://login.microsoftonline.com/テナントID/oauth2/token?api-version=1.0に対してデータをPOSTします(POSTの内容は下記のpayloadの箇所を参照)。 ここでPOSTの値に、上記で取得したApplication IDをセットします。 正常に動作すれば、JSON形式でデータが返ってくるので、access_tokenの値を取り出します。この値を以降で使います。

def self.api_token(application_id, client_secret, tenant_id)
  url = "https://login.microsoftonline.com/#{tenant_id}/oauth2/token?api-version=1.0"
  payload = {
    'grant_type' => 'client_credentials',
    'client_id' => application_id,
    'client_secret' => client_secret,
    'resource' => "https://management.azure.com/"
  }
  headers = {
    "Content-Type" => "application/x-www-form-urlencoded"
  }
  RestClient.post(url, payload, headers){ |response, request, result, &block|
    case response.code
    when 200
      json = JSON.parse(response)
      token = json["access_token"]
      token
    else
      false
    end
  }
end

2. 使用量をAzure Resource Usage APIを使って取得

ここで使う値は、先ほど取得したTokenとSubscription IDです。下記の例では取得範囲を当月1日から前日までにしていますが適宜変更してください。 ここではヘッダーに先ほど取得したTokenを埋めつつ、https://management.azure.com/subscriptions/サブスクリプションID/providers/Microsoft.Commerce/UsageAggregates?api-version=2015-06-01-preview&reportedStartTime=取得開始日時&reportedEndTime=取得終了日時&aggreagationGranularity=データ粒度&showDetails=falseというURLにGETリクエストを投げて指定期間の指定したサブスクリプションのJSON形式の使用量データを取得します。

def self.usages(token, subscription_id)
  now = DateTime.now.new_offset(0)
  start_time =  DateTime.new(now.year, now.month, 1, 0, 0, 0)
  end_time =  DateTime.new(now.year, now.month, now.day, 0, 0, 0)

  granularity = "Monthly"

  url = "https://management.azure.com/subscriptions/#{subscription_id}/providers/Microsoft.Commerce/UsageAggregates?api-version=2015-06-01-preview&reportedStartTime=#{url_encode(start_time.to_s)}&reportedEndTime=#{url_encode(end_time.to_s)}&aggreagationGranularity=#{granularity}&showDetails=false"
  headers = {
    "Content-type" => "application/json",
    "Authorization" => "Bearer #{token}"
  }

  results = []
  RestClient.get(url, headers){ |response, request, result, &block|
    case response.code
    when 200
      json = JSON.parse(response)
      return json["value"]
    else
      false
    end
  }
end

3. 料金表をAzure RateCard APIを使って取得

そして料金表をJSON形式で取得します。ここでいままで取得したデータの他にOfferDurableIdという値が必要になります。この値は通常の従量課金形式のライセンスの場合は、MS-AZR-0003Pとなります。詳細はAzureのポータルから自分のサブスクリプションを表示して確認します。 送信先は、https://management.azure.com/subscriptions/サブスクリプションID/providers/Microsoft.Commerce/RateCard?api-version=2015-06-01-preview&$filter=OfferDurableId eq 'OfferDurableIdの値' and Currency eq 'JPY' and Locale eq 'ja-JP' and RegionInfo eq 'JP'となります。最後の方のfilterというパラメータでどんなライセンス形式の価格を、どんな通貨、どんな言語で取得するかを指定できます。ここでは通貨と言語は日本に固定しています。

def self.rate_meters(token, subscription_id, offer_durable_id)
  url = "https://management.azure.com/subscriptions/#{subscription_id}/providers/Microsoft.Commerce/RateCard?api-version=2015-06-01-preview&$filter=OfferDurableId eq '#{offer_durable_id}' and Currency eq 'JPY' and Locale eq 'ja-JP' and RegionInfo eq 'JP'"
  headers = {
    "Content-type" => "application/json",
    "Authorization" => "Bearer #{token}"
  }
  RestClient.get(url, headers){ |response, request, result, &block|
    case response.code
    when 200
      json = JSON.parse(response)
      json["Meters"]
    else
      false
    end
  }
end

4. 値をマージする

あとは2と3で取得したデータをマージすればOKです。2で取得した使用量データには、適用する価格を示すMeterIdという項目があるので、これをキーにしてRateCard APIの結果から単価を取得します。また、使用量データにはquantityという数量を示すデータが入っているので、単価と数量を掛け合わせることで金額が算出できます。

あとは適当にテーブル形式で出力したり、もっとデータをまとめたりと好きにすれば良いでしょう。

ここで解説したコードの完全版は、https://github.com/ryuzee/azure_charge_example においてありますので適宜ご覧ください。サンプルを実行すると以下のような出力を取得できます。