GCP (Terraform)
Provision the GCP infrastructure Rhesis runs on (a GKE cluster, networking, and supporting services) using the same Terraform modules Rhesis uses internally.
Part 1 of 2
This guide provisions infrastructure only. Once your cluster is up, continue to the Kubernetes (Helm) guide to deploy the Rhesis application onto it.
Overview
Rhesis itself runs across three GCP projects (dev/stg/prd) plus a fourth project that hosts a WireGuard VPN gating access to the private GKE control planes and an internal-only DNS zone. That split exists for Rhesis’s own multi-environment team operations. It is not a requirement to run the application. This guide provisions the same building blocks in a single GCP project, with the GKE control plane reachable directly from an IP range you choose instead of through a VPN. Every piece Rhesis’s own environments add on top (the WireGuard VPN, Cloudflare-based DNS automation, self-hosted internal DNS) is covered further down as an optional section, so you can adopt as much or as little of the full setup as you want.
What this provisions:
- A VPC with a nodes subnet (plus secondary ranges for pod/service IPs)
- Cloud NAT so private nodes can reach the internet (pulling images, calling AI providers)
- A private-nodes GKE cluster with Workload Identity enabled
- A GCS bucket for Rhesis’s file storage backend
- A service account for External Secrets Operator (ESO), which the Kubernetes (Helm) guide uses to sync application secrets
Rhesis’s own environments add, on top of this, all optional and covered later on this page:
- A WireGuard VPN gating access to the GKE control plane, for teams running multiple clusters. See Gate control-plane access with a VPN
- Cloudflare-managed public DNS via ExternalDNS. See Automate public DNS
- Self-hosted internal DNS (BIND9) for VPN-only hostnames. See Add internal DNS
- The multi-project dev/stg/prd split; this guide uses one project throughout
- GitHub Actions Runner Controller (ARC) secrets, for running self-hosted CI runners in-cluster: out of scope for this guide
ArgoCD itself is bootstrapped in the Kubernetes (Helm) guide, not here; this page stops once you have a working cluster.
Prerequisites
| Requirement | Notes |
|---|---|
| GCP project | With billing enabled |
`gcloud` CLI | Authenticated: `gcloud auth login` + `gcloud config set project <id>` |
| Terraform CLI | Any version compatible with the `hashicorp/google ~> 6.0` provider |
| IAM permissions | Project Editor, or equivalent granular roles for networking/GKE/IAM/storage/Secret Manager |
Enable the required APIs:
Decisions before you start
- Region: any GCP region works; the examples below use
europe-west4. - Control-plane access: this guide uses a public endpoint restricted to authorized networks (no VPN required). You supply the CIDR(s) allowed to reach the GKE API, typically your office network or a static egress IP. Nodes themselves are always private (no public IPs), regardless of this setting. If you’d rather gate access through a VPN the way Rhesis does, see Gate control-plane access with a VPN.
- Public-facing ingress IP: the in-repo
ingress/gcpmodule reserves an internal static IP, for an internal-only ingress class (Rhesis uses this for admin surfaces like ArgoCD and Grafana; see Add internal DNS). For a public-facing deployment, skip that module: the Kubernetes (Helm) guide’singress-nginxLoadBalancerService provisions its own external IP automatically.
Get the Terraform code
Clone the repository and work from the shared modules directly, rather than Rhesis’s own
terraform/infrastructure/envs/* stacks; those are wired for Rhesis’s specific multi-project,
WireGuard-connected layout via remote-state lookups.
Create a new directory alongside envs/ (e.g. envs/customer/) for your own root module:
The CIDR blocks above (10.2.0.0/15, etc.) are examples taken from Rhesis’s own dev environment
IP plan. Adjust them if they conflict with networks you already peer with, but the pattern
(one /15 split into nodes/ilb/master/pods/services) works as-is for a standalone deployment.
Configure your backend
Create a GCS bucket to hold Terraform state (if you don’t already have one), then point Terraform at it:
Configure variables
Initialize, plan, apply
A first apply typically takes 10–15 minutes, mostly waiting on GKE cluster creation. On success, Terraform reports the new VPC, subnets, GKE cluster, ESO service account, and GCS bucket.
Connect to your cluster
If kubectl times out, double-check that admin_cidr includes the public IP you’re connecting
from (curl -s ifconfig.me).
Optional: automate public DNS with Cloudflare
Rhesis’s own environments run ExternalDNS in
the cluster, which watches Ingress/Service objects and writes matching A/TXT records
straight to a DNS provider: no manual DNS updates when an ingress hostname changes. Rhesis uses
Cloudflare; ExternalDNS supports most major providers if you use a different one.
The Terraform side needs a place to hold the Cloudflare API token so External Secrets Operator can sync it into the cluster; the ExternalDNS deployment itself is covered in the Kubernetes (Helm) guide.
This creates a Secret Manager secret named cloudflare-api-token-customer with a placeholder
value. Replace it with a real Cloudflare API token
scoped to Zone.DNS: Edit for your domain’s zone:
Optional: gate control-plane access with a WireGuard VPN
The admin_cidr approach above is simplest for a single admin or a static office IP. Rhesis
instead runs a small WireGuard VM in its own GCP project, VPC-peered to each environment, so
multiple engineers can each get their own tunnel with per-peer, per-subnet access rules.
Worthwhile once more than one or two people need cluster access, or your admin IP isn’t static.
The customer-side cluster’s main.tf needs the matching return peering and, once the WireGuard
project’s plan is applied, an extra_authorized_cidrs entry for the WireGuard server’s public IP
instead of your own:
Both sides of a peering must exist before it goes ACTIVE: apply the customer cluster first
(creating the inactive return-side peering), then the WireGuard project.
Retrieve each peer’s client config from Terraform output and import it into a WireGuard client:
Connecting sets your machine’s DNS resolver to the WireGuard server’s tunnel IP
(10.0.0.1), which matters once you add internal DNS below.
Optional: add self-hosted internal DNS
Once you’re gating access through the WireGuard VM above, Rhesis takes it a step further: a
second ExternalDNS deployment (provider: rfc2136) writes internal-only hostnames (for things
like a private ArgoCD or Grafana) to a BIND9 zone running on the WireGuard VM itself,
authenticated with a TSIG key. It’s a second, independent instance of the same ExternalDNS
mechanism as the public-DNS setup above, pointed at a different provider.
This generates a random TSIG key and stores it in Secret Manager
(internal-dns-tsig-key-customer). Pass the same key into the WireGuard module so BIND9 accepts
updates signed with it:
bind9_allowed_names scopes the TSIG key so it can only create records under the hostnames you
list, not the whole zone. The Kubernetes (Helm)
guide covers deploying the in-cluster internal-dns ExternalDNS instance and pointing an internal
ingress class at these hostnames.
Next step
Continue to Kubernetes (Helm) to bootstrap ArgoCD and deploy the Rhesis application onto this cluster.
Tearing down
Set gke_deletion_protection = false and force_destroy = true (in terraform.tfvars) before
destroying, or Terraform will refuse to delete the cluster and a non-empty storage bucket.
Troubleshooting
“API not enabled” errors on apply: re-run the gcloud services enable command above; API
enablement can take a minute to propagate.
Quota errors creating the node pool: GKE node pools need available regional CPU quota;
check IAM & Admin → Quotas in the GCP console for the target region, or lower
min_node_count/machine_type.
“Permission denied” on first apply: the identity running Terraform needs, at minimum, Compute Network Admin, Kubernetes Engine Admin, Service Account Admin, Storage Admin, and Secret Manager Admin on the project (or the broader Editor role).
WireGuard peering stuck INACTIVE: both sides of a google_compute_network_peering must
exist before it activates; apply the side without the peer yet first, then the other.