Recently, I had the opportunity to migrate existing Cloud Functions from 1st generation to 2nd generation. During this process, I was surprised to find that the IAM configuration had changed more than expected, and the setup method for custom service accounts was also different from the 1st generation.
In this article, I’ll share the technical insights gained from the actual migration work and the configurations required to get everything working properly.
Target Audience
- Those using Cloud Functions on Google Cloud
- Those considering or implementing migration from 1st to 2nd generation
- Those managing Cloud Functions with Terraform
Initial Problem Encountered: Changes in IAM Resource Types
The first stumbling block in migrating to 2nd generation was that the Terraform IAM resources were completely different.
1st Generation Configuration (Traditional Method)
# This is how we wrote it in 1st generation
resource "google_cloudfunctions_function_iam_member" "invoker" {
project = var.project_id
region = "asia-northeast1"
cloud_function = "my-function"
role = "roles/cloudfunctions.invoker"
member = "serviceAccount:${var.service_account_email}"
}
2nd Generation Configuration (Using Cloud Run Resources)
# In 2nd generation, we need to use Cloud Run resources
resource "google_cloud_run_service_iam_member" "invoker" {
project = var.project_id
location = "asia-northeast1"
service = "my-function" # Function name becomes the service name
role = "roles/run.invoker" # The role has changed too!
member = "serviceAccount:${var.service_account_email}"
}
The reason for this change is that 2nd generation Cloud Functions now runs on a Cloud Run base1. In other words, it’s internally treated as a Cloud Run service.
Differences in Roles Between 1st and 2nd Generation (This is Important!)
What I struggled with most during the migration was that the required roles had changed significantly. After investigating the official Google Cloud documentation, the following differences became clear:
Roles Required for Deployment
1st Generation:
resource "google_project_iam_member" "cloud_build_permissions_gen1" {
for_each = toset([
"roles/cloudfunctions.developer", # Cloud Functions developer
"roles/iam.serviceAccountUser", # Service account usage permission
])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
2nd Generation2:
resource "google_project_iam_member" "cloud_build_permissions_gen2" {
for_each = toset([
"roles/cloudfunctions.developer", # Cloud Functions developer (still needed)
"roles/run.admin", # Cloud Run admin permission additionally required!
"roles/iam.serviceAccountUser", # Service account usage permission
"roles/eventarc.admin", # Required when using Pub/Sub triggers
"roles/artifactregistry.reader", # Access to container images
])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
Roles Required for Execution
1st Generation:
roles/cloudfunctions.invoker
- To invoke the function
2nd Generation3:
roles/run.invoker
- To invoke as a Cloud Run service4roles/eventarc.eventReceiver
- When receiving Eventarc events other than Pub/Sub5
If you migrate without understanding these differences, you’ll end up in a situation where “it should have permissions but it doesn’t work.”
In particular, the need for roles/run.admin
was a point that’s hard to notice without understanding the official documentation stating that “Cloud Functions 2nd generation runs on Cloud Run infrastructure”6.
Service Account Changes
Another important change is that the default runtime service account has changed7:
1st Generation:
- Runtime:
PROJECT_ID@appspot.gserviceaccount.com
(App Engine default)
2nd Generation:
- Runtime:
PROJECT_NUMBER-compute@developer.gserviceaccount.com
(Compute Engine default)
Lessons Learned from Custom Service Account Implementation
Issues with Default Service Account
By default, PROJECT_NUMBER-compute@developer.gserviceaccount.com
is used, but this account has Editor permissions8. From a security perspective, I decided to create a dedicated service account.
Working Configuration
Here’s a generalized version of the configuration that worked properly in the actual project:
# Service account for Cloud Build (executes deployment)
resource "google_service_account" "cloud_build" {
account_id = "cloud-build-deployer"
project = var.project_id
display_name = "Cloud Build Deployer"
description = "Service account for deploying Cloud Functions"
}
# Service account for Cloud Functions runtime
resource "google_service_account" "cloud_functions" {
account_id = "cf-my-function"
project = var.project_id
display_name = "Cloud Functions Runtime"
description = "Service account used at Cloud Functions runtime"
}
Required Permission Settings
This is a crucial point. Since Cloud Build needs to use the Cloud Functions service account to execute deployment, granting roles/iam.serviceAccountUser
permission was necessary9:
# Permission for Cloud Build to use Functions service account
resource "google_service_account_iam_member" "cloud_build_act_as" {
service_account_id = google_service_account.cloud_functions.name
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
This permission allows the Cloud Build service account to specify the Cloud Functions service account as the runtime identity for Cloud Functions. Without this configuration, you’ll get permission errors during deployment.
Special Cases Encountered with Pub/Sub Triggers
Pub/Sub Service Account Format
When using Pub/Sub triggers, the following configuration was necessary:
resource "google_cloud_run_service_iam_member" "pubsub_invoker" {
project = var.project_id
location = "asia-northeast1"
service = "my-function"
role = "roles/run.invoker"
member = "serviceAccount:service-${var.project_number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}
Note that you must use PROJECT_NUMBER
, not PROJECT_ID
. I didn’t notice this at first and wondered why I was getting permission errors.
Why Pub/Sub IAM Configuration Became Complex
Upon investigation, I found that in 2nd generation, Pub/Sub triggers now operate through Eventarc10. According to the official documentation, roles/eventarc.admin
permission is required when creating Eventarc triggers11, and the trigger service account needs roles/run.invoker
permission12.
Additional Requirements When Deploying via Cloud Build
After checking the official documentation, I found that when deploying Gen2 functions via Cloud Build, the following additional permissions were also required13:
# Additional permissions required for Cloud Build service account
resource "google_project_iam_member" "cloud_build_additional" {
for_each = toset([
"roles/storage.objectViewer", # Access to source code
"roles/logging.logWriter", # Write build logs
])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
Cloud Build Deployment Configuration
The actual Cloud Build configuration used (generalized):
steps:
- id: deploy cloud function
name: gcr.io/google.com/cloudsdktool/cloud-sdk
entrypoint: 'bash'
args:
- '-c'
- |
gcloud functions deploy my-function \
--gen2 \
--region=asia-northeast1 \
--trigger-topic=my-topic \
--runtime=nodejs20 \
--entry-point=main \
--service-account=${_CF_SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com
The --gen2
flag and --service-account
option are important.
Secret Manager Integration (Additional Discovery)
Benefits of Using Secret Manager Instead of Environment Variables
During the migration work, I noticed that Secret Manager integration has become easier to use in 2nd generation2.
# Access permissions for Secret Manager
resource "google_secret_manager_secret_iam_member" "function_secrets" {
for_each = toset([
"API_KEY",
"DATABASE_PASSWORD",
])
project = var.project_id
secret_id = each.value
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloud_functions.email}"
}
Deployment configuration:
--set-secrets="API_KEY=API_KEY:latest" \
--set-secrets="DB_PASSWORD=DATABASE_PASSWORD:latest"
This eliminated the need to set sensitive information directly in environment variables.
Lessons Learned from Migration
1. Relationship Between Function Name and Cloud Run Service Name
In 2nd generation, the deployed function name becomes the Cloud Run service name as is. Without understanding this, you’ll be confused when setting up IAM.
2. Distinguishing Between PROJECT_NUMBER and PROJECT_ID
Especially for Pub/Sub service accounts, you must use PROJECT_NUMBER:
Correct: service-123456789@gcp-sa-pubsub.iam.gserviceaccount.com
Wrong: service-my-project-id@gcp-sa-pubsub.iam.gserviceaccount.com
3. Debugging Permission Errors
When permission errors occur, checking the following in Cloud Logging makes it easier to identify the cause:
- Which service account is being used
- Which roles are missing
- Whether the resource name (especially Cloud Run service name) is correct
Summary
The migration to Cloud Functions 2nd generation involved major changes in the following areas:
- IAM resource type change: From
google_cloudfunctions_function_iam_member
togoogle_cloud_run_service_iam_member
- Role change: From
roles/cloudfunctions.invoker
toroles/run.invoker
- Additional required roles:
roles/run.admin
,roles/eventarc.admin
,roles/artifactregistry.reader
- Pub/Sub trigger complexity: Additional configuration due to operating through Eventarc
- Default service account change: From App Engine default to Compute Engine default
- Custom service account permissions: Need for
roles/iam.serviceAccountUser
These changes stem from 2nd generation being based on Cloud Run. The official documentation clearly states that “Cloud Functions 2nd generation runs on Cloud Run infrastructure”14, and this fundamental architectural change leads to the differences in IAM configuration.
Understanding these differences will help make the migration process smoother. In particular, knowing that Cloud Run-related permissions are needed is an important point to be aware of beforehand.
I hope this article helps those undertaking similar migration work.
References
- Compare Cloud Run functions | Cloud Run Documentation
- Access control with IAM | Cloud Run functions Documentation
- Function Identity | Cloud Run functions Documentation
- Terraform Tutorial | Cloud Run functions Documentation
Google Cloud Functions is now Cloud Run functions | Google Cloud Blog ↩︎
Cloud Functions IAM Roles | Cloud Run functions Documentation ↩︎ ↩︎
Access control with IAM | Cloud Run functions Documentation ↩︎
Since 2nd generation Cloud Functions runs on Cloud Run,
roles/run.invoker
is required instead ofroles/cloudfunctions.invoker
↩︎Roles and permissions for Cloud Run targets | Eventarc Standard ↩︎
Best practices for using service accounts | IAM Documentation ↩︎
Create triggers from Pub/Sub events | Cloud Run Documentation ↩︎
Roles and permissions for Cloud Run targets | Eventarc Standard ↩︎
This is the permission required for the Pub/Sub service agent to invoke Cloud Run functions ↩︎
Build process overview | Cloud Run functions Documentation ↩︎
Google Cloud Functions is now Cloud Run functions | Google Cloud Blog ↩︎