Cloud Functionsから別のCloud Functionsを呼び出せるようにDeployment Managerで権限設定してみた

Deployment Managerを使ってCloud Functionsの関数にロールをバインディングする際のハマリどころと対応方法をご紹介します
2021.08.30

CX事業本部@大阪の岩田です。Cloud Functionsはデフォルトで「App Engineデフォルトサービスアカウント」を利用しますが、明示的に自作したサービスアカウントを利用したいケースもあると思います。今回サービスアカウントとCloud Functionsの作成に加えて、作成したサービスアカウントから別のCloud Functionsを呼び指す権限を設定するまでをDeployment Managerで自動化してみたので、ハマりどころを含めて紹介させて頂きます。

1.Cloud Functionsの呼び出し権限を設定せずにデプロイしてみる

まずは呼び出し元の関数と呼び出される側の関数をそれぞれ準備します。

まず呼び出される側です。

def handler(event, conttext):

    print('receiver')

ログを出力するだけのシンプルな内容です。こちらは後ほどreceiverという名前でデプロイします。

続いて呼び出し側です

from google.cloud.functions_v1 import CloudFunctionsServiceClient

PROJECT_ID = '<対象プロジェクトID>'

def handler(event, conttext):


    client = CloudFunctionsServiceClient()
    func_name = client.cloud_function_path(PROJECT_ID, 'asia-northeast1', 'receiver')
    res = client.call_function(name=func_name, data='{}')
    print(res)

receiverを起動するだけのシンプルな内容です。ではこれら2つの関数と関連するリソースをDeployment Managerでデプロイしましょう。事前にそれぞれのソースコードをZIP化し、

  • invoker.zip
  • receiver.zip

という名前で適当なストレージバケットにアップロードしていることが前提になります。ZIPの準備ができたら以下のテンプレートからまとめてデプロイします。

resources.yml

resources:
  - type: pubsub.v1.topic
    name: pubsub-topic
    properties:
      topic: test-topic
  - type: gcp-types/iam-v1:projects.serviceAccounts
    name: service-account
    properties:
      accountId: cloud-functions-invoker
      displayName: Service Account For Invoke Cloud Functions
  - type: gcp-types/cloudfunctions-v1:projects.locations.functions
    name: invoker
    properties:
      parent: projects/<プロジェクトID>/locations/asia-northeast1
      function: invoker
      sourceArchiveUrl: gs://<適当なストレージバケット>/invoker.zip
      serviceAccountEmail: $(ref.service-account.email)
      entryPoint: handler
      runtime: python39
      eventTrigger:
        resource: $(ref.pubsub-topic.name)
        eventType: providers/cloud.pubsub/eventTypes/topic.publish
  - type: gcp-types/cloudfunctions-v1:projects.locations.functions
    name: receiver
    properties:
      parent: projects/<プロジェクトID>/locations/asia-northeast1
      function: receiver
      sourceArchiveUrl: gs://<適当なストレージバケット>/receiver.zip
      entryPoint: handler
      runtime: python39
      eventTrigger:
        resource: $(ref.pubsub-topic.name)
        eventType: providers/cloud.pubsub/eventTypes/topic.publish

gcloud CLIを使ってデプロイします

gcloud deployment-manager deployments create cf-test --config resources.yml

デプロイできたらinvokerをテスト実行してみましょう

以下のエラーメッセージが表示され、実行に失敗してしまいます

Error: function terminated. Recommended action: inspect logs for termination reason. Additional troubleshooting documentation can be found at https://cloud.google.com/functions/docs/troubleshooting#logging Details:
500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.

ログの詳細を確認すると、以下のようにPermission 'cloudfunctions.functions.call' denied on resource 'projects/<プロジェクト名>/locations/asia-northeast1/functions/receiver' (or resource may not exist).というエラーメッセージが出力されていることが分かります。

エラーメッセージから、Deployment Managerの構成ファイル内でCloud Functions用に作成したサービスアカウントが別のCloud Functionsを呼び出す権限を持っていないことが原因と推測できます。

2.Deployment ManagerのaccessControlセクションを使って権限を追加してみる

