Create and Configure GKE Kubernetes Clusters

The gcp:container/cluster:Cluster resource, part of the Pulumi GCP provider, provisions a GKE cluster: its control plane, networking mode, and default node configuration. This guide focuses on three capabilities: node pool management patterns, Autopilot vs Standard cluster modes, and service account integration.

GKE clusters require IAM service accounts for node identity and typically reference VPC networks and subnets. The examples are intentionally small. Combine them with your own networking, security policies, and workload configurations.

Create a cluster with separately managed node pools

Most production deployments separate the cluster resource from node pool resources, allowing teams to add, remove, or modify node pools without recreating the entire cluster.

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

const _default = new gcp.serviceaccount.Account("default", {
    accountId: "service-account-id",
    displayName: "Service Account",
});
const primary = new gcp.container.Cluster("primary", {
    name: "my-gke-cluster",
    location: "us-central1",
    removeDefaultNodePool: true,
    initialNodeCount: 1,
});
const primaryPreemptibleNodes = new gcp.container.NodePool("primary_preemptible_nodes", {
    name: "my-node-pool",
    location: "us-central1",
    cluster: primary.name,
    nodeCount: 1,
    nodeConfig: {
        preemptible: true,
        machineType: "e2-medium",
        serviceAccount: _default.email,
        oauthScopes: ["https://www.googleapis.com/auth/cloud-platform"],
    },
});
import pulumi
import pulumi_gcp as gcp

default = gcp.serviceaccount.Account("default",
    account_id="service-account-id",
    display_name="Service Account")
primary = gcp.container.Cluster("primary",
    name="my-gke-cluster",
    location="us-central1",
    remove_default_node_pool=True,
    initial_node_count=1)
primary_preemptible_nodes = gcp.container.NodePool("primary_preemptible_nodes",
    name="my-node-pool",
    location="us-central1",
    cluster=primary.name,
    node_count=1,
    node_config={
        "preemptible": True,
        "machine_type": "e2-medium",
        "service_account": default.email,
        "oauth_scopes": ["https://www.googleapis.com/auth/cloud-platform"],
    })
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/container"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/serviceaccount"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_default, err := serviceaccount.NewAccount(ctx, "default", &serviceaccount.AccountArgs{
			AccountId:   pulumi.String("service-account-id"),
			DisplayName: pulumi.String("Service Account"),
		})
		if err != nil {
			return err
		}
		primary, err := container.NewCluster(ctx, "primary", &container.ClusterArgs{
			Name:                  pulumi.String("my-gke-cluster"),
			Location:              pulumi.String("us-central1"),
			RemoveDefaultNodePool: pulumi.Bool(true),
			InitialNodeCount:      pulumi.Int(1),
		})
		if err != nil {
			return err
		}
		_, err = container.NewNodePool(ctx, "primary_preemptible_nodes", &container.NodePoolArgs{
			Name:      pulumi.String("my-node-pool"),
			Location:  pulumi.String("us-central1"),
			Cluster:   primary.Name,
			NodeCount: pulumi.Int(1),
			NodeConfig: &container.NodePoolNodeConfigArgs{
				Preemptible:    pulumi.Bool(true),
				MachineType:    pulumi.String("e2-medium"),
				ServiceAccount: _default.Email,
				OauthScopes: pulumi.StringArray{
					pulumi.String("https://www.googleapis.com/auth/cloud-platform"),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.ServiceAccount.Account("default", new()
    {
        AccountId = "service-account-id",
        DisplayName = "Service Account",
    });

    var primary = new Gcp.Container.Cluster("primary", new()
    {
        Name = "my-gke-cluster",
        Location = "us-central1",
        RemoveDefaultNodePool = true,
        InitialNodeCount = 1,
    });

    var primaryPreemptibleNodes = new Gcp.Container.NodePool("primary_preemptible_nodes", new()
    {
        Name = "my-node-pool",
        Location = "us-central1",
        Cluster = primary.Name,
        NodeCount = 1,
        NodeConfig = new Gcp.Container.Inputs.NodePoolNodeConfigArgs
        {
            Preemptible = true,
            MachineType = "e2-medium",
            ServiceAccount = @default.Email,
            OauthScopes = new[]
            {
                "https://www.googleapis.com/auth/cloud-platform",
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.serviceaccount.Account;
import com.pulumi.gcp.serviceaccount.AccountArgs;
import com.pulumi.gcp.container.Cluster;
import com.pulumi.gcp.container.ClusterArgs;
import com.pulumi.gcp.container.NodePool;
import com.pulumi.gcp.container.NodePoolArgs;
import com.pulumi.gcp.container.inputs.NodePoolNodeConfigArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new Account("default", AccountArgs.builder()
            .accountId("service-account-id")
            .displayName("Service Account")
            .build());

        var primary = new Cluster("primary", ClusterArgs.builder()
            .name("my-gke-cluster")
            .location("us-central1")
            .removeDefaultNodePool(true)
            .initialNodeCount(1)
            .build());

        var primaryPreemptibleNodes = new NodePool("primaryPreemptibleNodes", NodePoolArgs.builder()
            .name("my-node-pool")
            .location("us-central1")
            .cluster(primary.name())
            .nodeCount(1)
            .nodeConfig(NodePoolNodeConfigArgs.builder()
                .preemptible(true)
                .machineType("e2-medium")
                .serviceAccount(default_.email())
                .oauthScopes("https://www.googleapis.com/auth/cloud-platform")
                .build())
            .build());

    }
}
resources:
  default:
    type: gcp:serviceaccount:Account
    properties:
      accountId: service-account-id
      displayName: Service Account
  primary:
    type: gcp:container:Cluster
    properties:
      name: my-gke-cluster
      location: us-central1
      removeDefaultNodePool: true
      initialNodeCount: 1
  primaryPreemptibleNodes:
    type: gcp:container:NodePool
    name: primary_preemptible_nodes
    properties:
      name: my-node-pool
      location: us-central1
      cluster: ${primary.name}
      nodeCount: 1
      nodeConfig:
        preemptible: true
        machineType: e2-medium
        serviceAccount: ${default.email}
        oauthScopes:
          - https://www.googleapis.com/auth/cloud-platform

When removeDefaultNodePool is true, GKE deletes the default node pool immediately after cluster creation. You must set initialNodeCount to at least 1 to satisfy GKE’s creation requirements, even though that pool is removed. The separate gcp.container.NodePool resource then creates your actual node pools, which you can modify independently. The nodeConfig block on the node pool specifies machine type, service account, and OAuth scopes for node identity.

Configure the default node pool inline

For simpler deployments or testing, you can configure nodes directly on the cluster resource using the default node pool.

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

const _default = new gcp.serviceaccount.Account("default", {
    accountId: "service-account-id",
    displayName: "Service Account",
});
const primary = new gcp.container.Cluster("primary", {
    name: "marcellus-wallace",
    location: "us-central1-a",
    initialNodeCount: 3,
    nodeConfig: {
        serviceAccount: _default.email,
        oauthScopes: ["https://www.googleapis.com/auth/cloud-platform"],
        labels: {
            foo: "bar",
        },
        tags: [
            "foo",
            "bar",
        ],
    },
});
import pulumi
import pulumi_gcp as gcp

default = gcp.serviceaccount.Account("default",
    account_id="service-account-id",
    display_name="Service Account")
primary = gcp.container.Cluster("primary",
    name="marcellus-wallace",
    location="us-central1-a",
    initial_node_count=3,
    node_config={
        "service_account": default.email,
        "oauth_scopes": ["https://www.googleapis.com/auth/cloud-platform"],
        "labels": {
            "foo": "bar",
        },
        "tags": [
            "foo",
            "bar",
        ],
    })
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/container"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/serviceaccount"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_default, err := serviceaccount.NewAccount(ctx, "default", &serviceaccount.AccountArgs{
			AccountId:   pulumi.String("service-account-id"),
			DisplayName: pulumi.String("Service Account"),
		})
		if err != nil {
			return err
		}
		_, err = container.NewCluster(ctx, "primary", &container.ClusterArgs{
			Name:             pulumi.String("marcellus-wallace"),
			Location:         pulumi.String("us-central1-a"),
			InitialNodeCount: pulumi.Int(3),
			NodeConfig: &container.ClusterNodeConfigArgs{
				ServiceAccount: _default.Email,
				OauthScopes: pulumi.StringArray{
					pulumi.String("https://www.googleapis.com/auth/cloud-platform"),
				},
				Labels: pulumi.StringMap{
					"foo": pulumi.String("bar"),
				},
				Tags: pulumi.StringArray{
					pulumi.String("foo"),
					pulumi.String("bar"),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.ServiceAccount.Account("default", new()
    {
        AccountId = "service-account-id",
        DisplayName = "Service Account",
    });

    var primary = new Gcp.Container.Cluster("primary", new()
    {
        Name = "marcellus-wallace",
        Location = "us-central1-a",
        InitialNodeCount = 3,
        NodeConfig = new Gcp.Container.Inputs.ClusterNodeConfigArgs
        {
            ServiceAccount = @default.Email,
            OauthScopes = new[]
            {
                "https://www.googleapis.com/auth/cloud-platform",
            },
            Labels = 
            {
                { "foo", "bar" },
            },
            Tags = new[]
            {
                "foo",
                "bar",
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.serviceaccount.Account;
import com.pulumi.gcp.serviceaccount.AccountArgs;
import com.pulumi.gcp.container.Cluster;
import com.pulumi.gcp.container.ClusterArgs;
import com.pulumi.gcp.container.inputs.ClusterNodeConfigArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new Account("default", AccountArgs.builder()
            .accountId("service-account-id")
            .displayName("Service Account")
            .build());

        var primary = new Cluster("primary", ClusterArgs.builder()
            .name("marcellus-wallace")
            .location("us-central1-a")
            .initialNodeCount(3)
            .nodeConfig(ClusterNodeConfigArgs.builder()
                .serviceAccount(default_.email())
                .oauthScopes("https://www.googleapis.com/auth/cloud-platform")
                .labels(Map.of("foo", "bar"))
                .tags(                
                    "foo",
                    "bar")
                .build())
            .build());

    }
}
resources:
  default:
    type: gcp:serviceaccount:Account
    properties:
      accountId: service-account-id
      displayName: Service Account
  primary:
    type: gcp:container:Cluster
    properties:
      name: marcellus-wallace
      location: us-central1-a
      initialNodeCount: 3
      nodeConfig:
        serviceAccount: ${default.email}
        oauthScopes:
          - https://www.googleapis.com/auth/cloud-platform
        labels:
          foo: bar
        tags:
          - foo
          - bar

The nodeConfig block defines node properties: service account for GCP API access, OAuth scopes for permissions, and labels/tags for organization. This approach is simpler but less flexible; changing node configuration requires recreating the cluster. For production workloads, use separately managed node pools instead.

Enable Autopilot for fully managed infrastructure

Autopilot mode shifts node management to Google, automatically provisioning and scaling infrastructure based on workload requirements.

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

const _default = new gcp.serviceaccount.Account("default", {
    accountId: "service-account-id",
    displayName: "Service Account",
});
const primary = new gcp.container.Cluster("primary", {
    name: "marcellus-wallace",
    location: "us-central1-a",
    enableAutopilot: true,
});
import pulumi
import pulumi_gcp as gcp

default = gcp.serviceaccount.Account("default",
    account_id="service-account-id",
    display_name="Service Account")
primary = gcp.container.Cluster("primary",
    name="marcellus-wallace",
    location="us-central1-a",
    enable_autopilot=True)
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/container"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/serviceaccount"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := serviceaccount.NewAccount(ctx, "default", &serviceaccount.AccountArgs{
			AccountId:   pulumi.String("service-account-id"),
			DisplayName: pulumi.String("Service Account"),
		})
		if err != nil {
			return err
		}
		_, err = container.NewCluster(ctx, "primary", &container.ClusterArgs{
			Name:            pulumi.String("marcellus-wallace"),
			Location:        pulumi.String("us-central1-a"),
			EnableAutopilot: pulumi.Bool(true),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.ServiceAccount.Account("default", new()
    {
        AccountId = "service-account-id",
        DisplayName = "Service Account",
    });

    var primary = new Gcp.Container.Cluster("primary", new()
    {
        Name = "marcellus-wallace",
        Location = "us-central1-a",
        EnableAutopilot = true,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.serviceaccount.Account;
import com.pulumi.gcp.serviceaccount.AccountArgs;
import com.pulumi.gcp.container.Cluster;
import com.pulumi.gcp.container.ClusterArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new Account("default", AccountArgs.builder()
            .accountId("service-account-id")
            .displayName("Service Account")
            .build());

        var primary = new Cluster("primary", ClusterArgs.builder()
            .name("marcellus-wallace")
            .location("us-central1-a")
            .enableAutopilot(true)
            .build());

    }
}
resources:
  default:
    type: gcp:serviceaccount:Account
    properties:
      accountId: service-account-id
      displayName: Service Account
  primary:
    type: gcp:container:Cluster
    properties:
      name: marcellus-wallace
      location: us-central1-a
      enableAutopilot: true

Setting enableAutopilot to true activates Autopilot mode, where GKE manages all node provisioning, scaling, and configuration. You don’t specify nodeConfig, initialNodeCount, or node pools; Google handles infrastructure based on your workload demands. Note that Autopilot restricts certain Standard GKE features; see the official documentation for feature availability.

Beyond these examples

These snippets focus on specific cluster-level features: node pool management strategies, Autopilot vs Standard cluster modes, and service account integration. They’re intentionally minimal rather than full Kubernetes deployments.

The examples may reference pre-existing infrastructure such as IAM service accounts for node identity, and VPC networks and subnets (examples use defaults). They focus on configuring the cluster rather than provisioning everything around it.

To keep things focused, common cluster patterns are omitted, including:

  • VPC-native networking (ipAllocationPolicy)
  • Private clusters and master authorized networks
  • Release channels and version management
  • Workload Identity and security features
  • Monitoring, logging, and observability configuration
  • Add-ons (HTTP load balancing, network policy, etc.)

These omissions are intentional: the goal is to illustrate how each cluster feature is wired, not provide drop-in Kubernetes platforms. See the GKE Cluster resource reference for all available configuration options.

Let's create and Configure GKE Kubernetes Clusters

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Cluster Lifecycle & Deletion
Why can't I destroy my cluster in provider version 5.0.0+?
Starting in provider version 5.0.0+, you must explicitly set deletionProtection = false and run pulumi up to write the field to state before you can destroy a cluster.
What happens to Kubernetes Alpha clusters?
Clusters with enableKubernetesAlpha = true cannot be upgraded and will be automatically deleted after 30 days. Only use this for temporary testing, not production workloads.
Are my cluster credentials stored securely?
All arguments and attributes, including certificate outputs, are stored in the raw state as plaintext. Use Pulumi’s secrets management features to encrypt sensitive values.
Node Pool Management
How do I add or remove node pools without recreating my cluster?
Create node pools as separate gcp.container.NodePool resources instead of using the inline nodePools property. Set removeDefaultNodePool = true and initialNodeCount = 1 on the cluster. Node pools defined inline cannot be changed after cluster creation without deleting and recreating the entire cluster.
What's the recommended way to configure node pools?
Use separate gcp.container.NodePool resources rather than the inline nodePools property. This allows you to add and remove node pools without recreating the cluster.
Versioning & Upgrades
What's the difference between minMasterVersion and masterVersion?
minMasterVersion is the minimum version you specify, but GKE auto-updates the master beyond this. Use the read-only masterVersion output to get the actual current master version.
Why am I seeing unexpected diffs with nodeVersion?
Fuzzy versions like ‘1.27’ cause spurious diffs. Use explicit versions like ‘1.27.3-gke.100’, or use the gcp.container.getEngineVersions datasource with versionPrefix to approximate fuzzy versions.
How do I stop managing my cluster's release channel?
Removing the releaseChannel field stops provider management but doesn’t unenroll the cluster. To unenroll, set the channel to "UNSPECIFIED" instead.
Why can't I find the right version with gcp.container.getEngineVersions?
When using the datasource with a regional cluster, you must provide a location parameter. Regions can have different supported versions than their zones, and not all zones in a region support the same version.
Cluster Configuration
What features are unavailable in Autopilot mode?
When enableAutopilot = true, certain Standard GKE features are not available. See the official GKE documentation for a comparison of available features between Autopilot and Standard modes.
Do I need to restart anything after enabling FQDN Network Policy?
Yes, when enabling enableFqdnNetworkPolicy on existing Standard clusters, you must manually restart the GKE Dataplane V2 anetd DaemonSet.
Should I use a region or zone for my cluster location?
Regional clusters are preferred over multi-zonal clusters. In regional clusters, master nodes are present in multiple zones, providing higher availability than zonal clusters where the master is only in a single zone.

Using a different cloud?

Explore containers guides for other cloud providers: