Register GCP GKE Hub Cluster Memberships

The gcp:gkehub/membership:Membership resource, part of the Pulumi GCP provider, registers Kubernetes clusters with GKE Hub for centralized management and policy enforcement. This guide focuses on three capabilities: GKE cluster registration, regional metadata placement, and workload identity configuration.

Memberships reference existing GKE clusters and may require workload identity configuration on those clusters. The examples are intentionally small. Combine them with your own cluster infrastructure and fleet policies.

Register a GKE cluster with labels

Teams managing multiple clusters use GKE Hub to register clusters for centralized visibility and policy enforcement. Labels organize clusters by environment, team, or purpose.

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

const primary = new gcp.container.Cluster("primary", {
    name: "basic-cluster",
    location: "us-central1-a",
    initialNodeCount: 1,
    deletionProtection: true,
    network: "default",
    subnetwork: "default",
});
const membership = new gcp.gkehub.Membership("membership", {
    membershipId: "basic",
    endpoint: {
        gkeCluster: {
            resourceLink: pulumi.interpolate`//container.googleapis.com/${primary.id}`,
        },
    },
    labels: {
        env: "test",
    },
});
import pulumi
import pulumi_gcp as gcp

primary = gcp.container.Cluster("primary",
    name="basic-cluster",
    location="us-central1-a",
    initial_node_count=1,
    deletion_protection=True,
    network="default",
    subnetwork="default")
membership = gcp.gkehub.Membership("membership",
    membership_id="basic",
    endpoint={
        "gke_cluster": {
            "resource_link": primary.id.apply(lambda id: f"//container.googleapis.com/{id}"),
        },
    },
    labels={
        "env": "test",
    })
package main

