Google Cloudのサーバレス主要サービスであるCloud BuildとCloud Functionsを使っていて、Cloud BuildからCloud Functionsをなるべく安全っぽく呼びたかったのだが、適当な手順がなかなか見つからなかったのでここに残しておき、将来自分が忘れた時に検索可能にしたい。
Prelude: Compute EngineのVMからCloud Functionsを呼ぶ
Cloud FunctionsはCompute EngineのVMから呼ぶのは比較的簡単である。以下は、認証が必要なHTTPS呼び出し設定をしたFunctionsを前提とする。
最初に、Cloud Functions 起動元(roles/cloudfunctions.invoker)を付与したサービスアカウントを作り、それをVMに紐づけて起動する。
そのVMの中から以下のようにJWTを発行すれば関数を呼び出せる。
FUNCTION_NAME="https://${REGION}-${PROJECT_ID}.cloudfunctions.net/hogehoge-func" # gcloud コマンドがある時 IDENTITY_TOKEN=$(gcloud auth print-identity-token --audiences=$FUNCTION_NAME) # metadata から取る時 IDENTITY_TOKEN=$(curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=$FUNCTION_NAME") # あとはAuthorizationヘッダにセットする curl -H "Authorization: bearer $IDENTITY_TOKEN" -X POST $FUNCTION_NAME
Cloud Buildのタスクの中から呼ぶには
ところが、Cloud Buildのタスクの実行環境の中では、上記の方法が使えなくなっている。 gcloud auth print-identity-token
を実行しようとすると怒られるし、metadataのidentityエントリポイントは404になっている。
一応、JSON credentialをSecretか何かで渡して、 gcloud auth login
などでログインし直してあげれば gcloud auth print-identity-token
は発行できる。のだが、当該サービスアカウントでタスクを実行しているはずなのになぜ再ログインが必要なのか... というそもそもの気持ちと、あとSecretが別に一つ増えてしまうのは管理とか場合によって入れ替えとか運用的な手間になりそうで気になる。
ということでもう少し安全っぽくCloud Functionsを呼ぶ。
projects.serviceAccounts.generateIdToken
を個別に呼び出す
実は、Googleのcredentials APIにはこういうエントリポイントがあり、 print-identity-token
コマンドで発行されるようなJWTを個別で発行可能になっている。
このエントリポイントを叩くにはサービスアカウントトークン作成者(roles/iam.serviceAccountTokenCreator)のロールが必要なので、
以下のように自分のトークンだけを発行できるようにロールバインディングを作る*1。
gcloud iam service-accounts add-iam-policy-binding mybuilder@${PROJECT_ID}.iam.gserviceaccount.com \ --member='serviceAccount:mybuilder@${PROJECT_ID}.iam.gserviceaccount.com' \ --role='roles/iam.serviceAccountTokenCreator'
その上で以下のようにAPIをcurlで直接叩けば無事トークンが発行されるはず。もちろん実行するSAには roles/cloudfunctions.invoker を付与しておくこと。
resp=$(curl -X POST -H "content-type: application/json" \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -d '{"audience": "'$FUNCTION_NAME'"}' \ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT}:generateIdToken") IDENTITY_TOKEN=$(echo $resp | jq -r .token)
stepの例
steps: - id: 'get-token' name: 'gcr.io/cloud-builders/gcloud:latest' entrypoint: 'bash' args: - -c - | set -e FUNCTION_URL="https://${_REGION_NAME}-${PROJECT_ID}.cloudfunctions.net/${_FUNCTION_NAME}" SERVICE_ACCOUNT="$(gcloud config list account --format 'value(core.account)')" resp=$(curl -s -f -X POST -H "content-type: application/json" \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -d '{"audience": "'$$FUNCTION_URL'"}' \ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$${SERVICE_ACCOUNT}:generateIdToken") # echo $$resp # IDENTITY_TOKEN="$(echo $$resp | jq -r .token)" # gcr.io/cloud-builders/gcloud に元々入ってるものだけでなんとかする場合 IDENTITY_TOKEN=$(echo $$resp | python3 -m json.tool | grep token | awk -F'"' '{print $4}') curl -H "Content-Type: application/json" -H "Authorization: bearer $$IDENTITY_TOKEN" $$FUNCTION_URL echo "OK"
デバッグ
どうしても認証が通らない... みたいな場合、SAの権限はもちろんだがJWTが正しい権限で発行されているか確認すると良さそう。
echo $$IDENTITY_TOKEN | awk -F. '{print $2}' | base64 -d | python3 -m json.tool
{ "aud": "https://***.cloudfunctions.net/...", "azp": "123456789...", "exp": 1673412169, "iat": 1673408569, "iss": "https://accounts.google.com", "sub": "123456789..." }
aud
がエスケープ間違い(Cloud Buildでよくあること)で想定していない文字列になっていないか、というのと、 sub
はidentityを発行したサービスアカウントの内部ID(Webコンソールなどで確認できる)なのでそれが一致しているかも見る感じ。
参考
多分、今回の手順はIAPを通したい時や、Cloud Run呼びたい時などにも使える(という認識)。