はじめに TIG DXユニットの岸下です。2022年2月にキャリア入社して、早4ヶ月経ちました。時が流れるのは早いですね。
参加しているプロジェクトで、Google Workspace Admin SDKのDirectory APIを使った開発を行いました。
本記事では、失敗談をテーマにした連載 として、APIを利用した際に500エラーを頻発させてしまった件について執筆していこうと思います。
結構あるあるな失敗なので、これから開発に入っていく新入社員・初学者の方にはぜひ読んで頂きたい内容となっております。
Google Workspace Admin SDKとDirectory APIについて Google WorkspaceはGoogleが提供する組織向けオンラインアプリケーションセットです。
Google Workspace Admin SDK はGoogle Workspaceに存在する情報を取得するための管理者向けSDKになっています。 また、Directory API はGoogle Workspaceで利用しているドメインのユーザーや繋がっているデバイス、サードパーティアプリケーションを管理したり、取得したりできます。
何をしていたのか 今回、Google Workspace上でグループ化された情報(グループの人数、グループのメールアドレス、グループメンバーのメールアドレスなど)を取得する必要がありました。
Google Workspaceのグループ化について
Google Workspaceではアカウントのグルーピングが可能です。
これにはGoogle Cloud Platform(GCP) 上で、グループに対してIAMロールを付与することができるという恩恵があり、グループに所属しているメンバー全員に対してGCPリソースの権限管理ができます。(例えば、グループAにはGoogle Cloud Storageの管理者権限、グループBにはGoogle Cloud Storageの閲覧権限のみなど)
何が起きたのか 開発環境
リクエスト間隔を考慮しなかったがために、500エラーを乱発 以下、サンプルコードになります。
main.go package mainimport ( "context" "fmt" "io/ioutil" "os" "golang.org/x/oauth2/google" admin "google.golang.org/api/admin/directory/v1" "google.golang.org/api/option" ) type Group struct { groupEmail string groupName string numberOfMembers int64 } type GroupMember struct { groupEmail string memberEmail string } var googleWorkspaceScopesForGroupAndMember = []string { admin.AdminDirectoryGroupMemberReadonlyScope, admin.AdminDirectoryGroupReadonlyScope, } var groups = []Group{ { groupEmail: "hoge-developer@test.com" , groupName: "hoge developer team" , numberOfMembers: 5 , }, { groupEmail: "fuga-owner@test.com" , groupName: "fuga owner team" , numberOfMembers: 10 , }, } func GetGroupMember () ([]GroupMember, error ) { var groupMemberList []GroupMember srv, err := getService() if err != nil { return nil , fmt.Errorf("get admin service: %w" , err) } for _, g := range groups { if g.numberOfMembers != 0 { members, err := createGroupMemberList(srv, g.groupEmail) if err != nil { return nil , fmt.Errorf("create group member list: %w" , err) } groupMemberList = append (groupMemberList, members...) } } return groupMemberList, nil } func createGroupMemberList (srv *admin.Service, email string ) ([]GroupMember, error ) { rm, err := srv.Members.List(email).Do() if err != nil { return nil , fmt.Errorf("get member list: %w" , err) } var memberList []GroupMember for _, m := range rm.Members { memberList = append (memberList, GroupMember{groupEmail: email, memberEmail: m.Email}) } return memberList, nil } func getService () (*admin.Service, error ) { serviceAccountJSON, err := ioutil.ReadFile("key/service-account-key.json" ) if err != nil { return nil , fmt.Errorf("read service account key: %w" , err) } config, err := google.JWTConfigFromJSON(serviceAccountJSON, googleWorkspaceScopesForGroupAndMember...) if err != nil { return nil , fmt.Errorf("authorize service account key: %w" , err) } config.Subject = "<管理者のメールアドレス>" config.Scopes = googleWorkspaceScopesForGroupAndMember ctx := context.Background() srv, err := admin.NewService(ctx, option.WithHTTPClient(config.Client(ctx))) if err != nil { return nil , fmt.Errorf("get new service: %w" , err) } return srv, nil } func main () { groupMembers, err := GetGroupMember() if err != nil { fmt.Println(err) os.Exit(1 ) } for _, member := range groupMembers { fmt.Println(member) } }
タイトルの通りなのですが、上記実装ではcreateGroupMemberList(srv, g.groupEmail)
にて、リクエスト間隔について全く考慮しておらず、間髪入れずにAPIへリクエストを送ったことによって500エラーを発生させてしまいました。
それもそのはずで、APIの仕様書 を見ると1分あたりの呼び出し制限数が記載されています。
Indicates that the user rate limit has been exceeded. The default value set in the Google Developers Console is 3,000 queries per 100 seconds per IP address.
解決策(1):リクエスト間隔に余裕を持たせる 高速でリクエストを投げつけるとDoSアタックと勘違いされてブロックされる場合もあるのでちゃんと間隔をおいてリクエストを投げましょう。
以下のように、Sleep
を入れてリクエスト間隔に余裕を持たせるのが一番簡単だと思います。
main.go (GetGroupMemberでリクエスト時間を調節) func GetGroupMember () ([]GroupMember, error ) { var groupMemberList []GroupMember srv, err := getService() if err != nil { return nil , fmt.Errorf("get admin service: %w" , err) } for _, g := range groups { if g.numberOfMembers != 0 { members, err := createGroupMemberList(srv, g.groupEmail) if err != nil { return nil , fmt.Errorf("create group member list: %w" , err) } groupMemberList = append (groupMemberList, members...) time.Sleep(time.Millisecond * 250 ) } } return groupMemberList, nil }
あれ? またリクエストがコケたぞ リクエスト間隔を調整したにも関わらず、たまーに500エラーが返ってきます。
StackOverflow にも同じ現象が起きている人が居て、リクエスト間隔に余裕を持たせていたとしてもGoogle側の何かしらのトラブルによって500エラーでコケるようです。
“You did everything right, but Google is having some trouble handling your request.” (コードは正しく書けているけど、Google側でリクエストを処理しようとした際に何かしらのエラーが起きているみたいよ)
解決策(2):指数バックオフを導入する こういったケースはどのAPIでもあり得るので、 指数バックオフ を導入しましょう。 「指数バックオフ??数学+横文字やめて!」となるかもしれませんが、簡単にまとめると「APIへリクエストしたにも関わらず失敗した際に、時間を少しおいてリクエストをもう一度送る」処理になります。
先ほどのリクエスト時間に余裕を持たせたうえで以下の変更を施します。
main.go(createGroupMemberList内のAPI利用時に指数バックオフを導入) func createGroupMemberList (srv *admin.Service, email string ) ([]GroupMember, error ) { maxRetries := 10 var memberList []GroupMember for i := 0 ; i <= maxRetries; i++ { rm, err := srv.Members.List(email).Do() if err != nil { var gerr *googleapi.Error if ok := errors.As(err, &gerr); ok { if gerr.Code >= 500 { waitTime := int (math.Pow(2 , float64 (i+1 )) * float64 (100 )) fmt.Println(waitTime) time.Sleep(time.Millisecond * time.Duration(waitTime)) } else { return nil , fmt.Errorf("get member list: %w" , err) } } } else { for _, m := range rm.Members { memberList = append (memberList, GroupMember{groupEmail: email, memberEmail: m.Email}) } return memberList, nil } } return nil , fmt.Errorf("reaching max retries in createGroupMemberList" ) }
リトライ数などはべた書きですが、関数として指数バックオフを定義して複数のAPIで共通で利用できるようにしておくと良さそうですね。
こうすることで、たまーにコケるエラーに対して頑健なリクエストをすることが可能になります。 (というか、APIの仕様書 にも指数バックオフ導入しといてねって書いてありますね…)
まとめ サードパーティのAPIを使う処理を書く場合は、
リクエスト間隔は気をつけましょう(APIの仕様書をちゃんと読みましょう)。
指数バックオフを導入しておきましょう。