G-gen の又吉です。当記事では、Cloud Functions の呼び出しを許可されたアカウントのみが実行できるよう制御する方法を紹介します。
概要
背景
Cloud Functions のトリガーには大きく 2 つのカテゴリに分かれています。
No | トリガー 名 | 説明 |
---|---|---|
1 | HTTP (S) トリガー | Cloud Functions の URL エンドポイントを使用し HTTP リクエストをトリガーに関数を実行する |
2 | イベントトリガー | Pub / Sub トピックのメッセージや Cloud Storage バケットの変更等をトリガーに関数を実行する |
さらに HTTP トリガーでは、以下の 2 つの認証方法が存在します。
No | 認証方法 | 説明 |
---|---|---|
1 | 未認証の呼び出しを許可 | 「allUsers 」という特別なプリンシパルに「Cloud Run 起動元 」ロールが付与されるため、インターネット上のすべてのユーザーがアクセスできるようになる |
2 | 認証が必要 | IAM を使用して許可するユーザーを管理できる |
Web サイト等のインターネットに公開するシステム以外は、基本的に[認証が必要]
としてトリガーを設定することが HTTP トリガーの推奨事項とされています。
構成
今回の構成はシンプルに、functions-1
関数を「認証が必要」とした HTTP トリガーでデプロイします。
functions-2
関数には functions-1 関数を実行できる権限を付与し、functions-3
関数には何も権限を付与せず、それぞれが functions-1
関数を HTTP リクエストで呼び出します。
尚、本記事では Cloud Functions の概要については触れていないため、詳しく知りたい方は以下の記事をご参考に下さい。
準備
準備と動作確認はすべて gcloud コマンドで実行します。 gcloud コマンドがローカル環境で使えない場合は、Cloud Shell からでも実行可能です。
gcloud コマンドのインストール方法はこちらの公式リファレンスを参照下さい。
必要な API の有効化
対象のプロジェクト内で以下の API を有効化します。
- artifactregistry.googleapis.com
- cloudbuild.googleapis.com
- cloudfunctions.googleapis.com
- logging.googleapis.com
- pubsub.googleapis.com
- run.googleapis.com
以下のコマンドを実行します。
gcloud services enable artifactregistry.googleapis.com \ cloudbuild.googleapis.com \ cloudfunctions.googleapis.com \ logging.googleapis.com \ pubsub.googleapis.com \ run.googleapis.com
サービスアカウントの作成
functions-2 関数と、functions-3 関数のサービスアカウントを作成する。
functions-2 関数のサービスアカウント作成
gcloud iam service-accounts create functions-2 \ --display-name="functions-2" \ --project=<project_id>
functions-3 関数のサービスアカウント作成
gcloud iam service-accounts create functions-3 \ --display-name="functions-3" \ --project=<project_id>
フォルダ構成
実行環境で、以下のフォルダを作成する。
尚、functions-2 関数と functions-3 関数は同じソースコードを利用し、相違点はサービスアカウントの権限のみ
とする。
authenticationg_for_invocation ├── functions_1 │ ├── main.py │ └── requirements.txt │ └── functions_2_3 ├── main.py └── requirements.txt
functions_1/main.py
functions-1 関数は、リクエストが来たら Hello HTTP!!
と返すだけのシンプルな関数とする。
import functions_framework @functions_framework.http def hello_http(request): return "Hello HTTP!!"
functions_1/requirements.txt
functions-framework==3.*
functions_2_3/main.py
Python クライアントライブラリのサンプル を参考に、実行関数に付与されたサービスアカウントの認証情報を使用して、functions-1 関数のトークンを取得する処理を実装している。
尚、実行関数に付与されたサービスアカウントに functions-1 関数に対してのroles/run.invoker
権限が含まれていればトークンが取得できる。
import os import functions_framework import urllib import google.auth.transport.requests import google.oauth2.id_token # functions-1 関数のエンドポイント URL ENDPOINT = os.environ["ENDPOINT"] @functions_framework.http def make_authorized_get_request(request): # Request をインスタンス化 req = urllib.request.Request(ENDPOINT) # サービスアカウントの認証情報を使用し functions-1 関数のトークンを取得 auth_req = google.auth.transport.requests.Request() id_token = google.oauth2.id_token.fetch_id_token(auth_req, ENDPOINT) print(id_token) # トークンをヘッダーに付与し functions-1 関数に HTTP リクエストを送信 req.add_header("Authorization", f"Bearer {id_token}") response = urllib.request.urlopen(req) return response.read()
functions_2_3/requirements.txt
functions-framework==3.* google-auth==2.16.0 requests==2.27.1 urllib3==1.26.14
Cloud Functions の作成
はじめに、デフォルトリージョンを設定する。
gcloud config set functions/region asia-northeast1 && \ gcloud config set run/region asia-northeast1
functions-1 関数の作成
authenticationg_for_invocation/
がカレントディレクトリになっていることを確認し、以下を実行。
gcloud functions deploy functions-1 \ --gen2 \ --trigger-http \ --runtime=python310 \ --entry-point=hello_http \ --source=functions_1/
functions-2 関数の作成
functions-2 関数のデプロイに必要な情報は以下の通り。
- functions-1 関数の
URI エンドポイント
- functions-2 関数に付与する
サービスアカウントのメールアドレス
以下のコマンドで functions-1 関数の URI エンドポイント
を確認する。
gcloud functions describe functions-1 --gen2 | grep uri
以下のように出力されたら、 https://functions-1-xxxxxxxxxx-an.a.run.app
をコピーしておく。
matayuuu@cloudshell:~ (project_id)$ gcloud functions describe functions-1 --gen2 | grep uri uri: https://functions-1-xxxxxxxxxx-an.a.run.app
次に、以下のコマンドで functions-2 関数のサービスアカウントのメールアドレス
を取得する
gcloud iam service-accounts list | grep EMAIL: | grep functions-2
以下のように出力された、functions-2@project_id.iam.gserviceaccount.com
をコピーしておく。
matayuuu@cloudshell:~ (project_id)$ gcloud iam service-accounts list | grep EMAIL: | grep functions-2 EMAIL: functions-2@project_id.iam.gserviceaccount.com
以下コマンドの、--set-env-vars
と -service-account
を書き換えて functions-2 関数デプロイを実行。
gcloud functions deploy functions-2 \ --gen2 \ --trigger-http \ --runtime=python310 \ --entry-point=make_authorized_get_request \ --source=functions_2_3/ \ --set-env-vars=ENDPOINT=<先程出力した functions-1 関数のURI> \ --service-account=<先程出力した functions-2 関数のサービスアカウント>
functions-3 関数の作成
先程と同様の手順で functions-3 関数をデプロイする情報を取得し、以下のデプロイコマンドを実行する。
gcloud functions deploy functions-3 \ --gen2 \ --trigger-http \ --runtime=python310 \ --entry-point=make_authorized_get_request \ --source=functions_2_3/ \ --set-env-vars=ENDPOINT=<先程出力した functions-1 関数のURI> \ --service-account=<先程作成した functions-3 関数のサービスアカウント>
権限の付与
functions-1 関数の呼び出し元権限 (roles/run.invoker
) を、functions-2 関数のサービスアカウントのみに付与する。
gcloud run services add-iam-policy-binding functions-1 \ --member='serviceAccount:functions-2@project_id.iam.gserviceaccount.com' \ --role='roles/run.invoker'
動作確認
実行 1
以下のコマンドから、functions-2 関数を実行する。
gcloud functions call functions-2 --gen2
以下のように、Hello HTTP!!
とレスポンスがあれば成功です。
matayuuu@cloudshell:~/authenticationg_for_invocation (project_id)$ gcloud functions call functions-2 --gen2 Hello HTTP!!
実行 2
以下のコマンドから、functions-3 関数を実行する。
gcloud functions call functions-3 --gen2
以下のように、エラーとなれば予測していた挙動となります。
matayuuu@cloudshell:~/authenticationg_for_invocation (project_id)$ gcloud functions call functions-3 --gen2 ERROR: gcloud crashed (HTTPError): 500 Server Error: Internal Server Error for url: https://functions-3-xxxxxxxxxx-an.a.run.app/ If you would like to report this issue, please run the following command: gcloud feedback To check gcloud for common problems, please run the following command: gcloud info --run-diagnostics
Cloud Logging にて functions-3 のログを確認すると、以下のエラーが出力されてました。
Traceback (most recent call last): 〜(省略)〜 File "/layers/google.python.runtime/python/lib/python3.10/urllib/request.py", line 643, in http_error_default raise HTTPError(req.full_url, code, msg, hdrs, fp) urllib.error.HTTPError: HTTP Error 403: Forbidden
HTTP リクエスト時に 403 エラー
が返ってきてることがわかります。
functions-3 関数のサービスアカウントには functions-1 関数の呼び出し元権限がないため、HTTP リクエストに失敗していることが想定されます。
また、Cloud Logging にて functions-1 のログも確認すると、以下のエラーが出力されてました。
The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header. Read more at https://cloud.google.com/run/docs/securing/authenticating Additional troubleshooting documentation can be found at: https://cloud.google.com/run/docs/troubleshooting#unauthorized-client
上記より、functions-3 関数からの呼び出しは認証されていない呼び出し
だったことがわかりました。
もう少し深堀ってみる
curl コマンドで実行
functions-1 関数の呼び出しを、Cloud Shell から curl
コマンドで呼び出すこともできます。
Cloud Functions のコンソール画面
> functions-1
関数を選択 > テスト中
タブを選択 > テストコマンド
を確認すると以下のコマンドが出力されてました。
curl -m 70 -X POST https://functions-1-xxxxxxxxxx-an.a.run.app \ -H "Authorization: bearer $(gcloud auth print-identity-token)" \ -H "Content-Type: application/json" \ -d '{ "name": "Hello World" }'
このコマンドを Cloud Shell から実行すると functions-1 関数からHello HTTP!!
が返ってきました。
matayuuu@cloudshell:~ (project_id)$ curl -m 70 -X POST https://functions-1-xxxxxxxxxx-an.a.run.app \ > -H "Authorization: bearer $(gcloud auth print-identity-token)" \ > -H "Content-Type: application/json" \ > -d '{ "name": "Hello World" }' Hello HTTP!!
functions-1 関数が呼び出せたことがわかります。
トークンの確認
先程のテストコマンドの 2 行目にあるリクエストヘッダーに注目いただくと、 Authorization: bearer $(gcloud auth print-identity-token)
と記載があります。
確認のため、Cloud Shell にて gcloud auth print-identity-token
以下を実行すると以下の文字列が返ってきました。
matayuuu@cloudshell:~ (project_id)$ gcloud auth print-identity-token eyJhbGciOiJSUzI1NiIsImtpZCI6ImFmYzRmYmE2NTk5ZmY1ZjYzYjcyZGM1MjI0MjgyNzg2ODJmM2E3ZjEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiNjE4MTA0NzA4MDU0LTlyOXMxYzRhbGczNmVybGl1Y2hvOXQ1Mm4zMm42ZGdxLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNjE4MTA0NzA4MDU0LTlyOXMxYzRhbGczNmVybGl1Y2hvOXQ1Mm4zMm42ZGdxLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTA0ODc3Mjg2MDgxMjE4NDY0NzA4IiwiaGQiOiJnLWdlbi5jby5qcCIsImVtYWlsIjoibWF0YXl1dXVAZy1nZW4uY28uanAiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlVGLWJuSHJSTzZBLW5kOE1zTVpBX0EiLCJpYXQiOjE2NzQzOTA0OTAsImV4cCI6MTY3NDM5NDA5MCwianRpIjoiMjlmY2VhOTQzMzFlNzMxOGIwNTFjNDQ4ZDUzOTBjMDBlNTQ5NDU4MSJ9.l1BfkuBlmYThwRkCpWFd7V3ATWSn-zwUfuLdChw6EKg3N0qyi2NYZioiDVEMFKy-OTbap4IpGzNYi3scpux9vMnGda-UDrDwWkN8iMhBpdD1ZVjiGew8kUYwOytr-2YRbk2dM_AnOTSIfB9dgj9drWsNZHrUuG6mnYK4l4zvwIRzzh4kwdtZpd0jLkmHHIaAORMHVkIdfx88peEK3BYKzdkNrwoveuG9VjZwtef0DHMtT0-Obmd0d7Yaa8v7Onzk0s0SrAwfvFyXrsi_wY8nRJ8C5f6ljNklLlyqxcZr8UFE-3Z64hYixWMjbTqgqpjFKq4v1Pxldm_MQauJ0RAg4A
また、functions_2_3 のコード内でトークンを取得する処理において、取得したトークンを確認するため print()
するようにしています 。
# 〜(抜粋)〜 # サービスアカウントの認証情報を使用し functions-1 関数のトークンを取得 auth_req = google.auth.transport.requests.Request() id_token = google.oauth2.id_token.fetch_id_token(auth_req, ENDPOINT) print(id_token)
再度、functions-2 関数を実行し Cloud Logging からログを確認すると、以下のトークンが print()
されてました。
eyJhbGciOiJSUzI1NiIsImtpZCI6ImFmYzRmYmE2NTk5ZmY1ZjYzYjcyZGM1MjI0MjgyNzg2ODJmM2E3ZjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Z1bmN0aW9ucy0xLXZlNXBzaHhjZHEtYW4uYS5ydW4uYXBwIiwiYXpwIjoiMTAxNzgwOTIxMzYzNDgwNTMyNTQ4IiwiZW1haWwiOiJmdW5jdGlvbnMtMkBwb2MtaW52b2tlLWdjZi1odHRwLTIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjc0Mzc1Mjc2LCJpYXQiOjE2NzQzNzE2NzYsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjEwMTc4MDkyMTM2MzQ4MDUzMjU0OCJ9.l7IvSXFWQu2NfvmCQEzdZbWQ3mPtzO3myqW45Grw_bEedLLAe7DYO0x3iPMBtOrlVXdl4REn8wxGNaDqsL_UZBAePAI6PQJdVmNn-QA46JGPC6K74ZohR3KzWFI7_NEudnL_l9ZBVazyQUc34XfG9T9OXJgtCmZefwWYTMvJC8EYkqg4FnvEkw1xAeuwCr9pE1aqFSsCQTFU-VMwzxPpQ33AF49UMtMfU5qhmFoeBMFnwfpMt9tFwR-K9HE2fAqtysUUXfk_FJDAkxw25rN2uA0EKHIS-FQ738N-ZKKD7wjlhrDcoAalerSjBpU0UgUpArK9o7gFAu5Ztyd4qv0tvQ
gcloud auth print-identity-token
で取得したトークンと比べてみると異なる文字列が返ってきました。
このトークンの正体は、 OpenID Connect(OIDC)トークンまたは ID トークンといい、Google によって署名された ID トークンであります。尚、存続期間は最大 1 時間 (デフォルト) となります。
Cloud Functions や Cloud Run の呼び出しにおいて、この ID トークンが必要になります。
トークンを取得する仕組み
トークンを取得するときの認証情報の仕組みについては、アプリケーションのデフォルト認証情報 (ADC)が使われています。
今回 ADC について詳しい説明は省略しますが、functions-2 関数では 接続されたサービスアカウント
認証情報が ADC に提供されています。
また、Cloud Shell のような Google Cloud 上のクラウドベース開発環境を使用する場合は、ログイン時に指定した認証情報が ADC に提供されるため、プロジェクトレベルのオーナー権限を持った筆者のユーザーアカウント
認証情報が提供されていたことがわかります。
トークンのデコード
No | 実行環境 | 誰のトークン |
---|---|---|
1 | Cloud Shell | 筆者のユーザーアカウント 認証情報で取得したトークン |
2 | functions-2 関数 | functions-2 関数のサービスアカウント 認証情報で取得したトークン |
このトークンはエンコードされているため、jwt.io Debugger を使えばデコードができます。
又吉 佑樹(記事一覧)
クラウドソリューション部
はいさい、沖縄出身のクラウドエンジニア!
セールスからエンジニアへ転身。Google Cloud 全 11 資格保有。Google Cloud Champion Innovator (AI/ML)。Google Cloud Partner Top Engineer 2024。Google Cloud 公式ユーザー会 Jagu'e'r でエバンジェリスト。好きな分野は生成 AI。
Follow @matayuuuu