Tools on your workstation
Your workstation must also reach the cluster’s control plane. By default the control plane has a public endpoint, which you restrict with
master_authorized_networks. With a private endpoint, you run kubectl and helm from inside your network or over a VPN. See Control-plane access.
Google Cloud project
- The project must already exist, with billing enabled. The configuration doesn’t create projects.
- Use a project that holds nothing else. The application’s service account gets
roles/datastore.userat project level, because Firestore has no per-database data role. In a shared project, that grant silently reaches every other Firestore database too. The APIs the configuration enables and the network it creates are project-scoped for the same reason.
Permissions for the deploying identity
OpenTofu runs as you, through Application Default Credentials (gcloud auth application-default login). No service-account key is needed.
Grant that identity roles/owner on the project. The configuration grants IAM roles to the service accounts it creates, and an identity that can grant roles can already reach everything the project holds.
If your policy doesn’t allow Owner, grant roles that cover every resource type the configuration creates.
On a Shared VPC, the host project needs grants of its own. See Shared VPC.
APIs
OpenTofu enables every API it needs on the first apply:compute, container, firestore, storage, secretmanager, sqladmin, redis, servicenetworking, artifactregistry, aiplatform, iam, iamcredentials, dns and modelarmor (all .googleapis.com). With Private Service Connect it also enables memorystore and networkconnectivity. OpenTofu leaves them enabled when you destroy the installation, because disabling an API affects the whole project.
To enable APIs and manage project IAM, OpenTofu itself needs two APIs first. Enable them:
certificatemanager.googleapis.com.
Capacity and quotas
At the default values, the application’s pods request about 9.6 vCPU and 37.5 GiB of memory: seventeen pods at 0.5 vCPU and 2 GiB, plus the MCP server, the sandbox, a queue worker and one smaller component. The platform services in thejedify namespace add to that, and GKE Autopilot gives default requests to any container that doesn’t set its own. CronJobs start short-lived pods every minute.
- CPU quota. Make sure your regional CPU quota covers those requests, with room for the extra nodes Autopilot adds while it upgrades or consolidates.
- Nodes. Autopilot adds nodes as load needs them. A typical installation runs 6 to 7 nodes.
- Datastores. The defaults are a production shape: Cloud SQL
db-custom-2-7680with a standby in a second zone (REGIONAL), and MemorystoreSTANDARD_HAwith a replica. For a pilot,postgres_availability_type = "ZONAL"andredis_tier = "BASIC"roughly halve their cost. See the OpenTofu reference.
Region and architecture
Decision: the region. It holds the cluster, the bucket, Cloud SQL, Memorystore and Artifact Registry. Decision: the Firestore location. Firestore’s location list is its own: it takes regions such aseurope-west1 and multi-regions such as nam5 and eur3, and not every compute region is on it. You can’t change it after the database exists. Moving means a second database and a migration, so get it right the first time if data residency matters.
Arm or amd64 nodes
Decision: the node architecture. Every pod runs on one architecture, chosen in one place per values file.- Arm (default)
- amd64
Pods run on GKE Autopilot’s That warning is expected. Don’t remove the label to silence it: with the class alone, pods stay
autopilot-arm compute class. It’s available in seven regions only:us-central1, us-east1, us-west1, europe-west1, europe-west2, europe-west4, asia-southeast1and from GKE version 1.35.3-gke.1389000. The cluster follows the REGULAR release channel, so a new cluster is newer than that. On an older cluster, every Arm pod stays Pending; OpenTofu warns about it after the apply.OpenTofu refuses a region outside that list while node_architecture is arm64. That’s deliberate: otherwise the apply succeeds and every pod then waits for an Arm node that never comes.Each pod names both the compute class and the architecture. The class gets Arm nodes provisioned at all; the architecture label adds the toleration that lets a pod onto them. During the install, Autopilot prints this for every pod:Pending next to a ready Arm node.Network
Decision: who owns the network. Either OpenTofu creates a dedicated VPC (the default), or you deploy into a network your network team already runs, including a Shared VPC.- OpenTofu creates the network
- Deploy into an existing network
OpenTofu creates, in your project:
Change the ranges with
subnet_cidrs before the first apply. Secondary ranges can’t be resized once the cluster uses them: running out of pod addresses means rebuilding the cluster. The control plane takes 172.16.0.0/28 (master_ipv4_cidr_block), which must not overlap anything your network routes.What an existing network must provide
Shared VPC
Before the first apply, the host project grants the usual GKE service-project roles:roles/container.hostServiceAgentUseron the host project, to your project’s GKE service agent.roles/compute.networkUseron the subnet, to your project’s GKE service agent and Google APIs service agent.
HOST_PROJECT to the host project and SUBNET to the node subnet’s name, then run, as an identity that can grant roles in the host project:
roles/compute.networkUser on the subnet. Both service agents should appear:
cloudsql_psc_endpoint = "forwarding_rule", OpenTofu creates the endpoint, and its forwarding rule needs compute.networks.use on the host VPC. A network can’t be granted on its own, so your deploying identity needs a role on the host project. With cloudsql_psc_endpoint = "connection_policy", Cloud SQL creates the endpoint through the network’s google-cloud-sql Service Connection Policy, and your deploying identity needs nothing on the host project.
Private Services Access or Private Service Connect
Decision: how the cluster reaches Cloud SQL and the cache (datastore_connectivity).
Gateway address
By default GKE allocates the Gateway’s address when it creates the Gateway, and releases it when the Gateway goes away. A reinstall, or a change of Gateway class, then moves the address, and your DNS records have to follow. To keep the address, reserve it and give its name to the chart asgateway.addressName:
- Internal (default)
- Regional external
- Global external
projects/<host project>/regions/<region>/subnetworks/<subnet>.The purpose is not optional. An address reserved without it gets the default purpose, GCE_ENDPOINT, and the Gateway never gets an address. You can’t change a purpose: delete the address and reserve it again, with --addresses <ip> to keep the same IP.Firewall
In a network OpenTofu creates, GKE manages the load balancer’s firewall rules itself. In an existing network, and especially on a Shared VPC, make sure these rules exist:Egress masquerade
Decision: whether pod addresses may leave the VPC. GKE Autopilot keeps a pod’s own address when it calls a private destination: itsdefault EgressNATPolicy exempts RFC 1918 ranges and 100.64.0.0/10. So a call to a gateway, a firewall or a warehouse on a private address arrives from the pod range.
If your pod range must never be seen outside the VPC (a 100.64.x range reused across clusters, or a central firewall that should see node addresses), set egressMasquerade.enabled: true in values/jedify-infra.yaml. Every packet a pod sends out of the cluster then leaves with its node’s address, so firewalls and allowlists see the node subnet. Traffic inside the cluster is unaffected.
The chart rewrites the default EgressNATPolicy on every install and upgrade. Turning the setting off later stops the rewrite; it doesn’t restore GKE’s original list.
Control-plane access
Decision: who can reach the Kubernetes API.- Public endpoint (default). With
master_authorized_networksempty, any address on the internet can reach the API server. Authentication and authorization still apply, but on an installation meant to talk to nobody, that’s the opposite of the intent. Setmaster_authorized_networksto your operators’ egress ranges. It defaults to empty only because a list that leaves out whoever runstofu applylocks them out of the cluster they just created. - Private endpoint.
enable_private_endpoint = truemakes the API reachable only from inside the network. GKE then requires authorized networks: list your VPN’s ranges inmaster_authorized_networks, because with the list empty only the cluster’s own nodes and pods reach the API, andhelm installtimes out from your VPN. If your VPN enters the network in a different region, also setprivate_endpoint_global_access = true. To place the private endpoint in a subnet your network team chose, setprivate_endpoint_subnetwork.
Egress
Nodes have no public addresses. Everything below needs a path out of the cluster, through Cloud NAT or a route your network provides. A blocked call hangs instead of failing, which is slower to diagnose, so allow these before the install.
From your workstation,
tofu init reaches registry.opentofu.org for providers, and you reach the release registry to pull the release.
DNS and TLS
Decision: two hostnames. The installation serves two names, and both resolve to the Gateway’s address:
Put them on names you control, not on names derived from an IP address. The web app names its endpoints in its settings and Zitadel stores redirect URIs per application, so a hostname tied to the address turns any address change into a settings change.
Decision: the Gateway class (
gateway.className).
Changing the class reallocates the address, so DNS has to follow.
Decision: the certificate. One certificate must cover both hostnames (a wildcard, or both names as subject alternative names), and your users’ browsers must trust it. A certificate from a private certificate authority works only on devices that trust that authority: the web app fetches the identity provider’s discovery document in the background, and browsers offer no click-through warning for a background request, so an untrusted certificate simply fails there.
To see the certificate a host actually serves, as opposed to what a certificate resource reports: