GKE with Terraform
Onyx ships Terraform modules for GCP that create the Google Cloud infrastructure for Onyx. After Terraform finishes, you install Onyx with the Helm chart.Managed services
onyx module connects all of these modules. You can also use each module on its own.
Prerequisites
Install the tools
- Terraform
>= 1.12.0 - Google Cloud CLI (
gcloud) kubectlandhelmjq, to read a Terraform output in a later step
Log in to Google Cloud
Enable two project APIs
onyx module enables the other project APIs it needs. It cannot enable these two,
because Terraform reads the project before the first apply. Enable them before the first terraform plan:Choose where Terraform runs
onyx namespace and a Kubernetes service account through the GKE API server.
Run terraform apply from an address in master_authorized_networks. If you set private_endpoint_enabled = true,
run it from inside the VPC. From any other address, the apply stops at those two resources.Allow OS Login (only with the OS Login policy)
compute.managed.requireOsLogin policy, the first cluster creation fails.
The module sets no node metadata. Turn on OS Login for the project before the first terraform apply:Quickstart
This root module creates a complete Onyx stack. It uses the module from the Onyx repository at a fixed tag.Pin the module version
GCP module releases have tags of the formtf-gcp/vX.Y.Z.
These versions are independent of the AWS modules (tf/vX.Y.Z), the Azure modules (tf-azure/vX.Y.Z),
and Onyx releases. A commit SHA also works as the ref. For automated pipelines, use a SHA, because nobody can move it.
Always set a ref. Without it, Terraform uses the default branch,
and an unrelated change can change your infrastructure.
T-shirt sizing
Thesize input sets all compute and data-plane values together. If you set an individual sizing input,
that value replaces the tier default.
small main node has,
so the cluster adds nodes. To set the pod resources for each tier,
see the chart’s SIZING.md.
Copy only the resources values. Its OpenSearch nodeSelector is for EKS. On GKE,
use the one in the Helm values below.
Common configuration
us-east1. Do not use a zone.dev, staging, and prod.small, medium, or large. See T-shirt sizing.postgres user. It must have at least 8 characters.
Pass it with TF_VAR_postgres_password or from a secret store.POSTGRES_DB to this value in the Helm values.REGIONAL adds a standby in a second zone. It costs approximately two times more.private_endpoint_enabled,
or allow_unrestricted_api_server_access.
The module does not create an API server that is open to all addresses by accident.false to use the Redis in the cluster.6378. A change to this value replaces the instance.["onyx.example.com"].
Use lowercase hostnames with no wildcard. You must set at least one when enable_l7_ingress = true.false to use an existing network. Then also set network_id, subnet_id, pods_range_name,
and services_range_name. The network must already have Private Service Access and Cloud NAT.false and apply before you run terraform destroy.modules/gcp/onyx/variables.tf.
Outputs
l7_* outputs are null when enable_l7_ingress = false.
Install Onyx with Helm
Connect to the cluster
Install cert-manager (only for Let's Encrypt)
letsencrypt.enabled: true in the Helm values, the chart creates a cert-manager ClusterIssuer.
The cert-manager CRDs must exist before you install the chart. Install cert-manager one time for each cluster:Create the Secrets
onyx namespace. Do not create it again. Create the Secrets in that namespace:Create the CA ConfigMaps
postgresTls in the next step:Write the Helm values
<...> with the Terraform output of the same name.GCS_SERVICE_ACCOUNT_KEY_PATH or GCS_SERVICE_ACCOUNT_KEY_JSON.
Onyx then uses Application Default Credentials,
which get the Workload Identity of the onyx-workload-access service account.
The service account needs no annotation,
because IAM grants the bucket roles directly to the Kubernetes service account.Install the chart
onyx namespace. The service account with the bucket grant exists only in that namespace.--version. Without it, each upgrade installs the newest chart.Verify the deployment
Check the pods
Running. Make sure that the OpenSearch pod runs on a node of the document index pool.Check the API server logs
403errors from Cloud Storage: the pods do not run asonyx-workload-access, or the release is not in theonyxnamespace.- Redis connection errors:
redisTlsis not enabled, orREDIS_PORTis not6378. Missing Authority Key Identifier:postgresTlsis set on an Onyx version that cannot verify Cloud SQL. RemovepostgresTls.
Open Onyx
WEB_DOMAIN to http://localhost:8080, or login redirects go to the wrong address.For public access, point a DNS record such as onyx.example.com to the external IP of the onyx-nginx Service.
This is the default L4 path.Serve through an L7 load balancer (Cloud Armor)
By default, the chart exposes Onyx through ingress-nginx behind a Service of typeLoadBalancer. On GKE,
that is an L4 pass-through load balancer. A Cloud Armor policy cannot attach to it.
If you do not attach the policy to an L7 load balancer, it protects nothing, and Google Cloud shows no error.
To put the policy in front of Onyx, serve through a global external Application Load Balancer.
The GKE Gateway API builds that load balancer. The gke module already turns on the Gateway API.
This path replaces the ingress-nginx LoadBalancer Service, cert-manager, and Let’s Encrypt for the hosts it serves.
Terraform creates the GCP resources that the Gateway uses: a global address,
and a certificate map with one Google-managed certificate for each domain. You apply the Kubernetes objects yourself.
They are CRDs, and a kubernetes_manifest of a CRD fails the plan of a new cluster.
The load balancer sends traffic to the Onyx server block of the chart’s nginx, on port 1024.
The chart’s LoadBalancer Service uses the same port. The API, web server, and MCP routes stay in nginx.
Turn it on in Terraform
enable_cloud_armor on, which is the default.
Then run terraform apply. The onyx module turns on the Certificate Manager API.
If you set enable_project_apis = false, turn on that API yourself first.Add the DNS authorization records
CNAME named _acme-challenge.<domain>..
The record proves that you control the domain. It does not move traffic,
so the current load balancer continues to serve Onyx.Wait for the certificates
ACTIVE on each certificate. This usually takes minutes after the CNAME resolves,
but it can take hours when the DNS provider is slow. If a certificate stays PROVISIONING,
managed.authorizationAttemptInfo in the full output tells why.Apply the Kubernetes objects
<> with the Terraform output of the same name.
The examples use the release name onyx and the namespace onyx.kubectl -n onyx describe gateway onyx shows Programmed: True. Each policy shows Attached: True in its status.Test the new path before you move DNS
Tell Onyx its public URL
Secure and builds its login redirects from WEB_DOMAIN.
Set it to the HTTPS address in the Helm values:Move DNS
A record of each domain at l7_ip_address.Remove the L4 load balancer
LoadBalancer.
Make it a cluster-internal Service. This removes the L4 load balancer and its public address.
Certificate Manager now serves the certificate for these hosts,
so turn off the chart’s ingress and letsencrypt too. You no longer need cert-manager for these hosts.helm upgrade with the new values.cloud_armor_preview = true to log rule matches without blocking requests while you tune the rules.
Notes:
- Cloud Armor sees the real client address. The rate limits count each client separately, and the IP and country rules match the client.
- When you add or remove a domain, Terraform adds or removes only the certificate of that domain. The other domains continue to serve.
deletion_protectionguards the address and the DNS authorizations. Set it tofalseand apply before you remove a domain.- If one response can stream for longer than one hour, make
timeoutSeclarger.
Run the model servers on GPUs (optional)
enable_gpu_node_pool = true adds a GPU node pool. It does not move a model server to the pool.
The pool has the taint nvidia.com/gpu=present:NoSchedule and the label onyx.app/gpu=true. To use it,
set nodeSelector, tolerations,
and an nvidia.com/gpu limit on inferenceCapability or indexCapability in the Helm values.
The default GPU pool has one node with one GPU, so give the GPU to one model server only.
Notes
- State storage. For shared or production use, store Terraform state in a
gcsbackend. - Workspaces. Resource names include the active Terraform workspace. A
name = "onyx"module in workspaceprodcreatesonyx-prodresources. - Existing network. With
create_network = false, the network must already have a Private Service Access connection. If not, Cloud SQL and Memorystore fail to create. - Database name. If you do not set
POSTGRES_DB, Onyx uses thepostgresdatabase that Cloud SQL creates, and theonyxdatabase stays empty. - Database connections.
postgres_max_connectionsdefaults to500. Make it larger if you add many API or worker replicas. A change restarts the instance. - Destroy. Set
deletion_protection = falseand apply before you runterraform destroy.
Compute Engine VM
Make sure that your Google Cloud account can create a VM instance.Create a VM instance
e2-standard-4 instance.- Give your instance a descriptive name like
onyx-prod - Select the
Debian GNU/Linux 12boot disk - Select the
e2-standard-4machine type - Select
Allow HTTPS trafficin the Firewall section - Configure storage following the Resourcing Guide



Create the instance
Point domain to the instance
A and CNAME record to our DNS provider.The A record should be the subdomain that you would like to use for the Onyx instance like prod.The CNAME record should be the same name with the www. in front resulting in www.prod pointing to the full
domain like prod.onyx.app.

Install Onyx requirements
git, docker, and docker compose.To install these on Debian GNU/Linux 12, run the following:docker without sudo, add your user to the docker group. Then log out and log in again:Install and Configure Onyx
.env and .env.nginx files.Launch Onyx
init-letsencrypt.sh script will get us a SSL certificate from letsencrypt and launch the Onyx stack.docker logs onyx-stack-api_server-1 -f.