サポートという仕事柄、お客様の Azure 環境の情報を頂いてレビューしたりトラシューしたりするのですが、Azure の情報をとってくる方法というのは中々厄介です。
一応社内ツールもあるものの、いろいろな制約があったりお客様と別の見え方をしたりするので、一番簡単かつ確実な方法はポータルのアクセス権をもらうことなのですが、それはそれでまた別のハードルがあります。
ということで、よさげなツールがないかと探していたもののどうやらなさそうだったので自分で作ってみました。
一番よく使いそうなパターンとしては、こちらで取得してほしいクエリを YAML として提供し、お客様側でこのツールを叩いてもらうような状況を想定してます。
使い方等詳細は、README を読んで頂くとして、今回はこのツールをベースに、Azure SDK for Go と Resource Graph API を使ってどうやって Azure のリソース情報を取得してくるかを紹介していきます。
ちなみに結構ググったのですが、Resource Graph API 周りの公式以外の記事は英語でもなさそうだったのでもしかしたら世界初。。かもしれません。
Azure SDK for Go
まず、このツールの動作環境と処理内容から、マルチ OS と並行処理は外せないなということを最初に考えたので、Go 言語で実装することにしました。幸い、Azure には Go 言語用の SDKがあるので REST API を叩かずに実装できます。
ドキュメントはこちら。
Go 開発者向けの Azure | Microsoft Docs
GoDoc はこちらにあります。
認証ってどうするの?
Azure SDK for Go では、環境に応じて使えるさまざまな認証方法があります。
詳しくはこちらのドキュメントを見てください。
Azure SDK for Go での認証 | Microsoft Docs
今回は、CLI ツールなので、
のいずれかを採用することにしました。(先ほどのツールはまだ Azure CLI の認証情報のみです)
こういうツールを作る時、結構悩ましいのは認証で、特に Azure の閲覧権限しかないとサービスプリンシパルが作れなかったりするので、代替方法があるのは非常に助かりますね。
今回のツールだとこの部分
https://github.com/tsubasaxZZZ/azr/blob/90cef9938e59e9be0d69241c79c8c5facae94064/azure.go#L29
a, err := auth.NewAuthorizerFromCLI()
予め Azure CLI のインストールと az login でサインインしておく必要がありますが、なんとこれだけで認証できます。
あとは、各 API に NewなんとかClient というメソッドが用意されているのでその戻り値の構造体の Authorizer に認証情報をセットしてあげれば準備は完了。
ちなみにこのあたりの流れは GitHub の README に載ってます。
GitHub - Azure/azure-sdk-for-go: Microsoft Azure SDK for Go
1. Import a package from the services directory. 2. Create and authenticate a client with a New*Client func, e.g. c := compute.NewVirtualMachinesClient(...). 3. Invoke API methods using the client, e.g. res, err := c.CreateOrUpdate(...). 4. Handle responses and errors.
初めはお作法が分からず途方に暮れてましたがちゃんと書いてあったのでドキュメントはちゃんと読みましょうということですね。。
Resource Graph API
続いて、Resource Graph API を叩く部分です。SDK ではこちらになります。
あとドキュメントで欠かせないのがこちらの API リファレンス。
Resources - Resources (Azure Azure Resource Graph) | Microsoft Docs
開発中はこの2つのドキュメントとにらめっこしながら実装することになりますが、ヒントとなりそうなあたりをいくつか紹介します。
どーやってリクエストするの?
こちらのメソッドを使います。
Resources
今回のツールでは azure.go のこの部分。
request := &resourcegraph.QueryRequest{ Subscriptions: &[]string{params.subscriptionID}, Query: ¶ms.query, Options: &resourcegraph.QueryRequestOptions{ResultFormat: resourcegraph.ResultFormatTable}, Facets: &facetRequest, } queryResponse, err := client.ResourceGraphClient.Resources(c, *request) if err != nil { return nil, err }
始めに、QueryRequest 構造体を組み立てて、resourcegraphapi.BaseClientAPI の第二引数として渡してあげると、QueryResponse が返ってきます。
QueryResponse
まず、 QueryRequest にリソース グラフのクエリや サブスクリプション ID やらをセットしてあげます。この時、一緒に設定できる、QueryRequestOptions が地味に重要な働きをします。
QueryRequestOptions
// QueryRequestOptions the options for query evaluation type QueryRequestOptions struct { // SkipToken - Continuation token for pagination, capturing the next page size and offset, as well as the context of the query. SkipToken *string `json:"$skipToken,omitempty"` // Top - The maximum number of rows that the query should return. Overrides the page size when ```$skipToken``` property is present. Top *int32 `json:"$top,omitempty"` // Skip - The number of rows to skip from the beginning of the results. Overrides the next page offset when ```$skipToken``` property is present. Skip *int32 `json:"$skip,omitempty"` // ResultFormat - Defines in which format query result returned. Possible values include: 'ResultFormatTable', 'ResultFormatObjectArray' ResultFormat ResultFormat `json:"resultFormat,omitempty"` }
このオプションの中で、ResultFormat があります。セットできるのは、ResultFormatTable
か、ResultFormatObjectArray
です。前者は、Column と Row が map と array の形式で表現され、後者は、列名が map のキーになります。どちらを使うかはその時々に応じて、になりますが、データの型に応じて処理をしたい時は、ResultFormatTable
がおすすめでしょうか。
というのは、Column に型の情報(integerか、stringか、objectか)が入ってくるのでそれに応じて処理を変えられます。
このツールでも、object の場合は、json に Marshal しています。
for i, r := range row.([]interface{}) { // カラムの型に応じて処理を変える switch columns.([]interface{})[i].(map[string]interface{})["type"] { case "integer", "string": _result = append(_result, fmt.Sprint(r)) case "object": // object の場合は JSON 化 j, err := json.Marshal(r) if err != nil { return nil, err } _result = append(_result, string(j)) } }
ResultFormatObjectArray
は、決まった型の構造体にデータを入れたいときに便利だと思います。
for _, elem := range queryResponse.Data.([]interface{}) { b, err := json.Marshal(elem) if err != nil { return nil, err } r := reflect.New(reflect.TypeOf(v).Elem()).Interface() errUnmarshal := json.Unmarshal(b, &r) if errUnmarshal != nil { return nil, errUnmarshal } result = append(result, r) } return result, nil
この方法がベストかどうかがわからないのですが、今回のツールとは別の上のコードでは、v という構造体を別に作って、その情報を reflect して 一旦 Marshal した json を Unmarshal することで、カラム数も型も不定形な QueryResponse.Data を型の決まった構造体に突っ込んでます。
この ResultFormat を説明したドキュメントを見つけました。
Facet って何よ?
QueryRequest に Facets というメンバがいます。
そのままの意味としては切り口とか面とかの意味で、返ってきた結果を切り出して、並び替えたりトップ3を出したり等サマライズするカラムとか条件を指定することが出来ます。
こちらの API リファレンスが参考になるでしょう。
"facets": [ ~~略~~ { "expression": "resourceGroup", "options": { "filter": "resourceGroup contains 'test'", "$top": 3 } }
一部を抜き出したものですが、リクエストの Body で query とは別にこのように facets オプションを指定することで、返ってきた結果に対して、さらにサマライズできます。
重要なのは、QueryResponse とは別の結果として返ってくる点です。リソース グラフのクエリで返ってきた結果に対してさらにフィルターをするようなイメージです。
例えば、特定のリソース グループの中のリソースを全取得するというクエリに対して、上の様に test という文字列を含んだ仮想マシンのみを返すとか、仮想マシンにアタッチされたディスク数でソートした結果を返すとか共通のフィルターを掛ける時に便利かなと思います。
SDK のコードだとこのあたりです。
API が分かっていれば単純に構造体に落としてるだけなので難しくはないでしょう。
ちなみに、このツールでは facet 周りはまだ実装していません。
結果って何個リソースがあっても無限数返ってくるの?
あらゆるリソースは有限です。
大体の一般的な API がそうであるように、Resource Graph API もスロットリングと最大レコード数があります。
いい感じのドキュメントがまとまってます。
スロットルされた要求に関するガイダンス - Azure Resource Graph | Microsoft Docs
こちらのスロットリングですが、5 秒間のウィンドウで最大 15 ということなので、調子に乗ってゴルーチンを 100 とかにするとエラーになる可能性が高くなります。 ツールへの影響だけならまだしも、Resource Graph API を使っている、Azure ポータルの表示にも影響が出そうなのでちゃんと制限してあげた方が良さそうです。(実際影響が出るかは未確認です)
次に、最大のレコード数です。
大規模なデータ セットを処理する - Azure Resource Graph | Microsoft Docs
API 経由でのアクセスは、最大 1000 レコードの上限があります。それなりの規模で、resources とかクエリしたら簡単に超えそうですね。
なので次のドキュメントにあるように、$skipToken にセットされたトークンを使って、ページング処理をしてあげる必要があります。
まぁそもそもリソース グラフのクエリを使って何かをしようとしている状況で 1000 を超える結果が返ってくる状態はクエリがいけてないか、目的にマッチしていない可能性もあるんじゃないかなとも思います。
ちなみに今回のこのツールではページング処理は実装していないですが、対応する予定です。
まとめ
ということで、Resource Graph API と Azure SDK for Go についてつらつら書いてみました。
Go ネタとして、
とか書きたいことがいろいろ出てきたので追って書いてきます。
またこのツールについては引き続きアップデートしてどんどん使いやすくしていく予定なので、ぜひ issue お待ちしています!