👨‍🦯

Google Cloud Task あれこれ(その2)

2023/12/23に公開

前回の記事で漏れた内容をフォローアップします。

最大試行時間とは何なのか

前回のリトライの説明で、最大試行時間(MAX_RETRY_DURATION)だけ説明できていませんでした。公式ドキュメントには、失敗したタスクを再試行する最大時間で、タスクが最初に試行された時点から測定されますと書いてあります。
つまり、1回目のタスク実行から最大試行時間経過後に、タスク試行が終わるように、試行時間を制限するパラメータのようです。
一方で、試行時間の制御には、最大試行回数(MAX_ATTEMPTS)や、最大バックオフ(MAX_INTERVAL)、最大倍増回数(MAX_DOUBLINGS)も関係するはずです。
これらの関係はどうなっているのでしょうか。
確認してみたところ、最大試行回数が十分小さいと、最大試行回数を無視して、最大試行時間が終わるまでタスク試行が行われるようです。

例えば、前回の記事で示したパラメータの設定(最大試行回数 = 20, 最小バックオフ = 10, 最大バックオフ = 600, 最大倍増回数 = 4)で、最大試行時間を300秒にしたとします。
この場合、最大試行時間以外のパラメータで算出される試行時間は、300秒をはるかに超えるので、最大試行時間は無視されます。

では、最大試行回数を3にしてみるとどうなるか。前回の記事の必ずエラーになるタスクをキューイングするGolangのプログラムで試してみた結果、以下の通りになりました。

  • 最初の試行と次のリトライ試行の差は約10秒です。
    • 最小バックオフの10秒が最初のリトライ試行までの間隔になる。
  • 次の試行と3番目の試行の差は、約20秒。
    • 10秒 * 2で20秒間隔。
    • 最大試行回数 3なので、ここで終わると思いきや、終わらない
  • 次の試行と4番目の試行の差は、約40秒。
    • 20秒 * 2で40秒間隔。
  • 次の試行と5番目の試行の差は、約80秒。
    • 40秒 * 2で80秒間隔。
  • 次の試行と6番目の試行の差は、約160秒。
    • 80秒 * 2で160秒間隔。ここで、最大倍増回数の4回に到達。
  • 最初の試行と6回目の試行の差が、約311秒で最大試行時間の300秒を上回っている。したがって、ここで試行終了

つまり、 最大試行回数が有効になるかどうかは、そのほかのパラメータに依存するということのようです。↑の例では、最大試行回数が終わった段階でも、まだ最大試行時間に到達していなかったので、最大試行時間までリトライするという挙動になった、と見ることができます。

まとめ

  • 最大試行時間は、効になる場合と効かない場合がある。
  • 同様に、最大試行時間の設定によっては、最大試行回数が効かない場合もあるので、意図しないリトライ設定にならないように各パラメータを設定する必要がある。

認証

Cloud TasksのHttpターゲットタスクは、公開されたhttpのエンドポイントへタスクを投げつけます。公開のエンドポイントなので、セキュリティを考慮したくなります。
IPで制限できるとよいのですが、GCPのすべてのIPからリクエストが飛んでくる可能性があるようなのです。よってIP制限は現実的な方法ではなさそうです。

そこで、公式ドキュメント書かれている、サービスアカウントを使ったIDトークンによる認証をやってみます。

実装方法は簡単で以下のようにタスクの作成時にサービスアカウントを設定します。


...()...
import (
	"cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb"
)
...()...
        queuePath := "projects/hoge/locations/asia-northeast1/queues/test-queue"
	req := &taskspb.CreateTaskRequest{
		Parent: queuePath,
		Task: &taskspb.Task{
			MessageType: &taskspb.Task_HttpRequest{
				HttpRequest: &taskspb.HttpRequest{
					HttpMethod: taskspb.HttpMethod_POST,
					Url:        "https://abcdefghijk.a.run.app/200",
					AuthorizationHeader: &cloudtaskspb.HttpRequest_OidcToken{
						OidcToken: &cloudtaskspb.OidcToken{ // ここでサービスアカウントを設定
							ServiceAccountEmail: "cloud-tasktest@hoge.iam.gserviceaccount.com",
						},
					},
				},
			},
		},
	}

HttpRequestAuthorizationHeaderに、&cloudtaskspb.OidcTokenで、サービスアカウント(メールアドレス)を設定します。
前提として、このサービスアカウントには、roles/cloudtasks.enqueuer が付与されている必要があります。

では、このトークンでどのように認証を実現するのでしょうか?

Google Cloud側で認証する

たとえば、タスクを処理するハンドラをCloud Runで動かしている場合、Cloud RunをIAMユーザー認証必須にします。

これで、このCloud RunのエンドポイントにIDトークンなしでタスクを処理させようとすると以下の通り、status: "PERMISSION_DENIED"で、エラーになります。

では、この状態で、↑に示した通りのコードにサービスアカウントを指定して、タスクをエンキューしてみます。
すると、きちんとCloud Run側まで、タスクが到達したのが確認できました。
この方式であれば、アプリ側で認証(トークンの検証処理)する必要もなく、Google Cloud側に認証を移譲できます。
おそらく、Cloud Runだけでなく、Cloud Function等のほかのサービスでも同様のことが可能だと思われます(筆者未検証)。

注意点として、この構成にするなら、サービスアカウントには、roles/cloudtasks.enqueuerだけでなく、 Cloud Run 起動元(roles/run.invoker) が必要になります。

