Marko
Marko

Reputation: 141

Cannot invoke Google Cloud Function from GCP Scheduler

I've been trying to invoke a GCP function (--runtime nodejs8 --trigger-http) from GCP scheduler, both located within the same project. I can only make it work, if I grant unauthenticated access by adding the allUsers member to the functions permissions, with the Cloud Functions-Invoker role applied to it. However, when I only use the service account of the scheduler as the Cloud Functions-Invoker, I get a PERMISSION DENIED Error.

I created a hello world example, to show in detail, how my setup looks like.

  1. I set up a service account:

gcloud iam service-accounts create scheduler --display-name="Task Schedule Runner"

  1. Setting the role:
svc_policy.json:
{
    "bindings": [
      {
        "members": [
          "serviceAccount:[email protected]"
        ],
        "role": "roles/cloudscheduler.serviceAgent"
      }    
    ]
  }

gcloud iam service-accounts set-iam-policy [email protected] svc_policy.json  -q
  1. Deploying the Cloud Function:

gcloud functions deploy helloworld --runtime nodejs8 --trigger-http --entry-point=helloWorld

  1. Adding the service account as a member to the function:

gcloud functions add-iam-policy-binding helloworld --member serviceAccount:[email protected] --role roles/cloudfunctions.invoker

  1. Creating the scheduler job:

gcloud beta scheduler jobs create http test-job --schedule "5 * * * *" --http-method=GET --uri=https://us-central1-mwsdata-1544225920485.cloudfunctions.net/helloworld --oidc-service-account-email=scheduler@mwsdata-1544225920485.iam.gserviceaccount.com --oidc-token-audience=https://us-central1-mwsdata-1544225920485.cloudfunctions.net/helloworld

Log: PERMISSION DENIED

{
 httpRequest: {
 }
 insertId: "1ny5xuxf69w0ck"  
 jsonPayload: {
  @type: "type.googleapis.com/google.cloud.scheduler.logging.AttemptFinished"   
  jobName: "projects/mwsdata-1544225920485/locations/europe-west1/jobs/test-job"   
  status: "PERMISSION_DENIED"   
  targetType: "HTTP"   
  url: "https://us-central1-mwsdata-1544225920485.cloudfunctions.net/helloworld"   
 }
 logName: "projects/mwsdata-1544225920485/logs/cloudscheduler.googleapis.com%2Fexecutions"  
 receiveTimestamp: "2020-02-04T22:05:05.248707989Z"  
 resource: {
  labels: {
   job_id: "test-job"    
   location: "europe-west1"    
   project_id: "mwsdata-1544225920485"    
  }
  type: "cloud_scheduler_job"   
 }
 severity: "ERROR"  
 timestamp: "2020-02-04T22:05:05.248707989Z"  
}

Update

Here are the corresponding settings.

Scheduler Service Account

gcloud iam service-accounts get-iam-policy [email protected]

bindings:
- members:
  - serviceAccount:[email protected]
  role: roles/cloudscheduler.serviceAgent
etag: BwWdxuiGNv4=
version: 1

IAM Policy of the function:

gcloud functions get-iam-policy helloworld    
bindings:
- members:
  - serviceAccount:[email protected]
  role: roles/cloudfunctions.invoker
etag: BwWdxyDGOAY=
version: 1

Function Description

gcloud functions describe helloworld
availableMemoryMb: 256
entryPoint: helloWorld
httpsTrigger:
  url: https://us-central1-mwsdata-1544225920485.cloudfunctions.net/helloworld
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/mwsdata-1544225920485/locations/us-central1/functions/helloworld
runtime: nodejs8
serviceAccountEmail: [email protected]
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-671641e6-3f1b-41a1-9ac1-558224a1638a/b4a0e407-69b9-4f3d-a00d-7543ac33e013.zip?GoogleAccessId=service-617967399269@gcf-admin-robot.iam.gserviceaccount.com&Expires=1580854835&Signature=S605ODVtOpnU4LIoRT2MnU4OQN3PqhpR0u2CjgcpRcZZUXstQ5kC%2F1rT6Lv2SusvUpBrCcU34Og2hK1QZ3dOPluzhq9cXEvg5MX1MMDyC5Y%2F7KGTibnV4ztFwrVMlZNTj5N%2FzTQn8a65T%2FwPBNUJWK0KrIUue3GemOQZ4l4fCf9v4a9h6MMjetLPCTLQ1BkyFUHrVnO312YDjSC3Ck7Le8OiXb7a%2BwXjTDtbawR20NZWfgCCVvL6iM9mDZSaVAYDzZ6l07eXHXPZfrEGgkn7vXN2ovMF%2BNGvwHvTx7pmur1yQaLM4vRRprjsnErU%2F3p4JO3tlbbFEf%2B69Wd9dyIKVA%3D%3D
status: ACTIVE
timeout: 60s
updateTime: '2020-02-04T21:51:15Z'
versionId: '1'

Scheduler Job Description

gcloud scheduler jobs describe test-job
attemptDeadline: 180s
httpTarget:
  headers:
    User-Agent: Google-Cloud-Scheduler
  httpMethod: GET
  oidcToken:
    audience: https://us-central1-mwsdata-1544225920485.cloudfunctions.net/helloworld
    serviceAccountEmail: [email protected]
  uri: https://us-central1-mwsdata-1544225920485.cloudfunctions.net/helloworld
lastAttemptTime: '2020-02-05T09:05:00.054111Z'
name: projects/mwsdata-1544225920485/locations/europe-west1/jobs/test-job
retryConfig:
  maxBackoffDuration: 3600s
  maxDoublings: 16
  maxRetryDuration: 0s
  minBackoffDuration: 5s
schedule: 5 * * * *
scheduleTime: '2020-02-05T10:05:00.085854Z'
state: ENABLED
status:
  code: 7
timeZone: Etc/UTC
userUpdateTime: '2020-02-04T22:02:31Z'

Upvotes: 14

Views: 12521

Answers (5)

piotrekkr
piotrekkr

Reputation: 3181

Question is probably about first generation of Cloud Functions but maybe my struggle will help someone who is trying with second generation CF.

After quite a struggle with this I managed to setup proper permissions to second generation cloud function.

Few things I did not realize from the start:

  • when creating second generation function, GCP is also creating Cloud Run service connected to this function
  • all http trigger requests comes through Cloud Run service
  • Cloud Run SA is the actual caller of Cloud Function (at least this seems to be the case when I tested)
  • name of Cloud Run service is generated based on Cloud Function name and in most cases is the same (not when using underscore in CF name, it will replace it with dash my_cf_function => my-cf-function)

So what I did to make it work:

#!/bin/bash

set -e

SA_NAME="my-cf-sa"
PROJECT="my-project-id"
REGION="europe-west1"
SA_EMAIL="${SA_NAME}@${PROJECT}.iam.gserviceaccount.com"
IAM_MEMBER="serviceAccount:${SA_EMAIL}"
CLOUD_FUNCTION_NAME="my-http-function"
SCHEDULER_JOB_NAME="my-cf-job"

# Create SA to use with function

gcloud iam service-accounts create $SA_NAME \
--description="My CF SA" \
--display-name="My CF SA" \
--project=$PROJECT


# Deploy function with created SA

gcloud functions deploy $CLOUD_FUNCTION_NAME \
--gen2 \
--runtime=python311 \
--source=. \
--entry-point=my_http_function \
--trigger-http \
--service-account=$SA_EMAIL \
--run-service-account=$SA_EMAIL \
--no-allow-unauthenticated \
--region=$REGION \
--project=$PROJECT

# COMMAND BELOW DOES NOT WORK even though it is in documentation here https://cloud.google.com/scheduler/docs/http-target-auth
#
# Generate this error:
#
# ERROR: (gcloud.functions.add-iam-policy-binding) ResponseError: status=[400], code=[Ok], message=[Invalid argument: 'An invalid argument was specified. Please check the fields and try again.']
#
# Bug report for this: https://issuetracker.google.com/issues/284853816
#
# gcloud functions add-iam-policy-binding $CLOUD_FUNCTION_NAME --member=$IAM_MEMBER --role=roles/run.invoker --gen2 --region=$REGION --project=$PROJECT


# Add roles/cloudfunctions.invoker to allow SA to invoke function run
#
# After running this command it will ask you if you want to also add iam for Cloud Run service connected to CF.
#
#
#     WARNING: The role [roles/cloudfunctions.invoker] was successfully bound to member [serviceAccount:[email protected]]
#     but this does not grant the member permission to invoke 2nd gen function [my-http-function]. Instead, the role [roles/run.invoker] must be
#     granted on the underlying Cloud Run service. This can be done by running the `gcloud functions add-invoker-policy-binding` command.
#
#     Would you like to run this command and additionally grant [serviceAccount:[email protected]] permission to invoke function [my-http-function] (Y/n)?
#
# If you choose YES you don't need to manually run iam binding command for Cloud Run service.
# I choose NO and did this in two steps so I could replicate all this in terraform later on

gcloud functions add-iam-policy-binding $CLOUD_FUNCTION_NAME \
--member=$IAM_MEMBER \
--role=roles/cloudfunctions.invoker \
--gen2 \
--region=$REGION \
--project=$PROJECT

# Add roles/run.invoker to allow SA invoke Cloud Run service that is connected to function
#
# Only needed if you choose NO in previous command

gcloud run services add-iam-policy-binding $CLOUD_FUNCTION_NAME \
--member=$IAM_MEMBER \
--role=roles/run.invoker \
--region=$REGION \
--project=$PROJECT

# Create Scheduler job with OIDC auth set to use SA

gcloud scheduler jobs create http $SCHEDULER_JOB_NAME \
--schedule="0 */6 * * *" \
--uri="$(gcloud functions describe $CLOUD_FUNCTION_NAME --gen2 --project=$PROJECT --region=$REGION --format="value(serviceConfig.uri)")" \
--http-method=GET \
--oidc-service-account-email=$SA_EMAIL \
--location=$REGION \
--project=$PROJECT

You need to wait few mintes before actually running a job because IAM permissions may take some time to propagate everywhere.

After this I run job manually:

gcloud scheduler jobs run $SCHEDULER_JOB_NAME --location=$REGION --project=$PROJECT

I've checked logs and

gcloud logging read "resource.type=\"cloud_scheduler_job\" AND resource.labels.job_id=\"$SCHEDULER_JOB_NAME\" AND resource.labels.location=\"$REGION\"" --project=$PROJECT --limit 1

And I got 200 from CF

---
httpRequest:
  status: 200
insertId: gde4phfur06nd
jsonPayload:
  '@type': type.googleapis.com/google.cloud.scheduler.logging.AttemptFinished
  jobName: projects/my-project-id/locations/europe-west1/jobs/my-cf-job
  targetType: HTTP
  url: <FUNCTION_URL>
logName: projects/my-project-id/logs/cloudscheduler.googleapis.com%2Fexecutions
receiveTimestamp: '2023-06-03T16:40:28.454858025Z'
resource:
  labels:
    job_id: my-cf-job
    location: europe-west1
    project_id: my-project-id
  type: cloud_scheduler_job
severity: INFO
timestamp: '2023-06-03T16:40:28.454858025Z'

Upvotes: 1

Vitalii Kyrychenko
Vitalii Kyrychenko

Reputation: 316

I had a similar issue.

In our case, we've enabled Cloud Scheduler quite a long time ago.

According to the docs, if you enabled Cloud Scheduler API before March 19, 2019, you need to manually add the Cloud Scheduler Service Agent role to your Cloud Scheduler service account.

So we had to create a new service account that looks like this service-[project-number]@gcp-sa-cloudscheduler.iam.gserviceaccount.com

Hope this will help anybody else.

Upvotes: 5

this tutorial helped me to invoke a programmer function, but there is a problem when creating the program after creating the service account, finally eliminating the programmer and doing it again.

Google Cloud Scheduler - Calling Cloud Function

Upvotes: 0

Nathanael Steinhauer
Nathanael Steinhauer

Reputation: 315

@Marko I went through the same issue, it seems to re-enable (disable/enable) the scheduler API did the fix. This is why creating a new project makes sense because you probably got a scheduler service account by doing so. So if your project doesn't have a scheduler service account created from google, doing this trick will give you one. And although you don't need to assign this specific service account to any of your tasks, it must be available. You can see my work here: How to invoke Cloud Function from Cloud Scheduler with Authentication

Upvotes: 8

Daniel Ocando
Daniel Ocando

Reputation: 3764

Here are the steps I followed to make Cloud Scheduler trigger an HTTP triggered Cloud Function that doesn't allow unauthenticated invocations:

  1. Create a service account, which will have the following form [SA-NAME]@[PROJECT-ID].iam.gserviceaccount.com.
  2. Adde the service account [SA-NAME]@[PROJECT-ID].iam.gserviceaccount.com as a project member and added the following roles to the service account: Cloud Functions Invoker and Cloud Scheduler Admin.
  3. Deploy an HTTP triggered Cloud Function that doesn't allow public (unauthenticated) access (if you are using the UI, simply uncheck the Allow unauthenticated Invocations checkbox) and that used the recently created service account [SA-NAME]@[PROJECT-ID].iam.gserviceaccount.com on the Service account field (click more and look for the Service account field, by default it should be set to the App Engine default service account) and take notice of the Cloud Function's URL.
  4. Create a Cloud Scheduler job with authentication by issuing the following command from the Cloud Shell: gcloud scheduler jobs create http [JOB-NAME] --schedule="* * * * *" --uri=[CLOUD-FUNCTIONS-URL] --oidc-service-account-email=[SA-NAME]@[PROJECT-ID].iam.gserviceaccount.com

In your specific case you are leaving the default App Engine service account for your Cloud Functions. Change it to the service account you created as specified on the previous steps.

Upvotes: 15

Related Questions