原因がわかったので、Cloud Functions用のサービスアカウントにcloudfunctions.developerのロールを割り当てましょう。今回デプロイしている関数はHTTPトリガーではないので、権限としてはcloudfunctions.functions.callの権限が必要になります

Cloud Functions IAM Permissions

そのため、指定するロールはcloudfunctions.functions.callの権限を持つroles/cloudfunctions.developerのロールを指定します。

Cloud Functions IAM Roles

Deplyment ManagerのaccessControlを利用して以下のように設定を追加します。

  - type: gcp-types/cloudfunctions-v1:projects.locations.functions
    name: receiver
    properties:
      parent: projects/<プロジェクトID>/locations/asia-northeast1
      ...略
    accessControl:
      gcpIamPolicy:
        bindings:
          - role: roles/cloudfunctions.developer
            members:
              - serviceAccount:$(ref.service-account.email)

構成ファイルが準備できたのでデプロイします

gcloud deployment-manager deployments update cf-test --config resources.yml

これでデプロイ完了すれば先程の権限エラーが解消されるはず...と思いきやこんなエラーが発生してしまいました

The fingerprint of the deployment is b'-FahLD1dFHh7hG29Y4yh-w=='
Waiting for update [operation-1630244417139-5cab2db617393-a5e426c8-7bd2e65c]...failed.
ERROR: (gcloud.deployment-manager.deployments.update) Error in Operation [operation-1630244417139-5cab2db617393-a5e426c8-7bd2e65c]: errors:
- code: RESOURCE_ERROR
  location: /deployments/cf-test/resources/receiver
  message: '{"ResourceType":"gcp-types/cloudfunctions-v1:projects.locations.functions","ResourceErrorCode":"404","ResourceErrorMessage":{"statusMessage":"Not
    Found","requestPath":"https://cloudfunctions.googleapis.com/v1/:setIamPolicy","httpMethod":"POST"}}'

3.gcp-types/cloudfunctions-v1:virtual.projects.locations.functions.iamMemberBindingを使って権限を追加してみる

先程のエラーメッセージをググってみると、こんなissueを発見しました

setIamPolicy for Cloud Functions #494

Cloud FunctionsのAPI仕様的にaccessControlセクションが利用できないので、代替策としてgcp-types/cloudfunctions-v1:virtual.projects.locations.functions.iamMemberBindingのリソースをデプロイして、サービスアカウントにロールを割り当てる必要があるそうです。issueを参考にDeployment Managerの構成ファイルに以下の記述を追加します

  - name: binding-cf-developer
    type: gcp-types/cloudfunctions-v1:virtual.projects.locations.functions.iamMemberBinding
    properties:
      resource: $(ref.receiver.name)
      role: roles/cloudfunctions.developer
      member: serviceAccount:$(ref.service-account.email)

構成ファイルの全体は以下のようになります

resources.yml

resources:
  - type: pubsub.v1.topic
    name: pubsub-topic
    properties:
      topic: test-topic
  - type: gcp-types/iam-v1:projects.serviceAccounts
    name: service-account
    properties:
      accountId: cloud-functions-invoker
      displayName: Service Account For Invoke Cloud Functions
  - type: gcp-types/cloudfunctions-v1:projects.locations.functions
    name: invoker
    properties:
      parent: projects/<プロジェクトID>/locations/asia-northeast1
      function: invoker
      sourceArchiveUrl: gs://<適当なストレージバケット>/invoker.zip
      serviceAccountEmail: $(ref.service-account.email)
      entryPoint: handler
      runtime: python39
      eventTrigger:
        resource: $(ref.pubsub-topic.name)
        eventType: providers/cloud.pubsub/eventTypes/topic.publish
  - type: gcp-types/cloudfunctions-v1:projects.locations.functions
    name: receiver
    properties:
      parent: projects/<プロジェクトID>/locations/asia-northeast1
      function: receiver
      sourceArchiveUrl: gs://<適当なストレージバケット>/receiver.zip
      entryPoint: handler
      runtime: python39
      eventTrigger:
        resource: $(ref.pubsub-topic.name)
        eventType: providers/cloud.pubsub/eventTypes/topic.publish
  - name: binding-cf-developer
    type: gcp-types/cloudfunctions-v1:virtual.projects.locations.functions.iamMemberBinding
    properties:
      resource: $(ref.receiver.name)
      role: roles/cloudfunctions.developer
      member: serviceAccount:$(ref.service-account.email)

