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:
- Pulumi CLI: You run
pulumi up. - Provider Interaction: The Pulumi CLI invokes the
@pulumi/gcpprovider. - State Comparison: The provider asks GCP for the current state of resources matching the names
my-bucketandmy-functionin your configured project and region. - Resource Creation/Update:
- If
my-bucketdoesn’t exist, the provider makes aPOSTrequest to the GCP Storage API to create it. - Once the bucket is confirmed, Pulumi then moves to the function. It makes a
POSTrequest to the Cloud Functions API. Crucially, it waits for the bucket creation to complete before sending this request. ThesourceArchiveObjectpart is a bit of a simplification here; in a real-world scenario, you’d likely upload asource.zipfile to the bucket first using Pulumi’s asset management, and then reference that specific object.
- If
- Output: Finally, Pulumi retrieves the
httpsTriggerUrlfor 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.