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 FREEFrequently Asked Questions
Configuration & Setup
endpoint property with gkeCluster.resourceLink pointing to your cluster. Use the format //container.googleapis.com/${cluster.id} with string interpolation.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
membershipId, project, endpoint, and location.Labels & Metadata
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.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
global. You can specify regional locations like us-west1 by setting the location property, but note that location is immutable after creation.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: