20200731のGoに関する記事は4件です。

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 loginAzure 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/usageDetails

1 行目の CSV ヘッダは json,flatten 指定時のキーと同じものです。 JSON 形式では省かれたキーもすべて含まれています。エンコーディングは BOM つきの UTF-8 で、改行コードは CRLF であるため Microsoft Excel で直接開くことができます。

アクセス可能な課金アカウントを CSV 形式で出力します。

$ azbill accounts

アクセス可能な Azure AD テナントを JSONL 形式で出力します。

$ azbill tenants --format json

アクセス可能なサブスクリプションをフラットな JSONL 形式で出力します。

$ azbill subscriptions --format flatten

Microsoft 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/sec

Enterprise 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/sec

azbill の開発

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 コードの生成については、別の記事で詳しく説明したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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文とキャストを組み合わせて処理することで、型に応じたメソッドが利用できるようになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.go
wire_gen.go
func InitReview() handler.Review {
    conn := db.New()
    review := repository.NewReview(conn)
    usecaseReview := usecase.NewReview(review)
    handlerReview := handler.NewReview(usecaseReview)
    return handlerReview

こんな感じの関数が生成されたら成功です。あとは実際にこの関数をhandlerで呼び出してあげれば全て使用することができます。お疲れ様でした。

すごくざっくり書いたので質問や訂正あればコメントお願いします。以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 は、以前はエクスポートされていない埋め込みフィールドへのアクセスを許可していましたが、エクスポートされていないすべてのフィールドのメソッドへのアクセスを禁止するようになりました.」(原文: Package reflect 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 にある. これを知っていれば一発で意味が取れたなあ.

まあ、この変更で困る人はほとんどいなそうですね.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む