Foundations
opentofu/gcp builds the network and an
empty Autopilot cluster, then steps away: the catalog arrives through the
bootstrap and Flux.
What is decided for you
Section titled “What is decided for you”Enforced: no variable. Default: a variable changes it.
| Position | |
|---|---|
| Autopilot, no cluster mode option | enforced |
Regular release channel; EXTENDED refused | default |
| A maintenance window of at least 4 hours, no default | required |
| Upgrade notifications to a Pub/Sub topic | default |
| Only the free system metrics; system and workload logs | default |
| Backup for GKE agent off | default |
| Workload Identity, pool and principal prefix as outputs | Autopilot |
| Dataplane V2 and NetworkPolicy | Autopilot |
| GKE’s Gateway API controller, standard channel | default |
| Private nodes and Cloud NAT | default |
| DNS-based control plane endpoint only; no client certificate | default |
Nodes /22, Pods /16, proxy-only /23, no Services range | default |
| Subnet flow logs, 0.5 sampling over 10 minutes | default |
| GKE cost allocation on from creation | default |
owner, environment, socle-version labels on every billable resource | enforced |
| Deletion protection on | default |
| 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_rolestakes 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_numberis 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, setcreate_subnetwork = falsewithsubnetwork_nameandpod_range_name, and the plan stops asking for NAT.
What is deliberately absent
Section titled “What is deliberately absent”EXTENDEDchannel: 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_kubernetescarries an exec block forgke-gcloud-auth-plugin, not a token.
Measured
Section titled “Measured”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/ownerwas denied; under the bucket condition, a bucket inside the prefix was created and one outside denied,roles/storage.objectAdminon a bucket granted androles/storage.admindenied. - A managed zone’s
getIamPolicyandsetIamPolicy, granted on the zone by a custom role and byroles/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.
Reference
Section titled “Reference”Requirements
Section titled “Requirements”| Name | Version |
|---|---|
| terraform | >= 1.10 |
| >= 8.0, < 9.0 |
Modules
Section titled “Modules”No modules.
Resources
Section titled “Resources”Inputs
Section titled “Inputs”| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| cluster_name | Name of the GKE cluster. Also prefixes the network resources the module creates. | string | n/a | yes |
| environment | Environment this cluster serves. Stamped as a label, and the axis the upgrade ring order follows. | string | n/a | yes |
| maintenance_window | When 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({ | n/a | yes |
| owner | Team accountable for the cluster. Stamped as a label on every billable resource. | string | n/a | yes |
| project_id | Google Cloud project that holds the cluster and its network. | string | n/a | yes |
| region | Region of the cluster and its subnetwork. The cluster is regional; zonal clusters are not offered. | string | n/a | yes |
| additional_labels | Extra labels merged onto the standard set. Cannot override owner, environment or socle-version. | map(string) | {} | no |
| backup_agent_enabled | Install 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. | bool | false | no |
| billing_export_dataset_id | When 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. | string | null | no |
| billing_export_dataset_location | Location of the billing export dataset. Ignored when billing_export_dataset_id is null. | string | "EU" | no |
| control_plane_dns_allow_external_traffic | Allow 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. | bool | true | no |
| control_plane_ip_endpoints_enabled | Expose the control plane on IP endpoints. Off: the DNS-based endpoint replaces them, and with it the authorized-networks maintenance problem. | bool | false | no |
| cost_allocation_enabled | Add cluster, namespace and workload labels to the detailed billing export. On from day one because it does not backfill. | bool | true | no |
| create_nat | Create a Cloud Router and Cloud NAT for egress. A cluster with private nodes and no NAT cannot pull an image from outside Google Cloud. | bool | true | no |
| create_network | Create the VPC instead of using an existing one. The common case is a network the consumer already owns. | bool | false | no |
| create_proxy_only_subnet | Create the proxy-only subnetwork. Set to false when another cluster in the same region and VPC already created it — the pool is shared. | bool | true | no |
| create_subnetwork | Create 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. | bool | true | no |
| crossplane | Give 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({ | null | no |
| deletion_protection | Refuse to destroy the cluster. On by default; test fixtures turn it off. | bool | true | no |
| enable_private_nodes | Nodes get no external address. Flips the Autopilot default, which is public. | bool | true | no |
| enable_upgrade_notifications | Create a Pub/Sub topic and publish cluster upgrade notifications to it, so automation can react instead of polling. | bool | true | no |
| gateway_api_enabled | Run 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. | bool | true | no |
| gateway_certificate | The Certificate Manager certificates the socle's two Gateways terminate TLS with, at the load balancer: every name in domains (wildcardsincluded, 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 mustnot already hold an _acme-challenge.<domain> record (one another ACMEclient 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({ | null | no |
| kubernetes_min_version | Floor for the control plane version. Raise-only escape hatch for promoting a minor deliberately; leave null in steady state and let the channel decide. | string | null | no |
| logging_components | GKE log sources to send to Cloud Logging. SYSTEM_COMPONENTS cannot be removed; drop WORKLOADS when application logs are collected in-cluster. | list(string) | [ | no |
| maintenance_exclusions | Windows during which GKE must not upgrade the cluster. The brake, not the routine: empty by default. | list(object({ | [] | no |
| monitoring_components | GKE metric sources. SYSTEM_COMPONENTS is free and mandatory; every other component is billed per sample, so none is defaulted on. | list(string) | [ | no |
| network_name | Name of an existing custom-mode VPC to attach the cluster to. Mutually exclusive with create_network. | string | null | no |
| node_range_cidr | Primary 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_members | Principals granted read-only access to this project's metrics — the central observability cluster's federated identity, never a key. | list(string) | [] | no |
| pod_range_cidr | Secondary 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_name | Name 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. | string | null | no |
| project_number | The 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. | string | null | no |
| proxy_only_range_cidr | Range 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_channel | GKE 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_enabled | Enable 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. | bool | true | no |
| subnetwork_name | Name of an existing subnetwork in var.region to place the cluster in. Required when create_subnetwork is false, ignored otherwise. | string | null | no |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
| billing_export_dataset | Reference 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_certificate | Base64-encoded cluster CA certificate, for building a kubeconfig. |
| cluster_dns_endpoint | The 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_endpoint | IP endpoint of the control plane. Empty when IP endpoints are disabled, which is the default. |
| cluster_location | Region of the cluster. Regional by default; there is no zonal option. |
| cluster_name | Name of the GKE cluster. |
| crossplane_principal | The 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_role | Full 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_map | Name 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_certificate | Name 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_kubernetes | Drop-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. |
| labels | The standard label set applied to every billable resource this module creates. |
| network_name | Name of the VPC the cluster is attached to, whether the module created it or not. |
| oidc_issuer_url | The cluster's OIDC issuer, for federating an external identity provider against this cluster. |
| pod_range_name | Name of the secondary range carrying Pod addresses. |
| project_id | The project the cluster lives in — half of every workload's principal, with project_number. |
| project_number | The project's number, which a Workload Identity Federation principal names the project by. |
| proxy_only_subnetwork_name | The REGIONAL_MANAGED_PROXY subnetwork regional Gateways draw their proxies from. Null when the module did not create it. |
| region | Region of the cluster, and of the internal Gateway's regional certificate. |
| subnetwork_name | Name of the cluster's subnetwork. |
| upgrade_notifications_topic | Pub/Sub topic carrying GKE upgrade and security bulletin notifications. Null when notifications are disabled. |
| workload_identity_pool | The 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_prefix | Prefix 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. |