RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

BigQuery Remote FunctionsからCloud Functions 2nd genを呼び出す際にハマったポイントを振り返る

こんにちは、DMP(Data Management Platform)グループの平井です。毎日デコポンを食べています。美味しい。

タイトルの通り、BigQuery Remote FunctionsからCloud Functions 2nd genを呼び出す際にハマったポイントがあったので共有します。

今回のケース

まずどのような場面でRemote Functionsを使用したのか説明します。

レアジョブグループには新旧2つのデータ基盤があります。

順次移行作業を進めており、その中でRを利用した集計処理を新しいデータ基盤に移行することになりました。

新しいデータ基盤ではBigQueryを利用しているため、RのスクリプトSQLで書き直したのですが、一部実装しきれない処理がありました。

そこで該当の処理をPythonで実装し、Remote Functionsで呼び出すことにしました。

Remote Functionsとは

公式ドキュメントでは以下のように説明されています。

BigQuery リモート関数を使用すると、SQLJavaScript 以外の言語や、BigQuery ユーザー定義関数で許可されていないライブラリやサービスを使用して関数を実装できます。

cloud.google.com

実際に使ってみた感想としては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.google.com

ドキュメントの説明にあるように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;

表題にあるハマったポイントはここの権限設定になります!

当時私は次のように権限設定をしました。

  • SQLを実行するサービスアカウントにCloud Function Invoker roleを設定しSQLを実行 → エラー

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 |
+------+-----+

当時は試行錯誤の末に解決しました(※)が、本記事を書くために公式ドキュメントを見たところ全て丁寧に説明されていました...

cloud.google.com

※ 2022年11月ごろにドキュメントを確認した際は権限の丁寧な説明はなかったように記憶しているのですが、ただの見落としかもしれません。

まとめ

権限設定周りでハマってしまいましたが、最終的には移行も完了し、本番環境で今日も元気に動いております。

公式ドキュメントも充実していたので使う機会も増えるかもしれません。

We're hiring!

データエンジニアとして一緒に働きたい!という方も探していますので、ご興味ある方は@ththicnまでお声がけください!

rarejob-tech.co.jp