import (
	"fmt"

	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/container"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/gkehub"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		primary, err := container.NewCluster(ctx, "primary", &container.ClusterArgs{
			Name:               pulumi.String("basic-cluster"),
			Location:           pulumi.String("us-central1-a"),
			InitialNodeCount:   pulumi.Int(1),
			DeletionProtection: pulumi.Bool(true),
			Network:            pulumi.String("default"),
			Subnetwork:         pulumi.String("default"),
		})
		if err != nil {
			return err
		}
		_, err = gkehub.NewMembership(ctx, "membership", &gkehub.MembershipArgs{
			MembershipId: pulumi.String("basic"),
			Endpoint: &gkehub.MembershipEndpointArgs{
				GkeCluster: &gkehub.MembershipEndpointGkeClusterArgs{
					ResourceLink: primary.ID().ApplyT(func(id string) (string, error) {
						return fmt.Sprintf("//container.googleapis.com/%v", id), nil
					}).(pulumi.StringOutput),
				},
			},
			Labels: pulumi.StringMap{
				"env": pulumi.String("test"),
			},
		})
		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 primary = new Gcp.Container.Cluster("primary", new()
    {
        Name = "basic-cluster",
        Location = "us-central1-a",
        InitialNodeCount = 1,
        DeletionProtection = true,
        Network = "default",
        Subnetwork = "default",
    });

    var membership = new Gcp.GkeHub.Membership("membership", new()
    {
        MembershipId = "basic",
        Endpoint = new Gcp.GkeHub.Inputs.MembershipEndpointArgs
        {
            GkeCluster = new Gcp.GkeHub.Inputs.MembershipEndpointGkeClusterArgs
            {
                ResourceLink = primary.Id.Apply(id => $"//container.googleapis.com/{id}"),
            },
        },
        Labels = 
        {
            { "env", "test" },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.container.Cluster;
import com.pulumi.gcp.container.ClusterArgs;
import com.pulumi.gcp.gkehub.Membership;
import com.pulumi.gcp.gkehub.MembershipArgs;
import com.pulumi.gcp.gkehub.inputs.MembershipEndpointArgs;
import com.pulumi.gcp.gkehub.inputs.MembershipEndpointGkeClusterArgs;
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 primary = new Cluster("primary", ClusterArgs.builder()
            .name("basic-cluster")
            .location("us-central1-a")
            .initialNodeCount(1)
            .deletionProtection(true)
            .network("default")
            .subnetwork("default")
            .build());

        var membership = new Membership("membership", MembershipArgs.builder()
            .membershipId("basic")
            .endpoint(MembershipEndpointArgs.builder()
                .gkeCluster(MembershipEndpointGkeClusterArgs.builder()
                    .resourceLink(primary.id().applyValue(_id -> String.format("//container.googleapis.com/%s", _id)))
                    .build())
                .build())
            .labels(Map.of("env", "test"))
            .build());

    }
}
resources:
  primary:
    type: gcp:container:Cluster
    properties:
      name: basic-cluster
      location: us-central1-a
      initialNodeCount: 1
      deletionProtection: true
      network: default
      subnetwork: default
  membership:
    type: gcp:gkehub:Membership
    properties:
      membershipId: basic
      endpoint:
        gkeCluster:
          resourceLink: //container.googleapis.com/${primary.id}
      labels:
        env: test

The membershipId provides a stable identifier for the cluster within the fleet. The endpoint block links to the GKE cluster via its resource path, establishing the connection between Hub and the cluster API server. Labels add organizational metadata that fleet policies can reference.

Register a cluster in a specific region

Some organizations need to control where membership metadata is stored for compliance or latency reasons.

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

const primary = new gcp.container.Cluster("primary", {
    name: "basic-cluster",
    location: "us-central1-a",
    initialNodeCount: 1,
    deletionProtection: false,
    network: "default",
    subnetwork: "default",
});
const membership = new gcp.gkehub.Membership("membership", {
    membershipId: "basic",
    location: "us-west1",
    endpoint: {
        gkeCluster: {
            resourceLink: pulumi.interpolate`//container.googleapis.com/${primary.id}`,
        },
    },
});
import pulumi
import pulumi_gcp as gcp

primary = gcp.container.Cluster("primary",
    name="basic-cluster",
    location="us-central1-a",
    initial_node_count=1,
    deletion_protection=False,
    network="default",
    subnetwork="default")
membership = gcp.gkehub.Membership("membership",
    membership_id="basic",
    location="us-west1",
    endpoint={
        "gke_cluster": {
            "resource_link": primary.id.apply(lambda id: f"//container.googleapis.com/{id}"),
        },
    })
package main

import (
	"fmt"

	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/container"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/gkehub"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		primary, err := container.NewCluster(ctx, "primary", &container.ClusterArgs{
			Name:               pulumi.String("basic-cluster"),
			Location:           pulumi.String("us-central1-a"),
			InitialNodeCount:   pulumi.Int(1),
			DeletionProtection: pulumi.Bool(false),
			Network:            pulumi.String("default"),
			Subnetwork:         pulumi.String("default"),
		})
		if err != nil {
			return err
		}
		_, err = gkehub.NewMembership(ctx, "membership", &gkehub.MembershipArgs{
			MembershipId: pulumi.String("basic"),
			Location:     pulumi.String("us-west1"),
			Endpoint: &gkehub.MembershipEndpointArgs{
				GkeCluster: &gkehub.MembershipEndpointGkeClusterArgs{
					ResourceLink: primary.ID().ApplyT(func(id string) (string, error) {
						return fmt.Sprintf("//container.googleapis.com/%v", id), nil
					}).(pulumi.StringOutput),
				},
			},
		})
		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 primary = new Gcp.Container.Cluster("primary", new()
    {
        Name = "basic-cluster",
        Location = "us-central1-a",
        InitialNodeCount = 1,
        DeletionProtection = false,
        Network = "default",
        Subnetwork = "default",
    });

    var membership = new Gcp.GkeHub.Membership("membership", new()
    {
        MembershipId = "basic",
        Location = "us-west1",
        Endpoint = new Gcp.GkeHub.Inputs.MembershipEndpointArgs
        {
            GkeCluster = new Gcp.GkeHub.Inputs.MembershipEndpointGkeClusterArgs
            {
                ResourceLink = primary.Id.Apply(id => $"//container.googleapis.com/{id}"),
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.container.Cluster;
import com.pulumi.gcp.container.ClusterArgs;
import com.pulumi.gcp.gkehub.Membership;
import com.pulumi.gcp.gkehub.MembershipArgs;
import com.pulumi.gcp.gkehub.inputs.MembershipEndpointArgs;
import com.pulumi.gcp.gkehub.inputs.MembershipEndpointGkeClusterArgs;
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 primary = new Cluster("primary", ClusterArgs.builder()
            .name("basic-cluster")
            .location("us-central1-a")
            .initialNodeCount(1)
            .deletionProtection(false)
            .network("default")
            .subnetwork("default")
            .build());

        var membership = new Membership("membership", MembershipArgs.builder()
            .membershipId("basic")
            .location("us-west1")
            .endpoint(MembershipEndpointArgs.builder()
                .gkeCluster(MembershipEndpointGkeClusterArgs.builder()
                    .resourceLink(primary.id().applyValue(_id -> String.format("//container.googleapis.com/%s", _id)))
                    .build())
                .build())
            .build());

    }
}
resources:
  primary:
    type: gcp:container:Cluster
    properties:
      name: basic-cluster
      location: us-central1-a
      initialNodeCount: 1
      deletionProtection: false
      network: default
      subnetwork: default
  membership:
    type: gcp:gkehub:Membership
    properties:
      membershipId: basic
      location: us-west1
      endpoint:
        gkeCluster:
          resourceLink: //container.googleapis.com/${primary.id}

The location property places membership metadata in a specific GCP region rather than the default global location. This extends basic registration by giving you control over data residency. The endpoint configuration remains the same, linking to the cluster’s resource path.

Configure workload identity with custom issuer

Workload Identity allows Kubernetes service accounts to authenticate as Google service accounts. The authority block configures how Google recognizes identities from the cluster.

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

const primary = new gcp.container.Cluster("primary", {
    name: "basic-cluster",
    location: "us-central1-a",
    initialNodeCount: 1,
    workloadIdentityConfig: {
        workloadPool: "my-project-name.svc.id.goog",
    },
    deletionProtection: true,
    network: "default",
    subnetwork: "default",
});
const membership = new gcp.gkehub.Membership("membership", {
    membershipId: "basic",
    endpoint: {
        gkeCluster: {
            resourceLink: primary.id,
        },
    },
    authority: {
        issuer: pulumi.interpolate`https://container.googleapis.com/v1/${primary.id}`,
    },
});
import pulumi
import pulumi_gcp as gcp

primary = gcp.container.Cluster("primary",
    name="basic-cluster",
    location="us-central1-a",
    initial_node_count=1,
    workload_identity_config={
        "workload_pool": "my-project-name.svc.id.goog",
    },
    deletion_protection=True,
    network="default",
    subnetwork="default")
membership = gcp.gkehub.Membership("membership",
    membership_id="basic",
    endpoint={
        "gke_cluster": {
            "resource_link": primary.id,
        },
    },
    authority={
        "issuer": primary.id.apply(lambda id: f"https://container.googleapis.com/v1/{id}"),
    })
package main

import (
	"fmt"

	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/container"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/gkehub"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		primary, err := container.NewCluster(ctx, "primary", &container.ClusterArgs{
			Name:             pulumi.String("basic-cluster"),
			Location:         pulumi.String("us-central1-a"),
			InitialNodeCount: pulumi.Int(1),
			WorkloadIdentityConfig: &container.ClusterWorkloadIdentityConfigArgs{
				WorkloadPool: pulumi.String("my-project-name.svc.id.goog"),
			},
			DeletionProtection: pulumi.Bool(true),
			Network:            pulumi.String("default"),
			Subnetwork:         pulumi.String("default"),
		})
		if err != nil {
			return err
		}
		_, err = gkehub.NewMembership(ctx, "membership", &gkehub.MembershipArgs{
			MembershipId: pulumi.String("basic"),
			Endpoint: &gkehub.MembershipEndpointArgs{
				GkeCluster: &gkehub.MembershipEndpointGkeClusterArgs{
					ResourceLink: primary.ID(),
				},
			},
			Authority: &gkehub.MembershipAuthorityArgs{
				Issuer: primary.ID().ApplyT(func(id string) (string, error) {
					return fmt.Sprintf("https://container.googleapis.com/v1/%v", id), nil
				}).(pulumi.StringOutput),
			},
		})
		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 primary = new Gcp.Container.Cluster("primary", new()
    {
        Name = "basic-cluster",
        Location = "us-central1-a",
        InitialNodeCount = 1,
        WorkloadIdentityConfig = new Gcp.Container.Inputs.ClusterWorkloadIdentityConfigArgs
        {
            WorkloadPool = "my-project-name.svc.id.goog",
        },
        DeletionProtection = true,
        Network = "default",
        Subnetwork = "default",
    });

    var membership = new Gcp.GkeHub.Membership("membership", new()
    {
        MembershipId = "basic",
        Endpoint = new Gcp.GkeHub.Inputs.MembershipEndpointArgs
        {
            GkeCluster = new Gcp.GkeHub.Inputs.MembershipEndpointGkeClusterArgs
            {
                ResourceLink = primary.Id,
            },
        },
        Authority = new Gcp.GkeHub.Inputs.MembershipAuthorityArgs
        {
            Issuer = primary.Id.Apply(id => $"https://container.googleapis.com/v1/{id}"),
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.container.Cluster;
import com.pulumi.gcp.container.ClusterArgs;
import com.pulumi.gcp.container.inputs.ClusterWorkloadIdentityConfigArgs;
import com.pulumi.gcp.gkehub.Membership;
import com.pulumi.gcp.gkehub.MembershipArgs;
import com.pulumi.gcp.gkehub.inputs.MembershipEndpointArgs;
import com.pulumi.gcp.gkehub.inputs.MembershipEndpointGkeClusterArgs;
import com.pulumi.gcp.gkehub.inputs.MembershipAuthorityArgs;
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 primary = new Cluster("primary", ClusterArgs.builder()
            .name("basic-cluster")
            .location("us-central1-a")
            .initialNodeCount(1)
            .workloadIdentityConfig(ClusterWorkloadIdentityConfigArgs.builder()
                .workloadPool("my-project-name.svc.id.goog")
                .build())
            .deletionProtection(true)
            .network("default")
            .subnetwork("default")
            .build());

        var membership = new Membership("membership", MembershipArgs.builder()
            .membershipId("basic")
            .endpoint(MembershipEndpointArgs.builder()
                .gkeCluster(MembershipEndpointGkeClusterArgs.builder()
                    .resourceLink(primary.id())
                    .build())
                .build())
            .authority(MembershipAuthorityArgs.builder()
                .issuer(primary.id().applyValue(_id -> String.format("https://container.googleapis.com/v1/%s", _id)))
                .build())
            .build());

    }
}
resources:
  primary:
    type: gcp:container:Cluster
    properties:
      name: basic-cluster
      location: us-central1-a
      initialNodeCount: 1
      workloadIdentityConfig:
        workloadPool: my-project-name.svc.id.goog
      deletionProtection: true
      network: default
      subnetwork: default
  membership:
    type: gcp:gkehub:Membership
    properties:
      membershipId: basic
      endpoint:
        gkeCluster:
          resourceLink: ${primary.id}
      authority:
        issuer: https://container.googleapis.com/v1/${primary.id}

The authority block specifies the issuer URL that Google uses to validate tokens from the cluster. This requires the GKE cluster to have workloadIdentityConfig enabled with a workload pool. The issuer URL follows the pattern https://container.googleapis.com/v1/{cluster.id}, establishing trust between the cluster and Google’s identity system.

Beyond these examples

These snippets focus on specific membership-level features: cluster registration and metadata, regional placement, and workload identity configuration. They’re intentionally minimal rather than full fleet management solutions.

The examples reference pre-existing infrastructure such as GKE clusters with appropriate configuration, and workload identity pools for identity federation. They focus on registering clusters rather than provisioning the clusters themselves.

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

  • Non-GKE cluster registration (external clusters)
  • Multi-cluster service mesh configuration
  • Fleet-level policy management
  • Cluster upgrade management

These omissions are intentional: the goal is to illustrate how each membership feature is wired, not provide drop-in fleet modules. See the GKE Hub Membership resource reference for all available configuration options.

Let's register GCP GKE Hub Cluster Memberships

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

Try Pulumi Cloud for FREE

Frequently Asked Questions

Configuration & Setup
How do I register a GKE cluster with GKE Hub?
Configure the endpoint property with gkeCluster.resourceLink pointing to your cluster. Use the format //container.googleapis.com/${cluster.id} with string interpolation.
How do I set up workload identity for a membership?
Configure the authority property with issuer set to https://container.googleapis.com/v1/${cluster.id}. Your GKE cluster must also have workloadIdentityConfig enabled with the appropriate workload pool.
Immutability & Limitations
What properties can't be changed after creating a membership?
The following properties are immutable and require resource recreation if changed: membershipId, project, endpoint, and location.
Labels & Metadata
Why don't I see all labels on my membership resource?
The labels field is non-authoritative and only manages labels present in your configuration. To see all labels on the resource (including those set by other clients or services), use the effectiveLabels output property.
What's the difference between labels, effectiveLabels, and pulumiLabels?
labels contains only the labels you configure directly. effectiveLabels shows all labels present on the resource in GCP. pulumiLabels combines your configured labels with default labels from the provider.
Location & Regional Deployment
What's the default location for a membership and can I use regional locations?
The default location is global. You can specify regional locations like us-west1 by setting the location property, but note that location is immutable after creation.
Can I import an existing membership?
Yes, you can import using one of three formats: projects/{{project}}/locations/{{location}}/memberships/{{membership_id}}, {{project}}/{{location}}/{{membership_id}}, or {{location}}/{{membership_id}}.

Using a different cloud?

Explore containers guides for other cloud providers: