- 投稿日:2020-07-31T23:03:20+09:00
azbill: Azure 利用明細・課金情報出力 CLI の紹介
はじめに
azbill という Azure クラウドサービスの利用明細・課金情報を出力してくれる CLI ツールを Go 言語で作ったので紹介します。主に Enterprise Agreement (EA) で Azure を導入している組織向けで、例えば部署ごと・リソースの種類ごと・タグごとの使用量を算出するといったようなコスト分析のためのデータを出力するツールです。
azbill を使うと EA の課金アカウントや Azure サブスクリプションに含まれるすべてのリソースの使用量・課金額を日次で取得し、CSV や JSONL の形式でファイルに保存できます。
このような利用明細の取得は Azure CLI でもできると思われるかもしれませんが、現行の Azure CLI は新しいバージョンの Consumption API に対応しておらず、予約 VM の利用状況といったような情報がとれません。 azbill を使えばそのような最新のメトリクスが取得できますし、CSV やフラットな JSON といったような、気の利いた出力が可能です。
azbill は現時点で次の種類の課金アカウントに対応しています。
- Microsoft Online Services Program (個人のクレジットカードによる従量課金契約はこれに含まれます)
- Enterprise Agreement (企業向けの年間契約)
インストール
azbill には Windows・macOS・Linux 向けにビルドされたバイナリがありますのでそれをダウンロードしご利用してください。ひとつの実行ファイルのみで動作します。
Go がインストールされているシステムであれば、次のコマンドでインストールすることもできます。
$ go get -u github.com/yaegashi/azbill使用法
多くのコマンドラインオプションや環境変数による設定があります。
azbill -h
で表示されるヘルプを参照してください。$ azbill -h Azure billing data exporter Usage: azbill [command] Available Commands: accounts List billing accounts you have access to help Help about any command invoices List invoices login Force auth-dev login subscriptions List subscriptions tenants List tenants usage-details List usage details Flags: --auth string auth source [dev,env,file,cli] (env:AZBILL_AUTH, default:dev) --auth-dev string auth dev store (env:AZBILL_AUTH_DEV, default:auth_dev.json) --auth-file string auth file store (env:AZBILL_AUTH_FILE, default:auth_file.json) --client string Azure client (env:AZURE_CLIENT_ID, default:4a034c56-da44-48ce-90db-039a408974bd) --config-dir string config dir (env:AZBILL_CONFIG_DIR, default:~/.azbill) --format string output format [csv,json,flatten,pretty] (env:AZBILL_FORMAT, default:csv) -h, --help help for azbill -o, --output string output file -q, --quiet quiet --tenant string Azure tenant (env:AZURE_TENANT_ID, default:common) -v, --version version for azbill Use "azbill [command] --help" for more information about a command.認証
azbill login
で Azure AD のデバイス承認フローによるサインインができます。 ブラウザで https://microsoft.com/devicelogin を開き、表示されたコードを入力した後にサインインしてください。$ azbill login 2020/07/06 23:05:05 To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code HL94ZL7Y8 to authenticate. 2020/07/06 23:05:21 Saving auth-dev token in /Users/yaegashi/.azbill/auth_dev.jsonホームとは異なるテナントにゲストとしてサインインするには
--tenant
で指定してください。$ azbill login --tenant l0wdev.onmicrosoft.com認証トークンは
~/.azbill/auth_dev.json
ファイルに保存されます。--auth-dev
オプションにより保存するファイルを指定できます。相対パスを指定した場合は--config-dir
で指定したディレクトリ (デフォルト~/.azbill
) 内に保存されます。また Azure Blob Storage の SAS つき URL を指定することもできます。これは CI/CD サービスの環境で azbill を実行する場合に便利です。 SAS つき URL は秘匿すべき情報ですので、
--auth-dev
ではなく環境変数AZBILL_AUTH_DEV
で設定するようにしてください。Azure SDK for Go がサポートする他の認証方式を使うこともできます。 Azure CLI で認証済みの場合は
--auth cli
を使うのが簡単です。 Azure CLI で生成したサービスプリンシパルの JSON ファイルを使う場合は--auth file
を使い、ファイルの場所を--auth-file
で指定してください。出力フォーマット
出力フォーマットは
--format
で指定します。基本はcsv
またはjson
の 2 種類です。カンマ区切りでflatten
またはpretty
を追加指定できます。
--format
説明 csv
1レコードが 1 行のCSV 形式で出力します (デフォルト) json
1 レコードが 1 JSON オブジェクトで、それらを改行で区切った JSONL 形式で出力します json,flatten
入れ子でない JSON オブジェクトを出力します。 flatten
でも同じ意味です。json,pretty
インデントつきの JSON オブジェクトを出力します。 pretty
でも同じ意味です。json,flatten,pretty
入れ子でないインデントつきの JSON オブジェクトを出力します。 flatten,pretty
でも同じ意味です。JSON 形式
次は
--format json,pretty
を指定した時の 1 レコードの出力です。 JSON 形式の場合、丸め誤差を避けるために数字は文字列で表現されていることに注意してください。{ "properties": { "billingPeriodStartDate": "2020-05-03T00:00:00Z", "billingPeriodEndDate": "2020-06-02T00:00:00Z", "billingProfileId": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "billingProfileName": "Pay-As-You-Go", "subscriptionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "subscriptionName": "Pay-As-You-Go", "date": "2020-06-01T00:00:00Z", "meterId": "d54686f0-77ff-43f3-9e7c-2099030d32a7", "meterDetails": { "meterName": "Public Queries", "meterCategory": "Azure DNS", "meterSubCategory": "", "unitOfMeasure": "10000000" }, "quantity": "0.000997", "effectivePrice": "55.3250345781466", "cost": "0.055159059474412", "unitPrice": "0", "billingCurrency": "JPY", "resourceLocation": "Unknown", "consumedService": "Microsoft.Network", "resourceId": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/dns/providers/Microsoft.Network/dnszones/l0w.dev", "resourceName": "l0w.dev", "resourceGroup": "dns", "offerId": "MS-AZR-0003P", "isAzureCreditEligible": false, "publisherType": "Azure", "chargeType": "Usage", "frequency": "UsageBased" }, "id": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Billing/billingPeriods/202006/providers/Microsoft.Consumption/usageDetails/de545724-82e8-f099-bc3b-53c5f0ae1711", "name": "de545724-82e8-f099-bc3b-53c5f0ae1711", "type": "Microsoft.Consumption/usageDetails", "tags": null, "kind": "legacy" }フラット形式
次は
--format json,flatten,pretty
を指定した時の出力です。フラットな JSON オブジェクトとなります。{ "id": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Billing/billingPeriods/202006/providers/Microsoft.Consumption/usageDetails/de545724-82e8-f099-bc3b-53c5f0ae1711", "kind": "legacy", "name": "de545724-82e8-f099-bc3b-53c5f0ae1711", "properties.billingCurrency": "JPY", "properties.billingPeriodEndDate": "2020-06-02T00:00:00Z", "properties.billingPeriodStartDate": "2020-05-03T00:00:00Z", "properties.billingProfileId": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "properties.billingProfileName": "Pay-As-You-Go", "properties.chargeType": "Usage", "properties.consumedService": "Microsoft.Network", "properties.cost": "0.055159059474412", "properties.date": "2020-06-01T00:00:00Z", "properties.effectivePrice": "55.3250345781466", "properties.frequency": "UsageBased", "properties.isAzureCreditEligible": false, "properties.meterDetails.meterCategory": "Azure DNS", "properties.meterDetails.meterName": "Public Queries", "properties.meterDetails.meterSubCategory": "", "properties.meterDetails.unitOfMeasure": "10000000", "properties.meterId": "d54686f0-77ff-43f3-9e7c-2099030d32a7", "properties.offerId": "MS-AZR-0003P", "properties.publisherType": "Azure", "properties.quantity": "0.000997", "properties.resourceGroup": "dns", "properties.resourceId": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/dns/providers/Microsoft.Network/dnszones/l0w.dev", "properties.resourceLocation": "Unknown", "properties.resourceName": "l0w.dev", "properties.subscriptionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "properties.subscriptionName": "Pay-As-You-Go", "properties.unitPrice": "0", "tags": null, "type": "Microsoft.Consumption/usageDetails" }CSV 形式
次は
--format csv
を指定した時の出力です。id,kind,name,properties.accountName,properties.accountOwnerId,properties.additionalInfo,properties.billingAccountId,properties.billingAccountName,properties.billingCurrency,properties.billingPeriodEndDate,properties.billingPeriodStartDate,properties.billingProfileId,properties.billingProfileName,properties.chargeType,properties.consumedService,properties.cost,properties.costCenter,properties.date,properties.effectivePrice,properties.frequency,properties.invoiceSection,properties.isAzureCreditEligible,properties.meterDetails.meterCategory,properties.meterDetails.meterName,properties.meterDetails.meterSubCategory,properties.meterDetails.serviceFamily,properties.meterDetails.unitOfMeasure,properties.meterId,properties.offerId,properties.partNumber,properties.planName,properties.product,properties.productOrderId,properties.productOrderName,properties.publisherName,properties.publisherType,properties.quantity,properties.reservationId,properties.reservationName,properties.resourceGroup,properties.resourceId,properties.resourceLocation,properties.resourceName,properties.serviceInfo1,properties.serviceInfo2,properties.subscriptionId,properties.subscriptionName,properties.term,properties.unitPrice,tags,type /subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Billing/billingPeriods/202006/providers/Microsoft.Consumption/usageDetails/de545724-82e8-f099-bc3b-53c5f0ae1711,legacy,de545724-82e8-f099-bc3b-53c5f0ae1711,,,,,,JPY,2020-06-02T00:00:00Z,2020-05-03T00:00:00Z,/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX,Pay-As-You-Go,Usage,Microsoft.Network,0.055159059474412,,2020-06-01T00:00:00Z,55.3250345781466,UsageBased,,false,Azure DNS,Public Queries,,,10000000,d54686f0-77ff-43f3-9e7c-2099030d32a7,MS-AZR-0003P,,,,,,,Azure,0.000997,,,dns,/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/dns/providers/Microsoft.Network/dnszones/l0w.dev,Unknown,l0w.dev,,,XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX,Pay-As-You-Go,,0,{},Microsoft.Consumption/usageDetails1 行目の CSV ヘッダは
json,flatten
指定時のキーと同じものです。 JSON 形式では省かれたキーもすべて含まれています。エンコーディングは BOM つきの UTF-8 で、改行コードは CRLF であるため Microsoft Excel で直接開くことができます。例
アクセス可能な課金アカウントを CSV 形式で出力します。
$ azbill accountsアクセス可能な Azure AD テナントを JSONL 形式で出力します。
$ azbill tenants --format jsonアクセス可能なサブスクリプションをフラットな JSONL 形式で出力します。
$ azbill subscriptions --format flattenMicrosoft Online Service Program アカウント: サブスクリプション
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
の 2020 年 6 月の利用明細をフラットなインデントした JSONL 形式で出力します。$ azbill usage-details --format flatten,pretty -S XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX --start 2020-06-01 --end 2020-06-30 -o usage.jsonl 2020/07/06 23:43:59 Loading auth-dev token in /Users/yaegashi/.azbill/auth_dev.json 2020/07/06 23:43:59 Requesting with consumption.UsageDetailsClient 2020/07/06 23:43:59 scope: "subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" 2020/07/06 23:43:59 filter: "properties/usageStart eq '2020-06-01' and properties/usageEnd eq '2020-06-30'" 2020/07/06 23:44:11 Writing to "usage.jsonl" in json,flatten,pretty . 138 records 2020/07/06 23:44:11 Done 138 records in 16.893702ms, 8168.724653 records/secEnterprise Agreement アカウント: 課金アカウント
XXXXXXXX
のビリング期間202006
の利用明細を CSV 形式で出力します。$ azbill usage-details --format csv -A XXXXXXXX -P 202006 -o usage.csv 2020/07/06 23:45:15 Loading auth-dev token in /Users/yaegashi/.azbill/auth_dev.json 2020/07/06 23:45:15 Requesting with consumption.UsageDetailsClient 2020/07/06 23:45:15 scope: "providers/Microsoft.Billing/billingAccounts/XXXXXXXX/providers/Microsoft.Billing/billingPeriods/202006" 2020/07/06 23:45:15 filter: "" 2020/07/06 23:45:34 Writing to "usage.csv" in csv .................................................. 5000 records .................................................. 10000 records .................................................. 15000 records .................... 17001 records 2020/07/06 23:46:38 Done 17001 records in 1m4.325743007s, 264.295431 records/secazbill の開発
azbill の CLI コマンド階層およびフラグ処理には cobra-comder を使用しています (cobra-cmder で Go の CLI を簡単に作る を参照)。
azbill では Azure SDK for Go を使用していますが、現行の SDK ではまだ対応していない新しいバージョンの Azure REST API を使用しています。 バージョン 2019-11-01 の Consumption 利用明細 API などです。
このため AutoRest ツールにより、新しいバージョンの OpenAPI 仕様書から SDK をコード生成しています。生成されたコードは azure-sdk-for-go ディレクトリにあります。
生成を自動的に行うシェルスクリプトが gen.sh です。このような AutoRest による Azure SDK コードの生成については、別の記事で詳しく説明したいと思います。
- 投稿日:2020-07-31T18:28:36+09:00
Golangで再帰関数を書く
仕事でGoで再帰関数を書いた時に苦戦したので備忘録として残しておきます。
サンプルコード
func unpackStruct(data interface{}) interface{} { switch d := data.(type) { case *structpb.Struct: _metadata := make(map[string]interface{}) for k, x := range d.Fields { _metadata[k] = unpackStruct(x.GetKind()) } return _metadata case *structpb.Value_StructValue: _metadata := make(map[string]interface{}) for k, x := range d.StructValue.Fields { _metadata[k] = unpackStruct(x.GetKind()) } return _metadata case *structpb.Value_ListValue: _metadata := make([]interface{}, len(d.ListValue.Values)) for i, x := range d.ListValue.Values { _metadata[i] = unpackStruct(x.GetKind()) } return _metadata case *structpb.Value_StringValue: return d.StringValue case *structpb.Value_NumberValue: return d.NumberValue case *structpb.Value_BoolValue: return d.BoolValue case *structpb.Value_NullValue: return d.NullValue default: return d } }内容としては、gRPCのgoogle.protobuf.Struct型をパースする処理です。
gRPCで受け取ったデータをDBに書き込む、といった処理はよくあると思うのですが、そのまま書き込むと取り出す時に非常に読みづらい形式になってしまうため取得時にパースしてやる必要があります。こういった処理はライブラリがあると思うのですが、ぱっと探したところ無いように見えたので自力で書きました。
Golangで再帰関数を書く時の注意点
再帰関数を書く場合のユースケースとして引数に与えた型によって条件を分岐する場合があるかと思います。
pythonやJavascriptのような動的型付けの言語では気にする必要は無いのですが、Goのような静的型付けの言語では引数の型をしなければならず、かつ指定すると型に応じたメソッドを呼び出せない、というジレンマに陥ってしまいます。(単にGoビギナーなだけですが...)この問題を解消する手段としてinterface型を引数に指定します(interface型は特殊な型でどのようなデータ型も許容できます)。
interface型として与えた引数を「switch d := data.(type)」のようにswitch文とキャストを組み合わせて処理することで、型に応じたメソッドが利用できるようになります。
- 投稿日:2020-07-31T10:08:07+09:00
【Go】依存性注入ライブラリwireの使い方【DI】
Hello, World
DI とは
Weblio辞書から引用
DIとは、プログラミングにおけるデザインパターン(設計思想)の一種で、オブジェクトを成立させるために必要となるコードを実行時に注入(Inject)してゆくという概念のこと現在Go言語でクリーンアーキテクチャを使用して開発をしているのですが、、依存関係のメソッドを呼び出すのにwireを利用したのでメモしておきます。
wireの使い方
ターミナルgo get github.com/google/wire/cmd/wireプロジェクトにdiディレクトリを作成し、その中にwire.goを作成して以下を記述します。
wire.go// +build wireinject package di func InitReview() handler.Review { wire.Build( db.New, repository.NewReview, usecase.NewReview, handler.NewReview, ) return nil //明示的にnilを返す }このwire.Buildのカッコ内に依存関係を解決するメソッドを全て(順不同)入れます。
クリーンアーキテクチャを採用している場合だと例えばrepositoryはDBの値を実際に操作するパッケージ(一番外側のinfra層になる)としていますが、これをusecaseで呼び出すときに外側のinfra層からは呼び出せないので、usecase層よりも内側で空のインターフェースの中の関数を呼び出しています。その依存関係を解決して実際のinfraの関数をdiを使って呼び出しています。このコードをもとに、実際に使うメソッドを生成するコマンドを打ちます。
ターミナルwire ./di/wire.gowire_gen.gofunc InitReview() handler.Review { conn := db.New() review := repository.NewReview(conn) usecaseReview := usecase.NewReview(review) handlerReview := handler.NewReview(usecaseReview) return handlerReviewこんな感じの関数が生成されたら成功です。あとは実際にこの関数をhandlerで呼び出してあげれば全て使用することができます。お疲れ様でした。
すごくざっくり書いたので質問や訂正あればコメントお願いします。以上です。
- 投稿日:2020-07-31T04:10:23+09:00
Go 1.15 の reflect パッケージの変更の影響詳細について
Go 1.15 の reflect パッケージの変更の影響詳細について
Go 1.15 リリースノート 日本語訳 として、reflect パッケージの変更内容を日本語訳したものの、いまいち何を言っているのか分からなかったので確認してみた.
package main import "reflect" type u struct{} func (u) M() {} type t struct { u u2 u } func main() { reflect.ValueOf(t{}).Method(0).Call(nil) // no problem reflect.ValueOf(t{}).Field(0).Method(0).Call(nil) // panic on go1.15 reflect.ValueOf(t{}).Field(1).Method(0).Call(nil) // panic on go1.14 and go1.15 }要するに go1.14 ではエクスポートされていないフィールドへアクセスすると
panic: reflect: reflect.flag.mustBeExported using value obtained using unexported field
でパニックするのだが、唯一埋め込みフィールドだけが例外になっていた. これについても一貫性のため同様にパニックするようにしたようだ.これが「パッケージ
reflect
は、以前はエクスポートされていない埋め込みフィールドへのアクセスを許可していましたが、エクスポートされていないすべてのフィールドのメソッドへのアクセスを禁止するようになりました.」(原文: Packagereflect
now disallows accessing methods of all non-exported fields, whereas previously it allowed accessing those of non-exported, embedded fields.) の意味するところだ.で、そのアクセスを使ってしまっている場合の対応だが、埋め込みフィールドに対してメソッドを呼ばずに、埋め込みの外側の値に対してメソッドを呼べばいい. それが出来るがゆえに、継承みたいなことをしたければ埋め込みフィールドを使えと言われているわけだから.
これが「以前の動作に依存しているコードは、代わりに取り囲む変数に対応する昇格メソッドにアクセスするように更新する必要があります.」(原文: Code that relies on the previous behavior should be updated to instead access the corresponding promoted method of the enclosing variable.) の意味するところだ.
promoted method という言葉を知らなかったので意味が取れてなかったのだが、この言葉は仕様書に出ていた. The Go Programming Language Specification - Struct types にある. これを知っていれば一発で意味が取れたなあ.
まあ、この変更で困る人はほとんどいなそうですね.