自前でIDトークンを検証する

タスクを処理するエンドポイントを、Google Cloudで動かせない場合もあります。この場合は、IDトークンを自前で、検証する必要があります。

とはいえ、これも、少なくとも、Golangでは、Google Cloudが用意しているライブラリ("google.golang.org/api/idtoken")で簡単に実現できます。

package middleware

import (
...()...
	"google.golang.org/api/idtoken"
...()...
)

func Verify(r *http.Request) bool {
	auth := strings.Split(r.Header.Get("Authorization"), " ")
	if len(v) < 2 {
		return false
	}
	_, err := idtoken.Validate(r.Context(), auth[1], "")
	return err == nil
}

IDトークンは、AuthorizationヘッダにBearer ID_TOKENという形式で入ってくるので、ID_TOKENの部分を取り出し、idtoken.Validateで、検証を行っています。これを例えば、middlewareで実装すれば、無駄なく認証処理が実現できそうです。

公式ドキュメントには、pythonでの実現方法の記載があります。おそらく、ほかの言語でも、同様に簡単に実現できるはずです。

まとめ

  • Cloud Tasksのタスクを処理するパブリックなエンドポイントにIPでアクセス制限するのは無理。
  • IDトークンを使った認証は、比較的簡単に実現できる。
    • タスク処理するハンドラがGoogle Cloud上で動いているなら、トークンの検証はGoogle Cloud側に任せられる。
    • そうでないなら、自前でIDトークンの検証をやる必要があるが、そんなに難しくない。

SDKを使わずにエンキューする

場合によっては、SDKが使えないけれど、Cloud Tasksにエンキューしたい、というケースがあります。
例えば、さまざまな運命のめぐりあわせで、VB scriptから、エンキューしたいと思ったとします。
このような場合は、REST API形式で、Cloud TasksのAPIをコールすることでエンキューが可能です。

API仕様に沿って、VB scriptで以下の通り実装してみましたChatGPTにvbsを生成してもらいました。

Dim httpRequest, url, postData, responseText, accessToken, project, location, queue
Dim payload, httpTargetUrl

' 事前に gcloud auth print-access-tokenで取得したアクセストークン
accessToken = "dummytoken"

' Cloud Tasks の設定
project = "hoge"
location = "asia-northeast1"
queue = "test-queue"

' タスクのペイロード(ここでは単純なテキストメッセージを使用)
payload = "Hello, World!" ' 送信するペイロード

' HTTP ターゲットタスクの URL
httpTargetUrl = "https://abcdefghijk.a.run.app/200"

' Cloud Tasks API エンドポイントの設定
url = "https://cloudtasks.googleapis.com/v2beta3/projects/" & project & "/locations/" & location & "/queues/" & queue & "/tasks"

' JSON ペイロードの準備
postData = "{""task"": {""httpRequest"": {""httpMethod"": ""POST"", ""url"": """ & httpTargetUrl & _
           """, ""headers"": {""Content-Type"": ""application/json""}, ""body"": """ & _
           EncodeBase64(payload) & """}}}"

' HTTP リクエストオブジェクトの作成
Set httpRequest = CreateObject("MSXML2.XMLHTTP")
httpRequest.Open "POST", url, False
httpRequest.setRequestHeader "Authorization", "Bearer " & accessToken
httpRequest.setRequestHeader "Content-Type", "application/json"
httpRequest.Send postData

' レスポンスの取得
responseText = httpRequest.responseText
WScript.Echo responseText

Set httpRequest = Nothing

Function EncodeBase64(text)
    Dim xml, node
    Set xml = CreateObject("MSXML2.DOMDocument")
    Set node = xml.createElement("b64")
    node.dataType = "bin.base64"
    node.nodeTypedValue = Stream_StringToBinary(text)
    EncodeBase64 = node.text
    Set node = Nothing
    Set xml = Nothing
End Function

Function Stream_StringToBinary(text)
    Const adTypeText = 2
    Const adTypeBinary = 1

    Dim binaryStream
    Set binaryStream = CreateObject("ADODB.Stream")
    binaryStream.Type = adTypeText
    binaryStream.Charset = "us-ascii"
    binaryStream.Open
    binaryStream.WriteText text
    binaryStream.Position = 0
    binaryStream.Type = adTypeBinary
    binaryStream.Position = 0
    Stream_StringToBinary = binaryStream.Read
    binaryStream.Close
    Set binaryStream = Nothing
End Function

さて、APIの認証をどのように通すか、という問題に突き当たります。SDKなら、仕様に沿ってクレデンシャルをクライアントに読み込めばいいのですが、そうはいかない。
この場合は、アクセストークンをAuthorizationヘッダに付与する必要があります。そのためには、アクセストークンの取得には、いくつかやり方がありますが、今回は手っ取り早く、サービスアカウントで認証済みの環境(gcloud auth login --cred-file=<サービスアカウントのkeyのパス>でログイン済みの環境)で以下のコマンドを実行し、トークンを取得します。

gcloud auth print-access-token

これで、準備ができたので、↑のvbsを実行してみましょう。
実行すると、ダイアログが表示され、エンキューの結果が、表示されます。無事、キューイングできました。

まとめ

  • Cloud TasksはREST APIでエンキューができるので、SDKが使えない環境でも、問題なく利用できる。
  • ただし、APIコールに必要なアクセストークンが必要。

Discussion