Deploying GCP Cloud Functions with Pulumi

Learn how to deploy a 2nd-gen Cloud Function with Pulumi and TypeScript, including packaging source code through Cloud Storage and public invocation IAM

Deploying GCP Cloud Functions with Pulumi

This guide deploys a Cloud Function with Pulumi using the 2nd generation runtime (cloudfunctionsv2) - 1st gen is Google’s older, more limited runtime and shouldn’t be used for anything new. Gen 2 functions run on Cloud Run under the hood, which shapes how source code and IAM work here.

Packaging Source Through Cloud Storage

Unlike Lambda’s pulumi.asset.AssetArchive (which uploads a deployment package directly as part of the resource), Cloud Functions Gen 2 requires the source archive to already exist in a Cloud Storage bucket that the function then references:

// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const config = new pulumi.Config();
const region = config.get("region") ?? "us-central1";

const sourceBucket = new gcp.storage.Bucket("func-source", {
    name: "my-func-source-bucket-001",
    location: "US",
    uniformBucketLevelAccess: true,
});

const sourceArchive = new gcp.storage.BucketObject("processor-source", {
    name: "processor-source.zip",
    bucket: sourceBucket.name,
    source: new pulumi.asset.FileArchive("./function-source"),
});

pulumi.asset.FileArchive still does the local zipping-and-packaging work here, same as in the Lambda guide - the difference is Cloud Functions needs that archive uploaded to a bucket object first, rather than accepting it as an inline deployment package.

The Function

const fn = new gcp.cloudfunctionsv2.Function("processor", {
    name: "processor",
    location: region,
    buildConfig: {
        runtime: "nodejs20",
        entryPoint: "handler",
        source: {
            storageSource: {
                bucket: sourceBucket.name,
                object: sourceArchive.name,
            },
        },
    },
    serviceConfig: {
        maxInstanceCount: 5,
        availableMemory: "256M",
        timeoutSeconds: 60,
    },
});

buildConfig controls how the function is built (runtime, entry point, source location); serviceConfig controls how it runs (scaling limits, memory, timeout) - this split exists because Gen 2 functions are actually Cloud Run services under the hood, built once and then run like any other Cloud Run deployment.

Allowing Invocation

Because Gen 2 functions are Cloud Run services, granting invoke permission means an IAM binding on the underlying Cloud Run service, not a function-specific permission resource:

const invoker = new gcp.cloudrunv2.ServiceIamMember("processor-invoker", {
    name: fn.name,
    location: region,
    role: "roles/run.invoker",
    member: "allUsers",
});

export const functionUri = fn.serviceConfig.apply(sc => sc?.uri);

member: "allUsers" makes the function publicly invokable with no authentication - appropriate for a public HTTP API, wrong for anything internal. Scope member to a specific service account (serviceAccount:name@project.iam.gserviceaccount.com) instead for function-to-function or scheduled-job invocation that shouldn’t be open to the internet.

Best Practices

  1. Always use cloudfunctionsv2, not the 1st-gen cloudfunctions.Function - Gen 1 is the older, more limited runtime; there’s no reason to start a new function on it.
  2. Scope serviceConfig.maxInstanceCount - Cloud Functions scales out per-request like Lambda, and an unbounded function under a traffic spike (or a bug causing runaway invocations) can generate very large bills without this cap.
  3. Grant roles/run.invoker to a specific service account instead of allUsers for anything that isn’t meant to be a public HTTP endpoint - remember this is a real Cloud Run IAM binding, not a lightweight function-only permission.

Conclusion

Compare this to the Lambda guide: both are pay-per-invocation serverless compute, but Cloud Functions Gen 2’s “it’s actually Cloud Run” architecture means source packaging goes through Cloud Storage and invocation permissions go through Cloud Run IAM, rather than Lambda’s more self-contained AssetArchive + lambda.Permission model.

For more Pulumi topics, check out: