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 service4
  • roles/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:

  1. IAM resource type change: From google_cloudfunctions_function_iam_member to google_cloud_run_service_iam_member
  2. Role change: From roles/cloudfunctions.invoker to roles/run.invoker
  3. Additional required roles: roles/run.admin, roles/eventarc.admin, roles/artifactregistry.reader
  4. Pub/Sub trigger complexity: Additional configuration due to operating through Eventarc
  5. Default service account change: From App Engine default to Compute Engine default
  6. 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