こんにちは、DMP(Data Management Platform)グループの平井です。毎日デコポンを食べています。美味しい。
タイトルの通り、BigQuery Remote FunctionsからCloud Functions 2nd genを呼び出す際にハマったポイントがあったので共有します。
今回のケース
まずどのような場面でRemote Functionsを使用したのか説明します。
レアジョブグループには新旧2つのデータ基盤があります。
順次移行作業を進めており、その中でRを利用した集計処理を新しいデータ基盤に移行することになりました。
新しいデータ基盤ではBigQueryを利用しているため、RのスクリプトをSQLで書き直したのですが、一部実装しきれない処理がありました。
そこで該当の処理をPythonで実装し、Remote Functionsで呼び出すことにしました。
Remote Functionsとは
公式ドキュメントでは以下のように説明されています。
BigQuery リモート関数を使用すると、SQL と JavaScript 以外の言語や、BigQuery ユーザー定義関数で許可されていないライブラリやサービスを使用して関数を実装できます。
実際に使ってみた感想としてはSQLで実装するのが難しい処理を、普段使っている言語で実装することができとても便利でした。
反面Remote Functionsから呼び出す処理のパフォーマンスはCloud Functions・Cloud Runに依存するので、大量のデータを処理させられるかは考慮した方が良さそうです。
ハマりポイント再現
実際に必要なリソースをデプロイし、Remote Functionsを動かしてハマったポイントを見ていきます。
Cloud Functions 2nd genとは
まずCloud Functions 2nd genにはどのような特徴があるか見てみましょう。
Cloud Functions(第 2 世代)は、Google Cloud の次世代の Functions as a Service サービスです。Cloud Functions(第 2 世代)は Cloud Run と Eventarc 上に構築されており、以下のようにインフラストラクチャの強化と幅広いイベント カバレッジを Cloud Functions に提供しています。
ドキュメントの説明にあるようにCloud Functionsとしてデプロイされるのですが、裏側がCloud Runになっているのが特徴です。
では実際にデプロイしていきます。
Cloud Functions 2nd genをデプロイ
Remote Functionsから呼び出されるCloud Functions 2nd genをデプロイしてみます。
今回は以下のような構成でファイルを作成します。
techblog ├── main.py └── requirements.txt
Cloud FunctionsのRuntimeはPython3.10を選択し、デプロイするコードはドキュメントのサンプルコードをもとに作成します。
main.py
import json import functions_framework _MAX_LOSSLESS=9007199254740992 @functions_framework.http def batch_add(request): try: return_value = [] request_json = request.get_json() calls = request_json['calls'] for call in calls: return_value.append(sum([int(x) if isinstance(x, str) else x for x in call if x is not None])) replies = [str(x) if x > _MAX_LOSSLESS or x < -_MAX_LOSSLESS else x for x in return_value] return_json = json.dumps( { "replies" : replies} ) return return_json except Exception as inst: return json.dumps( { "errorMessage": 'something unexpected in input' } ), 400
requirements.txt
functions-framework==3.*
以下コマンドから実際にデプロイしてみます。これ以降のコマンドは全てCloud Shellから実行しています。
gcloud functions deploy sample-tech-blog --gen2 --region=us-central1 --runtime=python310 --source=/home/takumi_hirai/techblog --entry-point=batch_add --trigger-http
リソースが作成されたか確認します。
takumi_hirai@cloudshell:~ $ gcloud functions describe sample-tech-blog buildConfig: ~省略~ environment: GEN_2 labels: deployment-tool: cli-gcloud name: projects/PROJECT_ID/locations/us-central1/functions/sample-tech-blog serviceConfig: ~省略~ uri: https://sample-tech-blog-aaaaa-uc.a.run.app state: ACTIVE updateTime: '2023-02-08T12:05:32.971991805Z'
ここでCloud Runのリソースも確認してみます。
takumi_hirai@cloudshell:~ $ gcloud run services list --filter="sample-tech-blog" ✔ SERVICE: sample-tech-blog REGION: us-central1 URL: https://sample-tech-blog-aaaaa-uc.a.run.app LAST DEPLOYED BY: service-aaaaa@gcf-admin-robot.iam.gserviceaccount.com LAST DEPLOYED AT: 2023-02-03T07:50:23.610982Z
Cloud Functions 2nd genの説明にあったようにCloud Runのリソースも作成されていることが確認できました。
Cloud Functionsのuri = Cloud RunのURL
となっていることもわかるかと思います。
BigQuery Connections作成
BigQuery Connectionを作成します。BigQuery Connection APIを有効化していないとエラーとなるので気をつけてください。
bq mk --connection --location=us --connection_type=CLOUD_RESOURCE sample-connection-tech-blog
Remote Funcitonを作成
BigQuery ConsoleからRemote Funcitonを作成するSQLを実行します。endpointには先ほど作成したCloud Functionsのendpointを指定します。
CREATE FUNCTION `PROJECT_ID.DATASET_ID`.remote_add(x INT64, y INT64) RETURNS INT64 REMOTE WITH CONNECTION `PROJECT_ID.US.sample-connection-tech-blog` OPTIONS ( endpoint = 'https://sample-tech-blog-aaaaa.a.run.app' )
権限付与
最後にSQLからRemote Functionsを実行するための権限付与を行い、次のSQLを実行します。
SELECT val, `PROJECT_ID.DATASET_ID`.remote_add(val, 2) FROM UNNEST([NULL,2,3,5,8]) AS val;
表題にあるハマったポイントはここの権限設定になります!
当時私は次のように権限設定をしました。
error message
Make sure the service account associated with the connection PROJECT_ID.us.sample-connection-tech-blog is granted the cloudfunctions.functions.invoke permission (e.g., via the Cloud Functions Invoker role) on your Cloud Function endpoint, or the run.routes.invoke permission (e.g., via the Cloud Run Invoker role) on your Cloud Run endpoint. Please allow 60 seconds for the permission change to propagate before retrying.
- エラーメッセージに従い、BigQuery Connectionに設定されているサービスアカウントに
Cloud Function Invoker role
を付与 → エラー
error message
Make sure the service account associated with the connection PROJECT_ID.us.sample-connection-tech-blog is granted the cloudfunctions.functions.invoke permission (e.g., via the Cloud Functions Invoker role) on your Cloud Function endpoint, or the run.routes.invoke permission (e.g., via the Cloud Run Invoker role) on your Cloud Run endpoint. Please allow 60 seconds for the permission change to propagate before retrying.
先ほどと同じエラーメッセージが表示されました。
???
ここで先ほどCloud Functions 2nd genを説明したときの一文に戻ります。
Cloud Functionsとしてデプロイされるのですが、裏側がCloud Runになっているのが特徴
もしやと思い、Cloud Function Invoker role
の代わりにCloud Run Invoker role
を付与しました。
- 成功!
+------+-----+ | val | f0_ | +------+-----+ | NULL | 2 | | 2 | 4 | | 3 | 5 | | 5 | 7 | | 8 | 10 | +------+-----+
当時は試行錯誤の末に解決しました(※)が、本記事を書くために公式ドキュメントを見たところ全て丁寧に説明されていました...
※ 2022年11月ごろにドキュメントを確認した際は権限の丁寧な説明はなかったように記憶しているのですが、ただの見落としかもしれません。
まとめ
権限設定周りでハマってしまいましたが、最終的には移行も完了し、本番環境で今日も元気に動いております。
公式ドキュメントも充実していたので使う機会も増えるかもしれません。
We're hiring!
データエンジニアとして一緒に働きたい!という方も探していますので、ご興味ある方は@ththicnまでお声がけください!