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 initial node configuration. This guide focuses on three capabilities: node pool management patterns, Autopilot vs Standard cluster modes, and service account configuration.

GKE clusters require a GCP project, 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 identity configuration.

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 pools without recreating the 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

The removeDefaultNodePool property deletes the default node pool immediately after cluster creation. Setting initialNodeCount to 1 satisfies GKE’s requirement for an initial node count, but the pool is removed before any workloads run. The separate gcp.container.NodePool resource defines the actual node configuration, including the serviceAccount for node identity and oauthScopes for API access. This pattern lets you manage node pools independently: you can add new pools, change machine types, or adjust node counts without touching the cluster resource.

Configure the default node pool inline

When node pool requirements are stable and won’t change independently, you can configure the default pool directly in the cluster resource.

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 properties for nodes in the default pool. The serviceAccount grants the node identity, while oauthScopes determines which Google Cloud APIs nodes can access. The labels and tags properties add metadata for organization and network policy enforcement. This approach is simpler than managing separate node pool resources, but changing node configuration requires recreating the entire cluster.

Enable Autopilot for managed infrastructure

Autopilot clusters delegate node management to GKE, which automatically provisions and scales 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. GKE manages all node provisioning, scaling, and configuration automatically. You don’t specify nodeConfig, nodePools, or initialNodeCount because GKE handles those decisions. Autopilot reduces operational overhead but disables certain Standard GKE features like custom node pools and SSH access to nodes.

Beyond these examples

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

The examples may reference pre-existing infrastructure such as Google Cloud service accounts, 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:

  • Private cluster configuration (privateClusterConfig)
  • Workload Identity setup (workloadIdentityConfig)
  • Release channels and version management (releaseChannel, minMasterVersion)
  • IP allocation and networking modes (ipAllocationPolicy, networkingMode)
  • Monitoring and logging configuration (monitoringConfig, loggingConfig)
  • Security features (binaryAuthorization, securityPostureConfig, enableShieldedNodes)

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
How do I destroy a GKE cluster in provider v5.0.0+?
Starting with provider version 5.0.0, you must explicitly set deletionProtection to false and run pulumi up to write this to state before you can destroy the cluster.
Why does my Kubernetes Alpha cluster get automatically deleted?
Clusters with enableKubernetesAlpha set to true cannot be upgraded and are automatically deleted after 30 days. This setting is immutable, so only enable it for temporary testing clusters.
Node Pool Management
Should I define node pools inside my cluster resource or as separate resources?
Use separate gcp.container.NodePool resources. Node pools defined inline using the nodePools property cannot be changed, added, or removed after cluster creation without deleting and recreating the entire cluster.
How do I set up a cluster to use separate node pool resources?
Set removeDefaultNodePool to true and initialNodeCount to at least 1 on the cluster resource, then create your node pools as separate gcp.container.NodePool resources.
Versioning & Upgrades
How should I manage cluster and node versions to avoid spurious diffs?
Use explicit versions instead of fuzzy versions for nodeVersion. The gcp.container.getEngineVersions data source with versionPrefix can help approximate fuzzy versions. Note that minMasterVersion sets the minimum (GKE auto-updates), while the read-only masterVersion shows the current version.
Why do regional clusters have different available versions than zones?
A region can have a different set of supported versions than its corresponding zones, and not all zones in a region are guaranteed to support the same version. When using gcp.container.getEngineVersions with a regional cluster, ensure you provide a location to the datasource.
How do I unenroll my cluster from a release channel?
Set releaseChannel to "UNSPECIFIED" rather than removing the field from your config. Removing the field stops the provider from managing the release channel but doesn’t actually unenroll the cluster.
Cluster Types & Configuration
What's the difference between regional and zonal GKE clusters?
Zonal clusters have a single master in one zone, while regional clusters have multiple masters spread across zones in a region. Regional clusters provide higher availability and are preferred. The location property is immutable, so you cannot convert between types after creation.
What Standard GKE features are unavailable in Autopilot mode?
When enableAutopilot is set to true, certain Standard GKE features become unavailable. This setting is immutable after cluster creation. Refer to the official GKE Autopilot documentation for a detailed feature comparison.
What networking mode should I use for new clusters?
Newly created clusters default to VPC_NATIVE, which enables IP aliasing. The networkingMode property is immutable. VPC_NATIVE is recommended over ROUTES for most use cases.
Security & Operations
Are my cluster credentials stored securely in Pulumi state?
All arguments and attributes, including certificate outputs, are stored in the raw state as plaintext. Consider using remote state backends with encryption when handling sensitive cluster data.
What do I need to do after enabling FQDN Network Policy on an existing cluster?
For existing Standard clusters, you must manually restart the GKE Dataplane V2 anetd DaemonSet after enabling enableFqdnNetworkPolicy. New clusters don’t require this step.

Using a different cloud?

Explore containers guides for other cloud providers: