Skip to content
The last release, 0.1.0, predates this site: this is the documentation of main.

Foundations

opentofu/gcp builds the network and an empty Autopilot cluster, then steps away: the catalog arrives through the bootstrap and Flux.

Enforced: no variable. Default: a variable changes it.

Position
Autopilot, no cluster mode optionenforced
Regular release channel; EXTENDED refuseddefault
A maintenance window of at least 4 hours, no defaultrequired
Upgrade notifications to a Pub/Sub topicdefault
Only the free system metrics; system and workload logsdefault
Backup for GKE agent offdefault
Workload Identity, pool and principal prefix as outputsAutopilot
Dataplane V2 and NetworkPolicyAutopilot
GKE’s Gateway API controller, standard channeldefault
Private nodes and Cloud NATdefault
DNS-based control plane endpoint only; no client certificatedefault
Nodes /22, Pods /16, proxy-only /23, no Services rangedefault
Subnet flow logs, 0.5 sampling over 10 minutesdefault
GKE cost allocation on from creationdefault
owner, environment, socle-version labels on every billable resourceenforced
Deletion protection ondefault
No Google service account: every workload, Crossplane included, a federated principal named by the project number (GCP-14)enforced
Crossplane’s identity off unless asked; when on, it grants only allowed_roles, buckets under <cluster_name>- only and never deleted, nothing of Cloud DNS (GCP-15)opt-in
Gateway TLS at the load balancer: a Certificate Manager map for the public Gateway, a regional certificate for the private one, DNS-authorised (GCP-16)opt-in
Under the hood
  • Private nodes flip Autopilot’s default; the IP endpoints are off.
  • Minimum sizes: Pods /17, proxy-only /26.
  • Crossplane’s principal is crossplane-system/provider-gcp, granted before the cluster has a node. allowed_roles takes at most nine roles; owner, editor, viewer, iam.*, resourcemanager.* and predefined admin or owner roles are refused at plan. The DNS records role, created here, is what Crossplane grants external-dns on the project: a zone’s own IAM is never honoured.
  • project_number is read from the API unless you pass it; it names every principal.
  • The network is the module’s (create_network = true) or yours (network_name). In a Shared VPC whose subnets the network team owns, set create_subnetwork = false with subnetwork_name and pod_range_name, and the plan stops asking for NAT.
  • EXTENDED channel: forbidden on Autopilot.
  • Workload Identity, CNI, datapath and Hubble toggles: Autopilot enforces them.
  • master_authorized_networks, master_ipv4_cidr_block: they govern the IP endpoints, which are off.
  • A Services range: GKE assigns its own on Autopilot 1.27 and later.
  • Auto IPAM: Preview when decided.
  • Auto-Monitoring, gke_auto_upgrade_config: silent recurring cost.
  • Config Connector: Crossplane is the choice.
  • Gateway, NetworkPolicy, alert, dashboard and uptime objects: catalog concerns.
  • Any credential as input, any key as output: the cluster CA is the one sensitive output; helm_kubernetes carries an exec block for gke-gcloud-auth-plugin, not a token.

On a GKE Autopilot sandbox project, 2026-10-07 (PR #84), through opentofu/clusters/gcp:

  • Under Crossplane’s Project IAM Admin condition, granting an allowed role succeeded and granting roles/owner was denied; under the bucket condition, a bucket inside the prefix was created and one outside denied, roles/storage.objectAdmin on a bucket granted and roles/storage.admin denied.
  • A managed zone’s getIamPolicy and setIamPolicy, granted on the zone by a custom role and by roles/dns.admin, were never honoured (403 for minutes); record operations granted there were.
  • Both certificates were issued once their authorization records resolved, and both shared Gateways programmed.
  • A destroy takes two runs: GKE removes the Gateways’ load balancers after the cluster, so the first fails on the certificate map, the regional certificate and the proxy-only subnetwork; the second completes, no Workload Identity binding left in the project policy.

CI also plans the module against floci-gcp, which implements no Compute Engine API.

NameVersion
terraform>= 1.10
google>= 8.0, < 9.0

No modules.

NameType
google_bigquery_dataset.billing_exportresource
google_certificate_manager_certificate.gatewayresource
google_certificate_manager_certificate.gateway_regionalresource
google_certificate_manager_certificate_map.gatewayresource
google_certificate_manager_certificate_map_entry.gatewayresource
google_certificate_manager_dns_authorization.gatewayresource
google_certificate_manager_dns_authorization.gateway_regionalresource
google_compute_network.socleresource
google_compute_router.socleresource
google_compute_router_nat.socleresource
google_compute_subnetwork.proxy_onlyresource
google_compute_subnetwork.socleresource
google_container_cluster.socleresource
google_dns_record_set.gateway_certificate_authorizationresource
google_project_iam_custom_role.crossplane_bucketsresource
google_project_iam_custom_role.dns_recordsresource
google_project_iam_member.crossplane_bucketsresource
google_project_iam_member.crossplane_project_grantsresource
google_project_iam_member.observability_readerresource
google_pubsub_topic.upgrade_notificationsresource
google_project.thisdata source
NameDescriptionTypeDefaultRequired
cluster_nameName of the GKE cluster. Also prefixes the network resources the module creates.stringn/ayes
environmentEnvironment this cluster serves. Stamped as a label, and the axis the upgrade ring order follows.stringn/ayes
maintenance_windowWhen GKE may touch this cluster. Required on purpose — a silent default
would mean nobody decided when production gets upgraded, and the day of
the week is what orders a dev/staging/prod ring.

start_time and end_time are RFC3339 timestamps whose difference is the
window length; recurrence is an RFC5545 RRULE. At least 48 hours of
maintenance availability must remain in any 92-day rolling window, and
only contiguous blocks of four hours or more count.
object({
start_time = string
end_time = string
recurrence = string
})
n/ayes
ownerTeam accountable for the cluster. Stamped as a label on every billable resource.stringn/ayes
project_idGoogle Cloud project that holds the cluster and its network.stringn/ayes
regionRegion of the cluster and its subnetwork. The cluster is regional; zonal clusters are not offered.stringn/ayes
additional_labelsExtra labels merged onto the standard set. Cannot override owner, environment or socle-version.map(string){}no
backup_agent_enabledInstall the Backup for GKE agent. Off by default: $9 per protected namespace per month, where Velero covers a Persistent-Disk-backed socle for a third of that.boolfalseno
billing_export_dataset_idWhen set, create a BigQuery dataset to receive the detailed billing export. Linking the billing account to it stays a Console step — Google exposes no API for it.stringnullno
billing_export_dataset_locationLocation of the billing export dataset. Ignored when billing_export_dataset_id is null.string"EU"no
control_plane_dns_allow_external_trafficAllow user traffic to the DNS-based control plane endpoint. True means the control plane is reachable wherever Google Cloud APIs are, gated by IAM; VPC Service Controls is the network boundary and is set outside this module.booltrueno
control_plane_ip_endpoints_enabledExpose the control plane on IP endpoints. Off: the DNS-based endpoint replaces them, and with it the authorized-networks maintenance problem.boolfalseno
cost_allocation_enabledAdd cluster, namespace and workload labels to the detailed billing export. On from day one because it does not backfill.booltrueno
create_natCreate a Cloud Router and Cloud NAT for egress. A cluster with private nodes and no NAT cannot pull an image from outside Google Cloud.booltrueno
create_networkCreate the VPC instead of using an existing one. The common case is a network the consumer already owns.boolfalseno
create_proxy_only_subnetCreate the proxy-only subnetwork. Set to false when another cluster in the same region and VPC already created it — the pool is shared.booltrueno
create_subnetworkCreate the cluster subnetwork. Set to false in a Shared VPC where the network team owns subnets, and supply subnetwork_name and pod_range_name instead.booltrueno
crossplaneGive the catalog's crossplane module its Google Cloud identity — the one
the socle cannot make for itself: grants to the federated principal of
crossplane-system/provider-gcp, no Google service account. allowed_roles
is the boundary: the roles (predefined, or custom by full name) Crossplane
may grant any module's principal on the project or on a bucket, through
Project IAM Admin under a modifiedGrantsByRole condition. Empty grants
nothing but the socle's own DNS records role; owner, editor, viewer,
iam.* and resourcemanager.* are refused, and Google caps the list at
nine. Predefined admin and owner roles (roles/storage.admin,
roles/bigquery.dataOwner, …) are refused too, roles/storage.objectAdmin
excepted: they grant roles on their resources, and Crossplane could give
one to itself. A custom role that carries a setIamPolicy permission must
not be listed, for the same reason. Buckets are reachable under the
<cluster_name>- prefix only, and never deletable. Nothing of Cloud DNS:
external-dns writes records through the socle's DNS records role,
granted on the project. Null, the default, creates nothing.
object({
allowed_roles = optional(list(string), [])
})
nullno
deletion_protectionRefuse to destroy the cluster. On by default; test fixtures turn it off.booltrueno
enable_private_nodesNodes get no external address. Flips the Autopilot default, which is public.booltrueno
enable_upgrade_notificationsCreate a Pub/Sub topic and publish cluster upgrade notifications to it, so automation can react instead of polling.booltrueno
gateway_api_enabledRun GKE's Gateway API controller and let GKE install and upgrade the standard-channel Gateway API CRDs. On by default and stated rather than assumed: the socle installs nothing for Gateway API on GKE because of it, and its templates target GKE's classes. Off leaves the cluster without Gateway API at all.booltrueno
gateway_certificateThe Certificate Manager certificates the socle's two Gateways terminate
TLS with, at the load balancer: every name in domains (wildcards
included, such as ["acme.example", "*.acme.example"]), authorised by DNS
in the Cloud DNS managed zone dns_zone (its name, in this project).
A global certificate in a certificate map serves the public Gateway; a
regional one serves the internal Gateway. Every route published through a
Gateway — ArgoCD's included — is then <name>.<domain>. The zone must
not already hold an _acme-challenge.<domain> record (one another ACME
client left behind): the global authorization writes that name. Null, the
default, creates nothing, and the bootstrap module creates no Gateway: the
socle never serves a route in clear text.
object({
dns_zone = string
domains = list(string)
})
nullno
kubernetes_min_versionFloor for the control plane version. Raise-only escape hatch for promoting a minor deliberately; leave null in steady state and let the channel decide.stringnullno
logging_componentsGKE log sources to send to Cloud Logging. SYSTEM_COMPONENTS cannot be removed; drop WORKLOADS when application logs are collected in-cluster.list(string)
[
"SYSTEM_COMPONENTS",
"WORKLOADS"
]
no
maintenance_exclusionsWindows during which GKE must not upgrade the cluster. The brake, not the routine: empty by default.
list(object({
name = string
start_time = string
end_time = string
scope = string
}))
[]no
monitoring_componentsGKE metric sources. SYSTEM_COMPONENTS is free and mandatory; every other component is billed per sample, so none is defaulted on.list(string)
[
"SYSTEM_COMPONENTS"
]
no
network_nameName of an existing custom-mode VPC to attach the cluster to. Mutually exclusive with create_network.stringnullno
node_range_cidrPrimary range of the cluster subnetwork, used by nodes and by internal load balancers. A /22 carries 1020 nodes, which is what the Pod range allows.string"10.0.0.0/22"no
observability_reader_membersPrincipals granted read-only access to this project's metrics — the central observability cluster's federated identity, never a key.list(string)[]no
pod_range_cidrSecondary range for Pod addresses. Autopilot fixes 32 Pods per node, so a /26 is consumed per node: a /16 carries 1024 nodes. Sized generously on purpose — a cluster's Pod range cannot be changed after creation, while the primary range can be expanded in place.string"10.4.0.0/16"no
pod_range_nameName of the secondary range that carries Pod addresses. Defaults to <cluster_name>-pods, which is what the module creates; set it when attaching to a subnetwork someone else owns.stringnullno
project_numberThe project's number, which every Workload Identity Federation principal names the project by. Null, the default, reads it from the project; set it only where that read cannot happen — the Cloud Billing call the provider makes alongside it is what an emulator lacks.stringnullno
proxy_only_range_cidrRange of the REGIONAL_MANAGED_PROXY subnetwork. Regional Application Load Balancers, and therefore Gateways, cannot exist without it.string"10.8.0.0/23"no
release_channelGKE release channel. REGULAR is Google's recommendation and the estate-wide default; RAPID is outside the GKE SLA and belongs in pre-production only.string"REGULAR"no
subnet_flow_logs_enabledEnable VPC flow logs on the cluster subnetwork, at half sampling over ten-minute windows. Vended network logs are billed at $0.25/GiB, which is a dollar or so a month at that sampling for a socle cluster.booltrueno
subnetwork_nameName of an existing subnetwork in var.region to place the cluster in. Required when create_subnetwork is false, ignored otherwise.stringnullno
NameDescription
billing_export_datasetReference of the BigQuery dataset waiting for the detailed billing export. Null when none was requested. The billing account still has to be linked to it by hand.
cluster_ca_certificateBase64-encoded cluster CA certificate, for building a kubeconfig.
cluster_dns_endpointThe control plane's DNS endpoint — the access path the socle and its automation use. Stable for the life of the cluster and authorised by IAM.
cluster_endpointIP endpoint of the control plane. Empty when IP endpoints are disabled, which is the default.
cluster_locationRegion of the cluster. Regional by default; there is no zonal option.
cluster_nameName of the GKE cluster.
crossplane_principalThe federated principal the catalog's crossplane module's GCP providers run as (crossplane-system/provider-gcp), read off its grants: whatever consumes it is applied after them and destroyed before them. Null when crossplane is not set.
dns_records_roleFull name of the custom role external-dns writes DNS records with: find the project's managed zones, read and change their record sets. Project-level, because a zone's IAM policy is never honoured when granted on the zone itself (measured 2026-10-07), so Crossplane binds it on the project, and --domain-filter bounds what external-dns writes. Always among the roles Crossplane may grant. Built from its ID, so known on the first plan. Empty when crossplane is not set.
gateway_certificate_mapName of the Certificate Manager map the public Gateway's networking.gke.io/certmap annotation takes — what the bootstrap module's gateway_certificate_map takes. Empty when gateway_certificate is not set.
gateway_regional_certificateName of the regional Certificate Manager certificate the internal Gateway's HTTPS listener takes, in its networking.gke.io/cert-manager-certs TLS option — what the bootstrap module's gateway_regional_certificate takes. Empty when gateway_certificate is not set.
helm_kubernetesDrop-in value for the helm provider's kubernetes attribute, so a root configures it in one line. Uses the DNS endpoint, the only one enabled by default, which serves a publicly trusted certificate: no cluster CA is carried (use cluster_ca_certificate with the IP endpoint). Carries no credential: gke-gcloud-auth-plugin obtains a short-lived token from the caller's ambient gcloud credentials at call time.
labelsThe standard label set applied to every billable resource this module creates.
network_nameName of the VPC the cluster is attached to, whether the module created it or not.
oidc_issuer_urlThe cluster's OIDC issuer, for federating an external identity provider against this cluster.
pod_range_nameName of the secondary range carrying Pod addresses.
project_idThe project the cluster lives in — half of every workload's principal, with project_number.
project_numberThe project's number, which a Workload Identity Federation principal names the project by.
proxy_only_subnetwork_nameThe REGIONAL_MANAGED_PROXY subnetwork regional Gateways draw their proxies from. Null when the module did not create it.
regionRegion of the cluster, and of the internal Gateway's regional certificate.
subnetwork_nameName of the cluster's subnetwork.
upgrade_notifications_topicPub/Sub topic carrying GKE upgrade and security bulletin notifications. Null when notifications are disabled.
workload_identity_poolThe Workload Identity Federation pool. Autopilot enforces it, so this is derived rather than configured. Grant IAM roles to principals in this pool to give a catalog workload direct resource access.
workload_identity_principal_prefixPrefix of a workload's IAM principal identifier, naming the project by its number as Workload Identity Federation requires. Append /ns/NAMESPACE/sa/SERVICEACCOUNT. Note that two clusters in one project produce identical principals for the same namespace and service account.