Go
Azure

go-autorest を使って、Azure の生REST-API をしばき上げる。

Azure の Go 関係の SDK いくつかあるのだが、Azure Go SDK でも内部で使われているのが、このgo-autorestだ。これは、Active Directory 系のサービスやクライアント、バリデーション、モックなどの処理が含まれており、swagger ベースの Autorestプロジェクトで生成した、GO ライブラリを使うようになっている。

Azure SDK for Go の仕組み

Azure SDK for Goは、このgo-autorestをベースに、Azure Rest API Specから構造体などを生成したものが、Azure SDK for GO である。大抵はそれでいいのだが、Azure Data Factory が、Azure SDK に含まれていないことに気がついた。しかも、Azure Rest API Specの方にも含まれていないから、将来も追加されるかわからない。とりあえずプロダクションチームには報告して置いたが、こう言う時どうすればいいだろう。
 単純に、生で、go-autorest を生で使えば良い。ちなみに、今の所ドキュメントが公開されていないので、公開して置いた。zure Go Library Documentation Project を辿ったら、API リファレンスを見つけられます。

基本的なサンプルの解説。

1つ基本的なサンプルを、go-autorest のみを使って書いてみましょう。私は先のGo の JSON のパースと nil の扱い segmentation violation 対策のことがわかっていなかったため、ものすごく解析に時間がかかりましたが、わかってしまえばあまり難しくありません。

今回はサンプルとして、Azure Data FactoryCreate Or Update を参考に簡単なものを書いてみます。

この仕様を実装してみます。

1. Service Principle Token の取得

Azure にアクセスするためには、トークンを取得する必要があります。大抵のコマンドラインでは、インタラクティブに操作をせず自動化したいでしょう。そのケースでは、Service Principle を取得しておくことをオススメします。Service Principle の作り方は、 Create an Azure service principal with Azure CLI 2.0

サービスプリンシパル部分を解説します。ポイントは、adal.NewOAuthConfig メソッドです。adal は、ActiveDirectoryAuthenticationLibraryの略です。まずは、REST-API のエンドポイントと、TENANT_ID を渡して、新しいコンフィグを作ります。

func main() {
    var spt *adal.ServicePrincipalToken
    var err error

    c := map[string]string{
        "AZURE_CLIENT_ID":       os.Getenv("AZURE_CLIENT_ID"),
        "AZURE_CLIENT_SECRET":   os.Getenv("AZURE_CLIENT_SECRET"),
        "AZURE_SUBSCRIPTION_ID": os.Getenv("AZURE_SUBSCRIPTION_ID"),
        "AZURE_TENANT_ID":       os.Getenv("AZURE_TENANT_ID"),
    }

    oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, c["AZURE_TENANT_ID"])
    if err != nil {
        panic(err)
    }

次に、NewServicePrincipalToken メソッドで、サービスプリンシパルトークンを取得します。ちなみに、サービスプリンシパルトークンを取得するためには、Azure にリクエストを送らないといけないのですが、ポイントとしてはこの時点では送っていません。ここがポイントです。

    spt, err = adal.NewServicePrincipalToken(*oauthConfig, c["AZURE_CLIENT_ID"], c["AZURE_CLIENT_SECRET"], azure.PublicCloud.ResourceManagerEndpoint)

    if err != nil {
        fmt.Printf("SPT ERROR: %v", err)
        return
    }

サービスプリンシパルの、Refresh メソッドを叩いた時に、実際のトークンが取得されます。トークンをリフレッシュしたい場合もこのメソッドです。

    err = spt.Refresh()
    if err != nil {
        fmt.Printf("SPT Error %v", err)
    }

取得したトークンをクライアントにセットしておきます。


type Client struct {
    autorest.Client
}
:

    client := &Client{}

    client.Authorizer = autorest.NewBearerAuthorizer(spt)

ちなみに、type Client struct { autorest.Client } ですが、このようにしておくと、Client の属性であるかのように、autorest.Client の属性を扱えます。

まずはここまでで、サービスプリンシパルの用意は終了。

2. リクエストの準備

この、go-autorest では、3段階で、メッセージを送信/処理します。

  • Prepare
  • Send
  • Respond

それぞれ解説していきます。

2.1. Prepare

    pathParameters := map[string]interface{}{
        "ScbscriptionId":    autorest.Encode("path", c["AZURE_SUBSCRIPTION_ID"]),
        "ResourceGroupName": autorest.Encode("path", resourceGroup),
        "DataFactoryName":   autorest.Encode("path", name),
    }

    queryParameters := map[string]interface{}{
        "api-version": "2015-10-01",
    }

    body := &DataFactoryRequest{
        Name:     ToStringAddress("gosdktestadfname03"),
        Location: ToStringAddress("West US"),
        Tags:     &map[string]*string{},
    }

    jsonBytes, _ := json.Marshal(body)
    fmt.Println(string(jsonBytes))

    p := autorest.CreatePreparer(
        autorest.AsJSON(),
        autorest.AsPut(),
        //      autorest.WithBearerAuthorization(spt.AccessToken), // 不要
        autorest.WithBaseURL("https://management.azure.com"),
        autorest.WithPathParameters("/subscriptions/{ScbscriptionId}/resourcegroups/{ResourceGroupName}/providers/Microsoft.DataFactory/datafactories/{DataFactoryName}", pathParameters),
        autorest.WithQueryParameters(queryParameters),
        autorest.WithJSON(body))

    req, err := p.Prepare(&http.Request{})
    if err != nil {
        fmt.Printf("ERROR: %v¥n", err)
    } else {
        fmt.Println(req.URL)
    }

autorest.CreatePreparer メソッドと、そこでサポートされているデコレーターで、リクエストを作っていきます。このREST-API は、PUT でそうしんします。

AsJSON(), AsPut がHeader系のでコレータです。例えば、AsJSON() は、ヘッダに、Content-Type: application/json を渡します。だいたい他は想像つくと思いますが、

WithBaseURL + WithPathParameters + WithQueryParameters で、URL が出来上がります。WithJSONは、Body部の内容です。struct を作って渡してあげれば、そのjson が出来上がります。

PUT https://management.azure.com/subscriptions/someresource/resourcegroups/SpikeADF03/providers/Microsoft.DataFactory/datafactories/gosdktestadfname01?api-version=2015-10-01
{"name":"gosdktestadfname01","location":"West US","tags":{}}

例えばこんな感じで送られます。

最後に、Prepareメソッドで、リクエストオブジェクトを作ります。

2.2. Send

ここでは純粋に、リクエストを送信します。一つのポイントは、実は、リクエストを作るまでの家庭では、Service Principle Token は、ヘッダの Authenticate: Bearer のヘッダにセットされていないことです。最初はデバッガで挙動をみて驚きましたが、この時点で、ちゃんとヘッダにセットされているようです。こちらも、同じく、いくつかのデコレーターがありますので、お好みでセットしておきます。

    logger := log.New(os.Stdout, "autorest: ", log.Lshortfile)
    logger.Println("*****Hello*****")
    res, err := autorest.SendWithSender(client, req,
        autorest.WithLogging(logger),
        autorest.DoErrorIfStatusCode(http.StatusAccepted),
        autorest.DoCloseIfError())
    // autorest.DoRetryForAttempts(5, time.Duration(0)))
    if err != nil {
        fmt.Printf("ERROR: %v¥n", err)
        return
    }

2.3. Respond

最後に取得した結果を、フォーマットします。大抵は、JSON にパースするでしょう。

    v := &DataFactoryCreateOrUpdateParameter{}
    fmt.Printf("Status Code: %d", res.StatusCode)

    autorest.Respond(res,
        autorest.ByUnmarshallingJSON(v),
        autorest.ByClosing())

ハマったところ

こんな単純なのに、私は結構はまりました。Go の JSON のパースと nil の扱い segmentation violation 対策の挙動で、しょっちゅう、segmentation violation が出ていたのと、400 BadRequestを受け取っていたのですが、何が悪くてそうなっているのかわからず、パースもうまくできていなかったため、原因もわからない的な辛さでした。でも、この解決策は簡単です。UnMarshalling に頼らず、Body 部を表示してあげれば、エラーもわかります。

    buf := new(bytes.Buffer)
    buf.ReadFrom(res.Body)
    newStr := buf.String()
    fmt.Printf("**********This: %s", newStr)

こんな感じにすると、エラーががっつり見えます。ムッチャ当たり前やけど、さっきのsegmentation violation と重なり合って、しかも、最初は、Service Principle のtoken がセットされない問題(Refleshがわからなかった) で、わけがわかりませんでした。が、最初からこれをやっていれば、簡単にわかったのになと思っています。400の原因は、URIのData Factory 名と、Bodyの DataFactory名が、違うからでした。orz

received 400 Bad Request
Status Code: 400**********This: {"error":{"code":"MismatchingResourceName","message":"The provided resource name 'datafactory' did not match the name in the Url 'gosdktestadfname01'."}}

この後も、

プログラム全体

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/Azure/go-autorest/autorest"
    "github.com/Azure/go-autorest/autorest/adal"
    "github.com/Azure/go-autorest/autorest/azure"
)

type Client struct {
    autorest.Client
}

type DataFactoryRequest struct {
    Name     *string             `json:"name,omitempty"`
    Location *string             `json:"location,omitempty"`
    Tags     *map[string]*string `json:"tags,omitempty"`
    // Properties  Unknown. I'm asking via the website.
}

type DataFactoryCreateOrUpdateParameter struct {
    ID                                   *string             `json:"id,omitempty"`
    Name                                 *string             `json:"name,omitempty"`
    Type                                 *string             `json:"type,omitempty"`
    Location                             *string             `json:"location,omitempty"`
    Tags                                 *map[string]*string `json:"tags,omitempty"`
    *DataFactoryCreateOrUpdateProperties `json:"properties,omitempty"`
}

type DataFactoryCreateOrUpdateProperties struct {
    DataFactoryID     *string `json:"dataFactoryId,omitempty"`
    ProvisioningState *string `json:"provisioningState,omitempty"`
    Error             *string `json:"error,omitempty"`
    ErrorMessage      *string `json:"errorMessage,omitempty"`
}

func ToStringAddress(str string) *string {
    return &str
}

func main() {
    var spt *adal.ServicePrincipalToken
    var err error

    resourceGroup := "SpikeADF03"
    name := "gosdktestadfname03"

    c := map[string]string{
        "AZURE_CLIENT_ID":       os.Getenv("AZURE_CLIENT_ID"),
        "AZURE_CLIENT_SECRET":   os.Getenv("AZURE_CLIENT_SECRET"),
        "AZURE_SUBSCRIPTION_ID": os.Getenv("AZURE_SUBSCRIPTION_ID"),
        "AZURE_TENANT_ID":       os.Getenv("AZURE_TENANT_ID"),
    }

    oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, c["AZURE_TENANT_ID"])
    if err != nil {
        panic(err)
    }
    fmt.Printf("AZURE_CLIENT_ID %s", c["AZURE_CLIENT_ID"])
    fmt.Printf("AZURE_CLIENT_SECRET %s", c["AZURE_CLIENT_SECRET"])
    fmt.Printf("AZURE_SUBSCRIPTION %s", c["AZURE_SUBSCRIPTION_ID"])
    fmt.Printf("AZURE_TENANT_ID %s", c["AZURE_TENANT_ID"])
    spt, err = adal.NewServicePrincipalToken(*oauthConfig, c["AZURE_CLIENT_ID"], c["AZURE_CLIENT_SECRET"], azure.PublicCloud.ResourceManagerEndpoint)

    if err != nil {
        fmt.Printf("SPT ERROR: %v", err)
        return
    }

    err = spt.Refresh()
    if err != nil {
        fmt.Printf("SPT Error %v", err)
    }

    client := &Client{}

    client.Authorizer = autorest.NewBearerAuthorizer(spt)

    fmt.Printf("Token: %s¥n", spt.AccessToken)

    pathParameters := map[string]interface{}{
        "ScbscriptionId":    autorest.Encode("path", c["AZURE_SUBSCRIPTION_ID"]),
        "ResourceGroupName": autorest.Encode("path", resourceGroup),
        "DataFactoryName":   autorest.Encode("path", name),
    }

    queryParameters := map[string]interface{}{
        "api-version": "2015-10-01",
    }

    body := &DataFactoryRequest{
        Name:     ToStringAddress("gosdktestadfname03"),
        Location: ToStringAddress("West US"),
        Tags:     &map[string]*string{},
    }

    jsonBytes, _ := json.Marshal(body)
    fmt.Println(string(jsonBytes))

    p := autorest.CreatePreparer(
        autorest.AsJSON(),
        autorest.AsPut(),
        //      autorest.WithBearerAuthorization(spt.AccessToken),
        autorest.WithBaseURL("https://management.azure.com"),
        autorest.WithPathParameters("/subscriptions/{ScbscriptionId}/resourcegroups/{ResourceGroupName}/providers/Microsoft.DataFactory/datafactories/{DataFactoryName}", pathParameters),
        autorest.WithQueryParameters(queryParameters),
        autorest.WithJSON(body))

    req, err := p.Prepare(&http.Request{})
    if err != nil {
        fmt.Printf("ERROR: %v¥n", err)
    } else {
        fmt.Println(req.URL)
    }

    // buf := new(bytes.Buffer)
    // buf.ReadFrom(req.Body)
    // newStr := buf.String()
    // fmt.Printf(newStr)
    // if req != nil {
    //  return
    // }

    logger := log.New(os.Stdout, "autorest: ", log.Lshortfile)
    logger.Println("*****Hello*****")
    res, err := autorest.SendWithSender(client, req,
        autorest.WithLogging(logger),
        autorest.DoErrorIfStatusCode(http.StatusAccepted),
        autorest.DoCloseIfError())
    // autorest.DoRetryForAttempts(5, time.Duration(0)))
    if err != nil {
        fmt.Printf("ERROR: %v¥n", err)
        return
    }

    v := &DataFactoryCreateOrUpdateParameter{}
    fmt.Printf("Status Code: %d", res.StatusCode)
    // buf := new(bytes.Buffer)
    // buf.ReadFrom(res.Body)
    // newStr := buf.String()
    // fmt.Printf("**********This: %s", newStr)

    autorest.Respond(res,
        autorest.ByUnmarshallingJSON(v),
        autorest.ByClosing())

    fmt.Printf("ProvisioningState: %s", *v.DataFactoryCreateOrUpdateProperties.ProvisioningState)

    //  fmt.Printf("%s %s %s", *v.DataFactoryCreateOrUpdateProperties.ProvisioningState, *v.DataFactoryCreateOrUpdateProperties.Error, *v.DataFactoryCreateOrUpdateProperties.ErrorMessage)

}

最後に、どうやったらはまりを避けられるのだろう?

デバッガとか使ってるのに、Go の JSON のパースと nil の扱い segmentation violation 対策の問題で、パースできず、いろんな問題が出て、一日を費やしました。最後には、Core Dump して調べようとしていました。

しかし、問題はそんなに複雑ではなく、そもそも、私は、REST-API を生で叩いたことがあるので、どういうプロセスであるかは、理解していたのにハマったのです。

もちろん、元のサンプルやドキュメントがわかりにくいということもあると思いますが、いくつかの注意でここまでハマらなかったと思います。

  • 問題を小さい単位に分割して、一つ一つ解決する
  • Manual に目を通す
  • 意味もわからず試行錯誤する暇があったら、意味を調べる。
  • 同じく、コードを読む
  • デバッガで nil を調べる

仕様通りにすれば間違いなく動くのが基本なので、間違っているのは自分なのです。一気に難しいところをやろうとするからダメで、例えば、私は、Go の Marshal Unmarshal もよくわかっていなかったし、構造体と、nil ポインタの関係も理解できていませんでした。自分がわからない部分は、一つのメソッドぐらいまでブレイクダウンして、挙動を理解すべきでした。なんとなくで、動くことを目指すから遅くなります。
だから、初回は人より3倍時間をかけてもいいから、一つ、一つ小さな単位に分割して、理解していくのが、結局最速への道なのかと思いました。

他にいいアドバイスがある人がおられましたら、是非ご指導ください。