再度デプロイを実行します

gcloud deployment-manager deployments update cf-test --config resources.yml

すると...

The fingerprint of the deployment is b'7nW3gWkSoKYs9lpVTd1tdA=='
Waiting for update [operation-1630245139328-5cab3066d2dbd-94d7bd65-43fae94a]...failed.
ERROR: (gcloud.deployment-manager.deployments.update) Error in Operation [operation-1630245139328-5cab3066d2dbd-94d7bd65-43fae94a]: errors:
- code: RESOURCE_ERROR
  location: /deployments/cf-test/resources/binding-cf-developer
  message: "{\"ResourceType\":\"gcp-types/cloudfunctions-v1:virtual.projects.locations.functions.iamMemberBinding\"\
    ,\"ResourceErrorCode\":\"403\",\"ResourceErrorMessage\":{\"code\":403,\"message\"\
    :\"Permission 'cloudfunctions.functions.setIamPolicy' denied on resource 'projects/<プロジェクト名>/locations/asia-northeast1/functions/receiver'\
    \ (or resource may not exist).\",\"status\":\"PERMISSION_DENIED\",\"statusMessage\"\
    :\"Forbidden\",\"requestPath\":\"https://cloudfunctions.googleapis.com/v1/projects/<プロジェクト名>/locations/asia-northeast1/functions/receiver:setIamPolicy\"\
    ,\"httpMethod\":\"POST\"}}"

今度はこんなエラーが出てしまいました

4.Deployment ManagerにIAM ポリシーを設定するための権限を付与してからリトライする

先程のエラーメッセージをググると、以下の公式ドキュメントに行き着きました。

Deployment Manager に IAM ポリシーを設定するための権限を付与する

Deployment Managerは「Google API サービス アカウント」を利用して他のGoogle APIを呼び出しており、この「Google API サービス アカウント」がroles/editorのロールしか持たないため、Deployment Managerを利用したIAMポリシーの設定は実行できないそうです。上記公式ドキュメントの手順に従って「Google API サービス アカウント」にroles/ownerの権限を追加してみます。

gcloud projects add-iam-policy-binding <プロジェクトID> \
    --member serviceAccount:<プロジェクトNo>@cloudservices.gserviceaccount.com --role roles/owner

権限が付与できたので、再度デプロイを実行します

gcloud deployment-manager deployments update cf-test --config resources.yml

今度は無事に成功しました

The fingerprint of the deployment is b'vfH8XsolpIBEGK0Hgo-nuQ=='
Waiting for update [operation-1630247071848-5cab3799d175a-c3b5e8e0-0673592f]...done.
Update operation operation-1630247071848-5cab3799d175a-c3b5e8e0-0673592f completed successfully.
NAME                  TYPE                                                                               STATE      ERRORS  INTENT
binding-cf-developer  gcp-types/cloudfunctions-v1:virtual.projects.locations.functions.iamMemberBinding  COMPLETED  []
invoker               gcp-types/cloudfunctions-v1:projects.locations.functions                           COMPLETED  []
pubsub-topic          pubsub.v1.topic                                                                    COMPLETED  []
receiver              gcp-types/cloudfunctions-v1:projects.locations.functions                           COMPLETED  []
service-account       gcp-types/iam-v1:projects.serviceAccounts                                          COMPLETED  []

改めてinvokerをテスト実行してみましょう

無事テスト実行に成功しました

まとめ

  • デフォルトのポリシーではDeployment ManagerからIAMポリシーの設定ができない
  • Deployment ManagerのaccessControlではCloud Functionsの関数にロールのバインディングを設定できない

という点にハマってしまいました。似たような境遇でお悩みの方の参考になれば幸いです。

参考