At a glance
Get the release
Set the install variables
PREFIXnames every resource, including the Cloud Storage buckets<prefix>-storageand<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_EMAILmust be a real address. Reserved domains such as.testor.exampleare rejected later by the application’s own email validation.ORG_NAMEis the organization your users belong to. Feature flags match it in lowercase, so pick the name you’ll keep.
Pull the bundle and the charts
$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
Create the state bucket
True.Write terraform.tfvars
terraform/terraform.tfvars.example shows them with comments.master_authorized_networks, add one entry per range.To verify, print the file and check that every value was filled in: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.kubectl to the new cluster: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:1.35.3-gke.1389000 and Arm pods won’t schedule until it’s upgraded.Images and secrets
Copy the images into your Artifact Registry
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.ghcr.io and docker.io. If your cluster can’t reach them, copy those too before step 12: see Airgapped installations.Create the application configuration secret
AUTH_PROVIDER, COMPONENT_AUTH_TOKEN, CREDS_KEY, LITELLM_PROXY_API_KEY.Create the realtime credentials secret
app_id is digits only. Expected output: true.Platform chart
Fill in the platform values
values/jedify-infra.yaml is the starting point. Fill each empty value from OpenTofu’s outputs and your variables: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.Set the Gateway, TLS, egress and architecture choices
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.If you reserved an address for the Gateway
If you reserved an address for the Gateway
If you use an internet-facing Gateway with a Certificate Manager map
If you use an internet-facing Gateway with a Certificate Manager map
CERT_MAP to your map’s name. A Google-managed certificate attaches only to the global class.If you use a regional internet-facing Gateway
If you use a regional internet-facing Gateway
If pod addresses must never leave the VPC
If pod addresses must never leave the VPC
If you run amd64 nodes
If you run amd64 nodes
nodeSelector that every component reuses. Switch both:--set nodeSelector... changes nothing. You also need node_architecture = "amd64" in terraform.tfvars (step 4).Create the TLS Secret
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:kubernetes.io/tls, and both $APP_HOST and $AUTH_HOST (or a wildcard that matches both).Install the platform chart: first pass
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.Install the platform chart: second pass
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.Point DNS at the Gateway
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:$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
Run the Zitadel setup script
scripts/zitadel-setup.sh does the rest, in one run that is safe to repeat:- the organization your users belong to, and the
jedifyproject with the role keys the application expects:Tenant Admin,Data expertandUser; - 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.
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.--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: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.Point the web app at your organization
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: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
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.Install the application
values/jedify-be.yaml; the workers release adds values/jedify-be-workers.yaml, which selects what it renders.successfully rolled out.Verify the installation
- every pod is
Runningand ready, apart from CronJob pods, which showCompleted; - the route shows
Accepted=TrueandResolvedRefs=True; - the API answers
200; - the MCP server’s discovery document names an issuer on your application host;
- the API logs
auth provider: zitadel.