Skip to main content
Every step below is a command and a check. Don’t move to the next step until the check passes. A step that asks you to choose something, rather than run something, says Decision. Run every command in one shell session: later steps reuse variables that earlier steps set. After step 2, run them from the release bundle’s directory.
Complete the requirements first. The first OpenTofu apply alone takes 15 to 20 minutes, most of it the cluster.

At a glance

See Two roles for what Role A and Role B mean.

Get the release

1

Set the install variables

Decision. Fill in your own values. Every later step reads these.
  • PREFIX names every resource, including the Cloud Storage buckets <prefix>-storage and <prefix>-tfstate. Bucket names are unique across all of Google Cloud, so a generic prefix fails with a name-taken error that reads like a permissions problem. Use something specific to your organization: lowercase letters, digits and hyphens, starting with a letter, 2 to 21 characters.
  • ADMIN_EMAIL must be a real address. Reserved domains such as .test or .example are rejected later by the application’s own email validation.
  • ORG_NAME is the organization your users belong to. Feature flags match it in lowercase, so pick the name you’ll keep.
To verify, print them back and check none is empty:
2

Pull the bundle and the charts

Sign in as the identity Jedify granted access to the release registry. Each of the first two commands opens a browser to sign in; the second sets the Application Default Credentials OpenTofu uses.
Then pull the release:
Expected output:
Every later command runs from this directory, $BUNDLE_DIR. The charts carry all their dependencies, so installing them reaches no chart repository. A 401 from either login means your identity hasn’t been granted read access to the release registry.

Infrastructure

3

Create the state bucket

OpenTofu’s state lives in a bucket in your project, created once. Versioning turns a bad apply or a corrupted state file into a restore, and it only protects objects written after it’s switched on, so switch it on now.
To verify, check versioning is on. Expected output: True.
4

Write terraform.tfvars

Decision. Add any of these that apply. Each is described in the OpenTofu reference, and terraform/terraform.tfvars.example shows them with comments.To add more ranges to master_authorized_networks, add one entry per range.To verify, print the file and check that every value was filled in:
5

Apply OpenTofu

tofu init refuses to run without -backend-config. That’s deliberate: without it, state would land on one workstation, with no locking between operators.
Read the plan, then apply it. A saved plan contains the generated database passwords, so delete it afterwards.
To verify, list the outputs and connect kubectl to the new cluster:
Expected: the outputs include artifact_registry_repository, mongo_uri_command, redis_host and zitadel_dsn_secret_name, and kubectl lists all three CRDs. GKE ships and manages these CRDs; if they’re missing, turn on the cluster’s Gateway API:
If the apply prints a warning about the cluster’s GKE version and the Arm compute class, the control plane is older than 1.35.3-gke.1389000 and Arm pods won’t schedule until it’s upgraded.

Images and secrets

6

Copy the images into your Artifact Registry

Your cluster’s nodes pull images as their own identity, which can read your project’s Artifact Registry but not Jedify’s release registry. Copy Jedify’s three images into the repository OpenTofu created:
crane copy copies the whole multi-architecture index. If you use skopeo instead, pass --all: a single-architecture copy fails on the other kind of node with no match for platform in manifest.To verify, list the platforms each copy holds. Expected: linux/amd64 and linux/arm64 for every image.
The platform chart’s other images come from ghcr.io and docker.io. If your cluster can’t reach them, copy those too before step 12: see Airgapped installations.
7

Create the application configuration secret

One Secret Manager object holds the application’s configuration as a JSON object. Every key becomes an environment variable in every application component. This is the one place a person handles a credential.
The Zitadel setup script adds more keys to this object later. The secrets reference lists every key.To verify, list the keys (not the values). Expected: AUTH_PROVIDER, COMPONENT_AUTH_TOKEN, CREDS_KEY, LITELLM_PROXY_API_KEY.
8

Create the realtime credentials secret

Realtime runs in the cluster, and its credentials are a second Secret Manager object with three keys. The platform chart hands them to sockudo and to the application from this one object, so the two can’t drift apart.
app_id must be numeric. sockudo accepts any string, but the application’s realtime client rejects a non-numeric id when it starts, and that crashes every application component, not only the parts that publish events.
To verify, check app_id is digits only. Expected output: true.

Platform chart

9

Fill in the platform values

values/jedify-infra.yaml is the starting point. Fill each empty value from OpenTofu’s outputs and your variables:
Leave frontend.image.tag empty: it then follows the chart’s version. Leave frontend.zitadel empty for now; step 16 fills it.To verify, print what you set. Expected: no empty value, and zitadel.enabled is true.
10

Set the Gateway, TLS, egress and architecture choices

Decision. The defaults are an internal Gateway (gke-l7-rilb) that serves your own certificate from the Secret jedify-tls, egress masquerade off, and Arm nodes. Open each item below that differs from your choices in Requirements, and run only those commands.
Set CERT_MAP to your map’s name. A Google-managed certificate attaches only to the global class.
Each values file has one nodeSelector that every component reuses. Switch both:
Edit the files, not the command line: the components reuse these blocks through YAML aliases, which are resolved when the file is read, so --set nodeSelector... changes nothing. You also need node_architecture = "amd64" in terraform.tfvars (step 4).
To verify, print the choices:
11

Create the TLS Secret

Skip this step if you set gateway.certMap, or if cert-manager will issue the certificate.Otherwise, put your certificate (with its chain) and key into a Kubernetes TLS Secret named jedify-tls in the jedify namespace. It must cover both hostnames:
To verify, check the Secret’s type and the names the certificate covers. Expected: kubernetes.io/tls, and both $APP_HOST and $AUTH_HOST (or a wildcard that matches both).
Until this Secret exists, GKE doesn’t program the Gateway.
12

Install the platform chart: first pass

A first install is two passes with the same values. This pass installs External Secrets and its CRDs. Zitadel can’t be part of it: its setup jobs need secrets that External Secrets syncs, and the chart refuses a first install with Zitadel on rather than letting it hang.
Expected: Helm’s notes end with a banner telling you to run helm upgrade with the same values, the rollout completes, and both CRDs exist. The LiteLLM proxy and sockudo wait in CreateContainerConfigError until the second pass creates their secrets. That’s expected.
13

Install the platform chart: second pass

The second pass creates the secret wiring and installs Zitadel. Zitadel migrates its database in a setup job, which can take several minutes on a new cluster.
Every later upgrade of this chart is this command alone.To verify, check the secret store, the synced secrets and the workloads:
Expected: gcp-secretmanager is ready; app-secrets (in jedify-be), litellm-master-key, sockudo-app, zitadel-db, zitadel-masterkey and zitadel-bootstrap-admin all show SecretSynced; and every Deployment in jedify is ready. An ExternalSecret that isn’t ready names what it couldn’t read: usually a Secret Manager object that doesn’t exist, or a name that doesn’t match.
14

Point DNS at the Gateway

GKE gives the Gateway an address once it’s programmed, which can take several minutes:
Expected: PROGRAMMED is True and the ADDRESS column holds an IP. If it stays empty, see the Gateway isn’t programmed.Decision, outside the cluster. In your DNS, create A records for $APP_HOST and $AUTH_HOST pointing at $GATEWAY_IP. With the internal Gateway, put them in a zone your users resolve on your network.To verify, from a machine on that network:
Expected: both names return $GATEWAY_IP, and Zitadel answers 200. If your certificate comes from a private certificate authority your workstation doesn’t trust, add --cacert with that authority’s certificate to the curl command.

Zitadel

15

Run the Zitadel setup script

The chart installs Zitadel and its instance administrator. scripts/zitadel-setup.sh does the rest, in one run that is safe to repeat:
  • the organization your users belong to, and the jedify project with the role keys the application expects: Tenant Admin, Data expert and User;
  • the web app’s sign-in application, and the MCP server’s;
  • a login policy with self-registration off;
  • every Zitadel setting the application reads, written into your application configuration secret.
It reaches Zitadel through a kubectl port-forward, so it needs no DNS. It needs kubectl, gcloud, jq, yq, curl and openssl, and a gcloud identity that can read and add versions to ${PREFIX}-app-config.
Pass the same --org-name on every run, including the grant command later. The script finds the organization by name, so a different name creates a second organization.
You don’t need your identity provider yet. Run the script again with its --oidc-* options when your provider’s application exists: see Connect your identity provider.Expected: the script ends with ==> done and prints a frontend.zitadel block. To verify, list the keys now in the secret:
Expected, alongside the four keys from step 7: MCP_SERVER_URL, ZITADEL_ALLOW_INSECURE_HTTP, ZITADEL_API_URL, ZITADEL_AUDIENCES, ZITADEL_FE_CLIENT_ID, ZITADEL_HOST_HEADER, ZITADEL_ISSUER, ZITADEL_MCP_CLIENT_ID, ZITADEL_MCP_CLIENT_SECRET, ZITADEL_MCP_JWT_SIGNING_KEY, ZITADEL_ORG_ID and ZITADEL_PROJECT_ID.
16

Point the web app at your organization

The web app needs three identifiers the script just created. Read them from the secret, set them, and upgrade the platform chart:
orgId pins sign-in to your organization: Zitadel then refuses users from any other organization. See One organization.To verify, read the web app’s sign-in settings and load the page:
Expected: VITE_ZITADEL_CLIENT_ID, VITE_ZITADEL_PROJECT_ID and VITE_ZITADEL_ORG_ID hold values, VITE_ZITADEL_ISSUER is https:// followed by your Zitadel host, and the page answers 200.

Application chart

17

Fill in the application values

values/jedify-be.yaml carries the application’s non-secret settings as plain environment variables. Fill them from OpenTofu’s outputs:
MONGO_URI comes from a command, not a value, because Firestore reports its host only once the database exists. Use the printed URI as it is: it has no database path (the application refuses one), and it carries the parameters Firestore needs, including authMechanism=MONGODB-OIDC (the pods authenticate as their Google identity, with no password) and maxIdleTimeMS=60000.Leave ENV at production. The application accepts only local, ci, dev, staging and production, and any other value stops the API from starting. The values reference describes every variable in the file.To verify, render the chart and read the settings back. Expected: every line holds a value, and MONGO_URI starts with mongodb:// and ends with authMechanism=MONGODB-OIDC.
18

Install the application

The application is one chart installed as two releases: the components, and the queue workers. Both use values/jedify-be.yaml; the workers release adds values/jedify-be-workers.yaml, which selects what it renders.
Don’t skip the workers release. Without it, grounding jobs are queued successfully and nothing ever runs them, so dimension lookups return nothing instead of an error.
The first start is slow: each new node pulls a large image, and the components can take several minutes to import. Wait for every rollout:
Expected: every rollout reports successfully rolled out.
19

Verify the installation

Expected:
  • every pod is Running and ready, apart from CronJob pods, which show Completed;
  • the route shows Accepted=True and ResolvedRefs=True;
  • the API answers 200;
  • the MCP server’s discovery document names an issuer on your application host;
  • the API logs auth provider: zitadel.
Then check that the model proxy answers a real completion: see Verify the models.

Next steps

Your installation is running, but three things still stand between it and a working first question.

Connect your identity provider

Add your provider as the sign-in button, then grant the first roles.

Model gateway

Check every model name the application calls.

First run

The account record, CORS, your data warehouse and the first build.

Troubleshooting

If a check above didn’t pass.