Quick take — A reusable Terraform module for google_cloudfunctions2_function: build & runtime config, Eventarc triggers, dedicated runtime service account, and least-privilege invoker IAM — pinned to hashicorp/google ~> 5.0. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.
Quickstart (copy-paste)
Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):
provider "google" {
project = "my-project"
region = "us-central1"
}
module "cloud_functions" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-functions?ref=v1.0.0"
project_id = "..." # GCP project ID that hosts the function.
location = "..." # Region for the function (e.g. us-central1).
name = "..." # Function name; lowercase alphanumeric/hyphen, starts wi…
runtime = "..." # Runtime, e.g. nodejs20, python312, go122, java21.
entry_point = "..." # Handler name within the source to execute.
source_bucket = "..." # GCS bucket holding the zipped source object.
source_object = "..." # GCS object path of the zipped source.
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
What this module is
Cloud Functions (2nd gen) is Google Cloud’s rebuilt functions-as-a-service offering. Unlike 1st gen, a 2nd gen function is a Cloud Run service under the hood plus an Eventarc trigger — so you inherit Cloud Run’s concurrency (up to 1,000 concurrent requests per instance), larger instances (up to 16 GiB / 4 vCPU), minimum-instance warm pools, request timeouts up to 60 minutes for event-driven functions, and traffic-splitting revisions. The single google_cloudfunctions2_function resource stitches together three things that are easy to misconfigure by hand: the build (source bucket object, runtime, entry point), the service (scaling, concurrency, memory, service account, VPC/ingress), and the optional event trigger (Eventarc event type, Pub/Sub or Cloud Storage source, retry policy).
Wrapping it in a module matters because the failure modes are repetitive and operational, not creative. Every team forgets to give the function its own runtime service account (so it silently runs as the default compute SA with Editor), forgets that the build step needs roles/cloudfunctions.builder and a source object that actually exists, sets available_memory as a bare number instead of "256M", or leaves ingress_settings wide open. This module makes the runtime SA, the source-object wiring, the trigger’s own SA, and least-privilege cloudfunctions2_function_iam_member invoker grants first-class, validated inputs so each deployment is reproducible and auditable.
When to use it
- Event-driven glue: react to a Cloud Storage
object.finalizedevent, a Pub/Sub message, or any Eventarc/Audit-Log event without standing up a GKE/Cloud Run service yourself. - Lightweight HTTP APIs that benefit from Cloud Run’s concurrency and scale-to-zero but want the simpler functions deployment model (just source + entry point, no Dockerfile).
- Per-team / per-environment fan-out: you need the same function (image, runtime, IAM posture) deployed across
dev/staging/prodprojects with only variable changes. - When you specifically need 2nd-gen features: concurrency > 1, min-instances warm pools, > 540 s timeouts, > 8 GiB memory, or traffic-split revisions. If you need none of those and want the absolute simplest 1st-gen behaviour, use
google_cloudfunctions_functioninstead.
Module structure
terraform-module-gcp-cloud-functions/
├── versions.tf # provider pin (hashicorp/google ~> 5.0)
├── main.tf # runtime SA, the function, trigger SA roles, invoker IAM
├── variables.tf # var-driven inputs with validations
└── outputs.tf # id/name + service URI, runtime SA email, build/service config
# versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
# main.tf
locals {
# 2nd-gen functions run on Cloud Run; build + invoke both need a Cloud Build
# backed source object that already exists in GCS.
create_runtime_sa = var.runtime_service_account_email == null
runtime_sa_email = local.create_runtime_sa ? (
google_service_account.runtime[0].email
) : var.runtime_service_account_email
# Eventarc needs its own SA with permission to receive events and (for the
# event flow) act as a token creator. Default to the runtime SA unless overridden.
trigger_sa_email = var.event_trigger == null ? null : coalesce(
var.event_trigger.service_account_email,
local.runtime_sa_email,
)
}
# ---------------------------------------------------------------------------
# Dedicated runtime service account (created unless caller supplies one)
# ---------------------------------------------------------------------------
resource "google_service_account" "runtime" {
count = local.create_runtime_sa ? 1 : 0
project = var.project_id
account_id = "${substr(var.name, 0, 26)}-run"
display_name = "Runtime SA for Cloud Function ${var.name}"
description = "Least-privilege runtime identity for 2nd-gen function ${var.name}"
}
# ---------------------------------------------------------------------------
# Project-level roles the function's runtime SA needs to do its job
# (e.g. roles/secretmanager.secretAccessor, roles/datastore.user, ...)
# ---------------------------------------------------------------------------
resource "google_project_iam_member" "runtime_roles" {
for_each = toset(var.runtime_service_account_roles)
project = var.project_id
role = each.value
member = "serviceAccount:${local.runtime_sa_email}"
}
# ---------------------------------------------------------------------------
# Roles required by the Eventarc trigger SA (only when a trigger is configured)
# ---------------------------------------------------------------------------
resource "google_project_iam_member" "trigger_event_receiver" {
count = var.event_trigger == null ? 0 : 1
project = var.project_id
role = "roles/eventarc.eventReceiver"
member = "serviceAccount:${local.trigger_sa_email}"
}
# ---------------------------------------------------------------------------
# The 2nd-gen Cloud Function
# ---------------------------------------------------------------------------
resource "google_cloudfunctions2_function" "this" {
project = var.project_id
name = var.name
location = var.location
description = var.description
labels = var.labels
build_config {
runtime = var.runtime
entry_point = var.entry_point
service_account = var.build_service_account
environment_variables = var.build_environment_variables
source {
storage_source {
bucket = var.source_bucket
object = var.source_object
generation = var.source_generation
}
}
dynamic "automatic_update_policy" {
for_each = var.runtime_update_policy == "automatic" ? [1] : []
content {}
}
dynamic "on_deploy_update_policy" {
for_each = var.runtime_update_policy == "on_deploy" ? [1] : []
content {}
}
}
service_config {
available_memory = var.available_memory
available_cpu = var.available_cpu
timeout_seconds = var.timeout_seconds
max_instance_count = var.max_instance_count
min_instance_count = var.min_instance_count
# Concurrency > 1 is the headline 2nd-gen feature; requires >= 1 vCPU.
max_instance_request_concurrency = var.max_instance_request_concurrency
ingress_settings = var.ingress_settings
all_traffic_on_latest_revision = true
service_account_email = local.runtime_sa_email
environment_variables = var.runtime_environment_variables
vpc_connector = var.vpc_connector
vpc_connector_egress_settings = (
var.vpc_connector == null ? null : var.vpc_connector_egress_settings
)
dynamic "secret_environment_variables" {
for_each = var.secret_environment_variables
content {
key = secret_environment_variables.value.key
project_id = coalesce(secret_environment_variables.value.project_id, var.project_id)
secret = secret_environment_variables.value.secret
version = secret_environment_variables.value.version
}
}
}
dynamic "event_trigger" {
for_each = var.event_trigger == null ? [] : [var.event_trigger]
content {
trigger_region = coalesce(event_trigger.value.trigger_region, var.location)
event_type = event_trigger.value.event_type
pubsub_topic = event_trigger.value.pubsub_topic
retry_policy = event_trigger.value.retry_policy
service_account_email = local.trigger_sa_email
dynamic "event_filters" {
for_each = event_trigger.value.event_filters
content {
attribute = event_filters.value.attribute
value = event_filters.value.value
operator = event_filters.value.operator
}
}
}
}
lifecycle {
precondition {
condition = var.max_instance_request_concurrency == 1 || var.available_cpu != null
error_message = "max_instance_request_concurrency > 1 requires available_cpu to be set (>= 1 vCPU)."
}
}
}
# ---------------------------------------------------------------------------
# Invoker IAM on the underlying Cloud Run service (2nd gen).
# For HTTP functions, members listed here may invoke the function URL.
# ---------------------------------------------------------------------------
resource "google_cloudfunctions2_function_iam_member" "invoker" {
for_each = toset(var.invoker_members)
project = var.project_id
location = var.location
cloud_function = google_cloudfunctions2_function.this.name
role = "roles/cloudfunctions.invoker"
member = each.value
}
# variables.tf
variable "project_id" {
description = "GCP project ID that hosts the function."
type = string
}
variable "location" {
description = "Region for the function (e.g. us-central1, europe-west1)."
type = string
}
variable "name" {
description = "Function name. Lowercase letters, digits and hyphens; must start with a letter."
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{0,61}[a-z0-9]$", var.name))
error_message = "name must be 2-63 chars, lowercase alphanumeric/hyphen, start with a letter, not end with a hyphen."
}
}
variable "description" {
description = "Human-readable description of the function."
type = string
default = null
}
variable "labels" {
description = "Labels applied to the function (e.g. team, env, cost-center)."
type = map(string)
default = {}
}
# ---- Build config -------------------------------------------------------
variable "runtime" {
description = "Execution runtime, e.g. nodejs20, python312, go122, java21, dotnet8, ruby33."
type = string
validation {
condition = can(regex("^(nodejs|python|go|java|dotnet|ruby|php)[0-9]+$", var.runtime))
error_message = "runtime must look like nodejs20 / python312 / go122 / java21 / dotnet8 / ruby33."
}
}
variable "entry_point" {
description = "Name of the function/handler within the source to execute."
type = string
}
variable "source_bucket" {
description = "GCS bucket holding the zipped source object."
type = string
}
variable "source_object" {
description = "GCS object (path) of the zipped source, e.g. functions/orders/v3.zip."
type = string
}
variable "source_generation" {
description = "Optional GCS object generation to pin an exact source version (immutable deploys)."
type = number
default = null
}
variable "build_service_account" {
description = "Optional service account (full resource name) used by Cloud Build for the build step. Defaults to the project default Cloud Build SA when null."
type = string
default = null
}
variable "build_environment_variables" {
description = "Environment variables available only at build time."
type = map(string)
default = {}
}
variable "runtime_update_policy" {
description = "How the base image/runtime is updated: 'automatic' (security patches auto-applied) or 'on_deploy' (only on redeploy)."
type = string
default = "automatic"
validation {
condition = contains(["automatic", "on_deploy"], var.runtime_update_policy)
error_message = "runtime_update_policy must be 'automatic' or 'on_deploy'."
}
}
# ---- Service (Cloud Run) config ----------------------------------------
variable "available_memory" {
description = "Memory per instance as a quantity string, e.g. 256M, 512M, 1Gi, 4Gi (up to 16Gi)."
type = string
default = "256M"
validation {
condition = can(regex("^[0-9]+(M|Mi|G|Gi)$", var.available_memory))
error_message = "available_memory must be a quantity like 256M, 512M, 1Gi or 4Gi."
}
}
variable "available_cpu" {
description = "vCPU per instance as a string, e.g. '0.583', '1', '2', '4'. Required (>= 1) when concurrency > 1. Null lets GCP derive it from memory."
type = string
default = null
}
variable "timeout_seconds" {
description = "Request timeout. Up to 3600 for event-driven, up to 3600 for HTTP on 2nd gen."
type = number
default = 60
validation {
condition = var.timeout_seconds >= 1 && var.timeout_seconds <= 3600
error_message = "timeout_seconds must be between 1 and 3600."
}
}
variable "min_instance_count" {
description = "Minimum (always-warm) instances. > 0 removes cold starts but bills idle instances."
type = number
default = 0
validation {
condition = var.min_instance_count >= 0
error_message = "min_instance_count must be >= 0."
}
}
variable "max_instance_count" {
description = "Maximum instances the function may scale to."
type = number
default = 100
validation {
condition = var.max_instance_count >= 1
error_message = "max_instance_count must be >= 1."
}
}
variable "max_instance_request_concurrency" {
description = "Concurrent requests per instance (1-1000). The defining 2nd-gen feature; requires available_cpu >= 1 when > 1."
type = number
default = 1
validation {
condition = var.max_instance_request_concurrency >= 1 && var.max_instance_request_concurrency <= 1000
error_message = "max_instance_request_concurrency must be between 1 and 1000."
}
}
variable "ingress_settings" {
description = "Ingress control: ALLOW_ALL, ALLOW_INTERNAL_ONLY, or ALLOW_INTERNAL_AND_GCLB."
type = string
default = "ALLOW_ALL"
validation {
condition = contains(["ALLOW_ALL", "ALLOW_INTERNAL_ONLY", "ALLOW_INTERNAL_AND_GCLB"], var.ingress_settings)
error_message = "ingress_settings must be ALLOW_ALL, ALLOW_INTERNAL_ONLY, or ALLOW_INTERNAL_AND_GCLB."
}
}
variable "runtime_environment_variables" {
description = "Environment variables available at runtime."
type = map(string)
default = {}
}
variable "secret_environment_variables" {
description = "Secret Manager values mounted as env vars."
type = list(object({
key = string
secret = string
version = string
project_id = optional(string)
}))
default = []
}
variable "vpc_connector" {
description = "Optional Serverless VPC Access connector (full resource name) for private egress."
type = string
default = null
}
variable "vpc_connector_egress_settings" {
description = "VPC egress: PRIVATE_RANGES_ONLY or ALL_TRAFFIC. Ignored when vpc_connector is null."
type = string
default = "PRIVATE_RANGES_ONLY"
validation {
condition = contains(["PRIVATE_RANGES_ONLY", "ALL_TRAFFIC"], var.vpc_connector_egress_settings)
error_message = "vpc_connector_egress_settings must be PRIVATE_RANGES_ONLY or ALL_TRAFFIC."
}
}
# ---- Runtime identity & invoker IAM ------------------------------------
variable "runtime_service_account_email" {
description = "Existing runtime SA email. If null, the module creates a dedicated least-privilege SA."
type = string
default = null
}
variable "runtime_service_account_roles" {
description = "Project roles to grant the runtime SA (e.g. roles/secretmanager.secretAccessor, roles/datastore.user)."
type = list(string)
default = []
}
variable "invoker_members" {
description = "IAM members granted roles/cloudfunctions.invoker on the function (e.g. allUsers, serviceAccount:..., group:...)."
type = list(string)
default = []
}
# ---- Event trigger (Eventarc) ------------------------------------------
variable "event_trigger" {
description = <<-EOT
Optional Eventarc trigger. Set to null for an HTTP-only function. Example (Pub/Sub):
{
event_type = "google.cloud.pubsub.topic.v1.messagePublished"
pubsub_topic = "projects/p/topics/orders"
retry_policy = "RETRY_POLICY_RETRY"
}
Example (GCS object finalized):
{
event_type = "google.cloud.storage.object.v1.finalized"
retry_policy = "RETRY_POLICY_RETRY"
event_filters = [{ attribute = "bucket", value = "my-uploads-bucket" }]
}
EOT
type = object({
event_type = string
pubsub_topic = optional(string)
retry_policy = optional(string, "RETRY_POLICY_RETRY")
trigger_region = optional(string)
service_account_email = optional(string)
event_filters = optional(list(object({
attribute = string
value = string
operator = optional(string)
})), [])
})
default = null
validation {
condition = var.event_trigger == null ? true : contains(
["RETRY_POLICY_RETRY", "RETRY_POLICY_DO_NOT_RETRY"],
var.event_trigger.retry_policy
)
error_message = "event_trigger.retry_policy must be RETRY_POLICY_RETRY or RETRY_POLICY_DO_NOT_RETRY."
}
}
# outputs.tf
output "id" {
description = "Fully-qualified function ID (projects/.../locations/.../functions/...)."
value = google_cloudfunctions2_function.this.id
}
output "name" {
description = "Short function name."
value = google_cloudfunctions2_function.this.name
}
output "uri" {
description = "HTTPS URI of the underlying Cloud Run service (use to invoke an HTTP function)."
value = google_cloudfunctions2_function.this.service_config[0].uri
}
output "service_name" {
description = "Name of the backing Cloud Run service (for Cloud Run-level operations/monitoring)."
value = google_cloudfunctions2_function.this.service_config[0].service
}
output "runtime_service_account_email" {
description = "Email of the runtime service account the function executes as."
value = local.runtime_sa_email
}
output "state" {
description = "Current lifecycle state of the function (ACTIVE, FAILED, DEPLOYING, ...)."
value = google_cloudfunctions2_function.this.state
}
output "event_trigger_id" {
description = "Eventarc trigger resource name, or null for HTTP-only functions."
value = try(google_cloudfunctions2_function.this.event_trigger[0].trigger, null)
}
How to use it
# A GCS-triggered image-thumbnailing function that reads secrets and writes to Firestore.
resource "google_storage_bucket" "uploads" {
project = "kv-media-prod"
name = "kv-media-prod-uploads"
location = "US"
uniform_bucket_level_access = true
}
module "cloud_functions_2nd_gen_thumbnailer" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-functions?ref=v1.0.0"
project_id = "kv-media-prod"
location = "us-central1"
name = "media-thumbnailer"
description = "Generates thumbnails when an object is finalized in the uploads bucket"
labels = { team = "media", env = "prod", "cost-center" = "cc-4471" }
runtime = "python312"
entry_point = "on_object_finalized"
source_bucket = "kv-media-prod-artifacts"
source_object = "functions/thumbnailer/v3.zip"
source_generation = 1736500000123456
# 2nd-gen scaling: concurrency requires CPU; keep one warm instance to avoid cold starts.
available_memory = "1Gi"
available_cpu = "1"
max_instance_request_concurrency = 20
min_instance_count = 1
max_instance_count = 50
timeout_seconds = 300
ingress_settings = "ALLOW_INTERNAL_AND_GCLB"
runtime_environment_variables = {
THUMBNAIL_BUCKET = "kv-media-prod-thumbnails"
LOG_LEVEL = "INFO"
}
secret_environment_variables = [
{
key = "SIGNING_KEY"
secret = "media-signing-key"
version = "latest"
}
]
# Dedicated runtime SA gets exactly the roles it needs.
runtime_service_account_roles = [
"roles/secretmanager.secretAccessor",
"roles/datastore.user",
"roles/storage.objectAdmin",
]
event_trigger = {
event_type = "google.cloud.storage.object.v1.finalized"
retry_policy = "RETRY_POLICY_RETRY"
event_filters = [
{ attribute = "bucket", value = google_storage_bucket.uploads.name }
]
}
}
# Downstream: grant the function's runtime SA write access to the thumbnails bucket
# using the module output, and surface the service name for a Cloud Run alert policy.
resource "google_storage_bucket_iam_member" "thumbnailer_writer" {
bucket = "kv-media-prod-thumbnails"
role = "roles/storage.objectCreator"
member = "serviceAccount:${module.cloud_functions_2nd_gen_thumbnailer.runtime_service_account_email}"
}
output "thumbnailer_run_service" {
value = module.cloud_functions_2nd_gen_thumbnailer.service_name
}
With Terragrunt
Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.
1. Root config — live/terragrunt.hcl (inherited by every module):
remote_state {
backend = "gcs"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
# ...gcs state bucket/container + key per path...
}
}
2. Module config — live/prod/cloud_functions/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-functions?ref=v1.0.0"
}
inputs = {
project_id = "..."
location = "..."
name = "..."
runtime = "..."
entry_point = "..."
source_bucket = "..."
source_object = "..."
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/cloud_functions && terragrunt apply # this module
terragrunt run-all apply # every module under live/prod
Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| project_id | string | — | yes | GCP project ID that hosts the function. |
| location | string | — | yes | Region for the function (e.g. us-central1). |
| name | string | — | yes | Function name; lowercase alphanumeric/hyphen, starts with a letter. |
| description | string | null | no | Human-readable description. |
| labels | map(string) | {} | no | Labels (team, env, cost-center, …). |
| runtime | string | — | yes | Runtime, e.g. nodejs20, python312, go122, java21. |
| entry_point | string | — | yes | Handler name within the source to execute. |
| source_bucket | string | — | yes | GCS bucket holding the zipped source object. |
| source_object | string | — | yes | GCS object path of the zipped source. |
| source_generation | number | null | no | Pin an exact GCS object generation for immutable deploys. |
| build_service_account | string | null | no | Cloud Build SA (full resource name) for the build step. |
| build_environment_variables | map(string) | {} | no | Build-time environment variables. |
| runtime_update_policy | string | “automatic” | no | Base-image update policy: automatic or on_deploy. |
| available_memory | string | “256M” | no | Memory quantity per instance (256M…16Gi). |
| available_cpu | string | null | no | vCPU per instance; required (>=1) when concurrency > 1. |
| timeout_seconds | number | 60 | no | Request timeout, 1–3600. |
| min_instance_count | number | 0 | no | Always-warm instances (kills cold starts; bills idle). |
| max_instance_count | number | 100 | no | Maximum instances. |
| max_instance_request_concurrency | number | 1 | no | Concurrent requests per instance, 1–1000 (2nd-gen feature). |
| ingress_settings | string | “ALLOW_ALL” | no | ALLOW_ALL / ALLOW_INTERNAL_ONLY / ALLOW_INTERNAL_AND_GCLB. |
| runtime_environment_variables | map(string) | {} | no | Runtime environment variables. |
| secret_environment_variables | list(object) | [] | no | Secret Manager values mounted as env vars. |
| vpc_connector | string | null | no | Serverless VPC Access connector for private egress. |
| vpc_connector_egress_settings | string | “PRIVATE_RANGES_ONLY” | no | VPC egress mode; ignored when no connector. |
| runtime_service_account_email | string | null | no | Existing runtime SA; if null a dedicated SA is created. |
| runtime_service_account_roles | list(string) | [] | no | Project roles granted to the runtime SA. |
| invoker_members | list(string) | [] | no | Members granted roles/cloudfunctions.invoker. |
| event_trigger | object | null | no | Eventarc trigger config; null for HTTP-only functions. |
Outputs
| Name | Description |
|---|---|
| id | Fully-qualified function ID (projects/…/locations/…/functions/…). |
| name | Short function name. |
| uri | HTTPS URI of the backing Cloud Run service (invoke an HTTP function). |
| service_name | Name of the backing Cloud Run service for monitoring/ops. |
| runtime_service_account_email | Email of the runtime SA the function executes as. |
| state | Lifecycle state (ACTIVE, FAILED, DEPLOYING, …). |
| event_trigger_id | Eventarc trigger resource name, or null for HTTP-only functions. |
Enterprise scenario
A payments platform processes settlement files dropped hourly into a landing bucket by partner banks. They instantiate this module once per environment with a GCS object.v1.finalized trigger, retry_policy = RETRY_POLICY_RETRY, min_instance_count = 1 to avoid cold-start latency on the first file, and max_instance_request_concurrency = 1 so each settlement file is parsed in isolation. The dedicated runtime SA is granted only roles/secretmanager.secretAccessor (for the HSM signing key) and roles/pubsub.publisher (to emit a settlement.parsed event), and ingress_settings = ALLOW_INTERNAL_ONLY keeps the function off the public internet — giving auditors a clean, least-privilege blast radius per environment.
Best practices
- Always run as a dedicated runtime SA. Leave
runtime_service_account_email = nullso the module creates a per-function identity, and grant the minimum viaruntime_service_account_roles. Never let a 2nd-gen function fall back to the default compute SA (which often carries Editor). - Pin the source object generation in production. Set
source_generationso a redeploy is reproducible and a re-uploaded zip under the same path cannot silently change running code; bump it deliberately per release tag. - Right-size concurrency vs. isolation.
max_instance_request_concurrency > 1(withavailable_cpu >= 1) dramatically cuts cost for I/O-bound HTTP functions, but keep it at1for non-thread-safe handlers or strict per-message isolation (file/financial processing). - Lock down ingress and invoker IAM. Prefer
ALLOW_INTERNAL_ONLY/ALLOW_INTERNAL_AND_GCLBfor internal services, and grantroles/cloudfunctions.invokerto specific principals viainvoker_members— only addallUsersfor genuinely public endpoints. - Control cold starts with intent. Use
min_instance_count = 1+only where first-request latency matters; idle warm instances bill continuously, so default to0for spiky, latency-tolerant workloads. - Keep
runtime_update_policy = "automatic"so security patches land on the base image without a redeploy, and switch to"on_deploy"only when you need byte-for-byte build reproducibility for compliance.