You can deploy almost any Google Cloud resource using Pulumi, but the real magic is how it lets you manage them like code, even when those resources have complex interdependencies.

Let’s see it in action. Imagine we want to deploy a Google Cloud Storage bucket and then a Cloud Function that can read from it.

First, the Pulumi.yaml to define our project:

name: gcp-storage-function
runtime: nodejs
description: A basic GCP Storage and Function example

Next, the index.ts where we define our infrastructure:

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

// Create a Google Cloud Storage bucket
const bucket = new gcp.storage.Bucket("my-bucket", {
    location: "US",
    uniformBucketLevelAccess: true,
});

// Create a Cloud Function that reads from the bucket
const myFunction = new gcp.cloudfunctions.Function("my-function", {
    runtime: "nodejs18", // Specify the runtime
    entryPoint: "handler", // The name of the function in your code
    region: "us-central1", // Region for the function
    sourceArchiveBucket: bucket.name, // Link to the bucket we created
    sourceArchiveObject: bucket.name.apply(name => `${name}-source.zip`), // Placeholder for the zip file
    triggerHttp: true, // Make it an HTTP-triggered function
});

// Export the URL of the function
export const functionUrl = myFunction.httpsTriggerUrl;

This code declares a bucket named my-bucket and a Cloud Function my-function. Notice how sourceArchiveBucket directly references the bucket.name we just created. Pulumi understands this dependency and will ensure the bucket is provisioned before it attempts to configure the function.

When you run pulumi up, Pulumi communicates with the GCP API. It generates a desired state based on your code and compares it to the current state of your GCP project. If a resource doesn’t exist, or if its configuration differs from what’s in your code, Pulumi creates or updates it.

Here’s what happens under the hood for this example:

  1. Pulumi CLI: You run pulumi up.
  2. Provider Interaction: The Pulumi CLI invokes the @pulumi/gcp provider.
  3. State Comparison: The provider asks GCP for the current state of resources matching the names my-bucket and my-function in your configured project and region.
  4. Resource Creation/Update:
    • If my-bucket doesn’t exist, the provider makes a POST request to the GCP Storage API to create it.
    • Once the bucket is confirmed, Pulumi then moves to the function. It makes a POST request to the Cloud Functions API. Crucially, it waits for the bucket creation to complete before sending this request. The sourceArchiveObject part is a bit of a simplification here; in a real-world scenario, you’d likely upload a source.zip file to the bucket first using Pulumi’s asset management, and then reference that specific object.
  5. Output: Finally, Pulumi retrieves the httpsTriggerUrl for the function and displays it as an output.

The whole mental model is about desired state. You declare what you want, and Pulumi figures out how to get there, managing the order of operations and potential retries.

The most surprising thing is how Pulumi handles secrets. When you define a resource that might expose sensitive information (like a database password or a private key), Pulumi automatically encrypts it in its state file. You can still access the value in your code, but it’s never stored in plaintext in the state, which is critical for security.

If you wanted to make the bucket publicly readable, you’d add a storage.BucketIAMMember resource.

The next concept you’ll likely grapple with is managing multiple environments (dev, staging, prod) with Pulumi.

Want structured learning?

Take the full Pulumi course →