Provisioning Google Kubernetes Engine (GKE) with Pulumi

Learn how to provision a VPC-native GKE cluster with a separately managed node pool using Pulumi and TypeScript

Provisioning Google Kubernetes Engine (GKE) with Pulumi

This guide provisions a GKE cluster with Pulumi, building on the VPC guide - a VPC-native cluster with its default node pool removed immediately, and a separately managed node pool in its place.

Cluster and Node Pool as Separate Resources

GKE’s own best practice - and the pattern the Terraform GKE guide on this site follows too - is to remove the cluster’s automatically-created default node pool and manage node pools as their own resources instead, since the default pool can’t be resized or configured the way an explicitly-managed one can:

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

const cluster = new gcp.container.Cluster("main", {
    name: "app-gke-cluster",
    location: region,
    removeDefaultNodePool: true,
    initialNodeCount: 1,
    network: network.id,
    subnetwork: privateSubnet.id,
    networkingMode: "VPC_NATIVE",
    ipAllocationPolicy: {},
    deletionProtection: false,
});

const nodePool = new gcp.container.NodePool("default", {
    name: "default-pool",
    cluster: cluster.name,
    location: region,
    nodeCount: 3,
    nodeConfig: {
        machineType: "e2-medium",
        oauthScopes: ["https://www.googleapis.com/auth/cloud-platform"],
    },
});

removeDefaultNodePool: true combined with initialNodeCount: 1 is the idiomatic way to get a clean cluster with no nodes from the default pool: GKE requires initialNodeCount to create the cluster in the first place, then Pulumi immediately deletes that default pool once the cluster exists, leaving only the nodePool resource defined explicitly below it.

networkingMode: "VPC_NATIVE" (with an empty ipAllocationPolicy: {}, which tells GKE to auto-allocate the secondary IP ranges pods and services need) is the current default and recommended mode - it gives pods real routable VPC IPs rather than the older “routes-based” networking mode’s overlay network. deletionProtection: false is set here only to make pulumi destroy work cleanly for this guide’s example; leave it at its default (true) for any real cluster, the same reasoning as deletion_protection on the Terraform side.

Connecting to the Cluster

Unlike AKS (which has a direct API returning a ready-to-use kubeconfig) or @pulumi/eks (which builds one for you as an Output), GKE’s standard connection path runs through the gcloud CLI rather than a Pulumi-generated kubeconfig blob:

export const clusterName = cluster.name;
export const clusterLocation = cluster.location;
gcloud container clusters get-credentials $(pulumi stack output clusterName) \
  --region $(pulumi stack output clusterLocation)
kubectl get nodes

get-credentials writes an entry into your local kubeconfig that shells out to gcloud for a short-lived access token on every kubectl call, rather than embedding a long-lived credential the way a raw kubeconfig blob would - this is Google’s own recommended pattern, not a limitation of using Pulumi to provision the cluster.

Best Practices

  1. Always remove the default node pool and manage pools explicitly, as above - it’s the only way to get predictable machine types, autoscaling, and node counts.
  2. Use VPC_NATIVE networking for any new cluster - it’s the current default for good reason (real VPC-routable pod IPs, compatibility with VPC-level firewall rules and Cloud NAT).
  3. Leave deletionProtection at its default (true) in any stack that isn’t purely a throwaway example - it exists specifically to make an accidental pulumi destroy fail loudly instead of silently deleting a production cluster.

Conclusion

Compare this to the EKS guide and its @pulumi/eks component: GKE’s raw container.Cluster/container.NodePool resources are closer in spirit to that guide’s “existing VPC” example than to the one-call eks.Cluster quickstart - there’s no equivalent all-in-one GKE component package, so explicitly separating cluster and node pool, as here, is the normal pattern rather than an advanced option.

For more Pulumi topics, check out: