Install on GCP
Admin guide
Greenlight installs into a Google Cloud project you control. Plan on about 15–20 minutes for infrastructure provisioning and another 15–20 minutes for the install wizard.
This page covers Google Cloud. See Install on Azure or Install on AWS for the equivalent deployment in those clouds.
What Shift provides
Section titled “What Shift provides”Your Shift implementation contact delivers the approved release as a private, versioned greenlight-gcp-<version>.zip archive. It opens into one versioned directory holding install.sh, a README and troubleshooting guide written for the installing administrator, the LICENSE you install Greenlight under, the release manifest, and a deployment/ directory with the Terraform configuration, provider lock file, pinned Kubernetes assets, and the Greenlight chart. The script is part of the archive; IT does not write or download it separately. The public docs describe the resources and permissions without publishing the package or image access details.
What you need before you start
Section titled “What you need before you start”- A dedicated Google Cloud project with billing enabled, or one the installer creates. A project without billing can be linked by the installer to a billing account you may use, after one question; otherwise it shows the command and console page to pass to whoever can link one, and waits. Enter an existing project’s ID (for example
acme-greenlight), not the display name. Greenlight’s service accounts hold roles across the whole project, so anything else in it would be within their reach. The installer offers an existing project only when it holds nothing else, and a first install into a project that holds other resources stops before anything is created. A project with APIs turned on that a new project and Greenlight do not use is left out of the list too; if you name it anyway, the installer warns and goes on. - A provisioning identity with Project Owner on that project. To have the installer create the project instead, the identity needs the permissions in Creating the project, and owns the project it creates.
- A chosen region and a non-overlapping RFC 1918 range if the Greenlight VPC will peer with an existing network.
- Nothing for the GKE node zone — the install takes your region’s first zone that offers both machine types, records it, and pins every later run to it. Pass
--node-zoneonly if you need a particular zone. - A domain where you can delegate one subdomain, such as
greenlight.acme.com, to Cloud DNS. - An IT administrator who can configure the OIDC identity provider, delegate the DNS subdomain, install the Greenlight GitHub App, and become the initial Greenlight
org_admin. curl, plus bash 3.2 or newer, which is the/bin/bashmacOS already ships. You do not need the Google Cloud CLI, the GKE authentication plugin,terraform,helm,kubectl, orjq: the installer brings its own digest-checked copies, keeps them in a cache under your home directory, and uses them only for that run — nothing you installed yourself is replaced or relinked. If you already have the Google Cloud CLI orjqat a version the installer supports, it uses yours; it always uses its ownterraform,helm,kubectl, and GKE authentication plugin. Preparing that cache also usestar,unzip, andshasum, which macOS and Linux both ship. If you already sign in with your own Google Cloud CLI, nothing changes: the installer’s copy reads the same sign-in from your home directory. The installer checks what remains before it asks for anything and tells you how to install whatever is missing.
The installer turns on the Google Cloud service APIs it uses before it checks anything about your region, names each one it turned on, and lists them again on the review. Your team does not enable them one at a time. Turning an API on creates nothing billable.
What the install provisions
Section titled “What the install provisions”Everything below is created inside the dedicated Greenlight project. The installer prints what it will create, grouped in four layers, before its first question.
| Area | Resources |
|---|---|
| Network | A custom VPC, a GKE subnet with pod/service ranges, Private Service Access, and a Cloud Router with Cloud NAT, which carries all outbound traffic from the nodes |
| Compute | One GKE Standard cluster with a dedicated system node pool and an autoscaling app node pool, both running private nodes with no public IP address |
| Databases | Two private Cloud SQL for PostgreSQL instances: one for Greenlight and one for app-owned logical databases |
| Storage | Private, versioned Cloud Storage buckets for app objects, app assets, and platform storage, plus a versioned Terraform state bucket |
| Registry | One regional Docker Artifact Registry repository for Greenlight and app images |
| Secrets | Secret Manager entries for platform, database, integration, and app secrets |
| DNS and ingress | One Cloud DNS zone, a reserved ingress IP (external, or internal on a private install), Traefik, and cert-manager |
| Identity | Dedicated service accounts and Workload Identity Federation bindings for the control plane, certificates, data access, and deployed apps |
The public entry points are the Greenlight/app ingress and the authenticated GKE API, which you and the installer use from outside. The cluster’s own nodes and Greenlight’s services reach the GKE API at its internal endpoint inside the VPC, on every install. A private install has neither: its ingress is internal, and its GKE API has no public IP and is reached through GKE’s DNS-based endpoint, which checks every call against Google IAM. No node has a public IP address: outbound traffic such as image pulls and calls to GitHub leaves through Cloud NAT, and nothing on the internet can reach a node directly. Of the two halves of what Google calls a GKE private cluster, every install has the first, private nodes; a private install also has the second, a control plane with no public endpoint. Cloud SQL uses private IP, Cloud Storage blocks public access, and workloads use Workload Identity Federation instead of service-account keys.
If you want Greenlight to send notification email, provide an SMTP relay such as Google Workspace SMTP relay or another approved provider. Email remains disabled when no relay is configured.
Greenlight-managed capacity
Section titled “Greenlight-managed capacity”Greenlight selects and manages the infrastructure profile in the versioned deployment package. The values below are the standard installation footprint, sized for cost and quota review.
| Resource | Default |
|---|---|
| GKE system node pool | 1 × e2-standard-4 |
| GKE app node pool | e2-standard-4, autoscale 0–3 |
| Cloud SQL control plane | db-custom-2-8192, 32 GB SSD with automatic growth |
| Cloud SQL app databases | db-custom-2-8192, 32 GB SSD with automatic growth |
| Cloud Storage | Regional, versioned, public access prevented |
Greenlight manages workload autoscaling within this profile. After install, the app node pool’s node counts, and adding a node to Greenlight’s own pool, are changed from the Cluster capacity card on Internal Health (Upgrades).
One choice sets every size. --sizing-profile picks a whole footprint at once. standard is the table above. minimal suits test, dev and lightly used installs: a smaller 2 vCPU / 8 GiB machine for Greenlight’s own node (e2-standard-2), and an app pool that stops at one node.
./install.sh --sizing-profile minimalA profile sets starting sizes and ceilings. It never creates capacity up front, so your install still grows only when something needs it. You can name a single size alongside a profile and it wins for that setting alone.
Choose the profile when you first install. On an install that already exists, a profile that disagrees with the sizes it is running stops the run and tells you which single flag changes each one, rather than reporting success and leaving everything where it was. Naming the profile the install was created with is fine, so your original install command reruns as it was. If the installer cannot read the install’s recorded settings, for example because its cluster credentials did not arrive, any profile you name stops the run, since there is nothing to check it against. Drop --sizing-profile and change a size with its own flag instead.
Greenlight’s own pool can survive losing a node. It runs on one node by default. Two keeps Greenlight answering if a node is lost, at the cost of a second machine:
./install.sh --system-node-count 2Your cloud replaces a failed node in minutes and Greenlight’s state lives in its database, so one node is the right default for most installs. Raise it when an hour of downtime would matter more than the extra machine. On an install that already exists, the installer never falls back to that default when it cannot read the recorded settings or finds none: the run stops and asks for the node counts. Rerun it with access to the cluster, or pass --system-node-count, --app-node-min-count and --app-node-max-count yourself, along with --control-node-size and --app-node-size.
The app node pool’s size is yours to set. Its machine type and the autoscaler’s smallest and largest node counts are install settings:
./install.sh --app-node-size n2-standard-8 --app-node-min-count 1 --app-node-max-count 8The counts can be changed later; the machine type cannot. Change the smallest or largest node count from the Cluster capacity card on Internal Health; the updater applies it, and automatic updates replay your recorded sizing rather than resetting it to these defaults.
Raising the largest count raises a ceiling; it does not add a node. GKE’s autoscaler adds one only when an app pod cannot otherwise be scheduled. Lowering it below the nodes running now removes nodes your apps run on, so the Cluster capacity card accepts that only when their apps fit on the nodes that stay, and otherwise says there is not enough room and to remove some apps first.
Apps prefer the app node pool and share the node that runs Greenlight itself when the pool has no room, so a small installation stays on one machine and your first app does not buy a second. Greenlight’s own services outrank apps on that shared node, so a busy app cannot crowd them out. It is moved to the app node pool instead, and the autoscaler adds a node for it.
The machine type is settled at install. Changing it afterwards means replacing the node pool — a manual maintenance-window operation your Greenlight contact will walk you through, not an installer rerun. Do not pass --app-node-size to a rerun against a running installation.
Do not edit node pools or database tiers directly — the next Greenlight update replays your recorded settings and undoes the edit.
Required access
Section titled “Required access”| Identity | Access | Why |
|---|---|---|
| Provisioning identity | Project Owner on the dedicated Greenlight project | Enables services, creates resources, and creates IAM bindings |
gl-runner |
Project- or resource-scoped access to GKE, Cloud SQL, Secret Manager, Storage, Artifact Registry, and the delegated DNS zone, plus conditional project IAM administration restricted to granting a fixed set of app roles | Operates the control plane, provisions app resources, and grants each app’s service account only its own resources’ roles |
greenlight-push |
Artifact Registry writer on the Greenlight repository, nothing else | Default-branch builds of your Greenlight app repositories impersonate it through GitHub’s OIDC token and the gl-push Workload Identity pool to push their images; Greenlight binds only your GitHub organization, by its numeric id |
gl-cert-manager |
DNS administrator on the delegated zone only | Completes DNS-01 certificate challenges |
gl-data |
No project IAM roles by default | Receives access inside a connected database only when a DBA grants it |
gl-updater |
Infrastructure access within the dedicated Greenlight project, including the install’s own image repository and the push identity’s Workload Identity pool | Applies Greenlight releases inside the maintenance window |
The installer creates the runtime service accounts and their Workload Identity bindings. Your team does not create or distribute service-account JSON keys. If an organization policy restricts Workload Identity pool providers (constraints/iam.workloadIdentityPoolProviders), allow https://token.actions.githubusercontent.com for the Greenlight project. Delete protection guards the databases, buckets, DNS zone, and the cluster and its node pools, so no automatic update can remove or replace them.
What the installer checks before anything is created
Section titled “What the installer checks before anything is created”On an interactive first install, as soon as the project is chosen, the installer asks Google Cloud which of the permissions below your Application Default Credentials hold on the project (testIamPermissions). Those are the credentials Terraform creates everything with, which can differ from the account gcloud itself is signed in as. The list ships in the release archive as first-install-permissions.json.
| Permission | Granted by |
|---|---|
serviceusage.services.enable |
roles/serviceusage.serviceUsageAdmin |
resourcemanager.projects.setIamPolicy |
roles/resourcemanager.projectIamAdmin |
iam.serviceAccounts.create |
roles/iam.serviceAccountAdmin |
iam.serviceAccounts.setIamPolicy |
roles/iam.serviceAccountAdmin |
iam.workloadIdentityPools.create |
roles/iam.workloadIdentityPoolAdmin |
storage.buckets.create |
roles/storage.admin |
compute.networks.create |
roles/compute.networkAdmin |
compute.subnetworks.create |
roles/compute.networkAdmin |
compute.routers.create |
roles/compute.networkAdmin |
compute.addresses.create |
roles/compute.networkAdmin |
compute.globalAddresses.create |
roles/compute.networkAdmin |
servicenetworking.services.addPeering |
roles/servicenetworking.networksAdmin |
container.clusters.create |
roles/container.admin |
cloudsql.instances.create |
roles/cloudsql.admin |
secretmanager.secrets.create |
roles/secretmanager.admin |
dns.managedZones.create |
roles/dns.admin |
artifactregistry.repositories.create |
roles/artifactregistry.admin |
cloudquotas.quotas.update |
roles/cloudquotas.admin (optional: only for the installer to request CPU quota for you) |
A missing permission is named at once, with a role that grants it. The questions go on so your answers are kept; once they are answered, and before anything is created, you can check again after the role is granted, sign in again and check, or stop. If someone else has to run the install, they run it on their own computer. If the cloud cannot answer exactly — the identity may not run the check, or the read fails — the installer says it could not check and continues. Updates and --non-interactive runs skip the check: the updater’s identity is deliberately narrower than a first install’s.
Creating the project
Section titled “Creating the project”On an interactive install, the project question lists the projects your account can see and offers Create a new project for Greenlight. Choosing it asks for the new project’s ID (a suggested one is filled in; IDs are unique across all of Google Cloud), the organization or folder it goes in (listed from the organizations your account can see and the folders directly under them; type folders/ID for any other folder, or choose No organization or folder), and the open billing account it is charged to, each from what your account can see (type a billing account ID, such as 0A0A0A-111111-222222, for one your account cannot list). Before the review, the installer asks Google Cloud whether the signed-in gcloud account may create a project in that parent and link that billing account:
| Permission | Granted by | Checked on |
|---|---|---|
resourcemanager.projects.create |
roles/resourcemanager.projectCreator | The organization or folder |
billing.resourceAssociations.create |
roles/billing.user | The billing account |
A missing one is named at once, with the same check again, sign in again, or stop choices as the project check. Owning the project it creates gives your account everything on the list above. The parent question is asked even when your account can list no organization, so an account granted Project Creator on a single folder can type that folder. With no organization or folder there is nothing to check resourcemanager.projects.create on, so the installer says it could not check it and Google Cloud decides at creation: an account in a Google Workspace or Cloud Identity organization is refused a project without a parent. The same holds for a typed folder Google Cloud will not answer for; the billing account is still checked.
Nothing is created until you confirm the review, which shows the new project with its parent and billing account. The installer then runs gcloud projects create, gcloud billing projects link, and gcloud services enable for the APIs the install uses, and the install continues in the new project. If the run stops after that, the project stays: the stop names it, and rerunning with --project and its ID continues the install there, or gcloud projects delete removes it. --non-interactive runs never create a project; --project must name one that exists.
What the installer runs
Section titled “What the installer runs”These are all the commands the installer can run against your cloud, the new cluster, or the computer it runs on, with when each one runs and why. The same list ships in the release archive as commands.json, and the release’s tests fail if the installer runs a cloud or cluster command that is not on it. ./install.sh --list-commands prints it without signing in to anything. On an interactive run, the installer shows the changes on this list before its first question, and the review shows the commands this run will execute with your values filled in. A word in angle brackets stands for a value your answers or the deployment supply, and brackets mark arguments a run adds only sometimes.
The list is a record of what the installer does, not steps to run yourself: the installer runs each command, and never asks you to run one of its own steps and come back.
Every command the installer can run
Reads before any confirmation
Section titled “Reads before any confirmation”gcloud version --format=json— reads · this computer · every run. Reads the gcloud version, for the dependency check.terraform version -json— reads · this computer · every run. Reads the Terraform version, for the dependency check.gcloud auth list --filter=status:ACTIVE --format=value(account)— reads · this computer · every run. Reads which Google account gcloud is signed in as.gcloud auth application-default print-access-token— reads · this computer · every run. Checks that Application Default Credentials, the identity Terraform creates with, can sign in; on a first install the short-lived token is passed on standard input to the permission check and never written down.gcloud auth login --update-adc— changes · this computer · interactive runs. Signs you in to Google Cloud when gcloud has no signed-in account; the sign-in is gcloud’s own flow.gcloud auth application-default login— changes · this computer · interactive runs. Signs in Application Default Credentials when gcloud is signed in but they are not.gcloud config get-value project— reads · this computer · interactive runs. Reads gcloud’s active project, offered as the default.gcloud projects list --format=value(projectId,name)— reads · your cloud · interactive runs. Lists the projects your account can see, for the project question.gcloud --quiet services list --enabled --project <projectId> --format=value(config.name)— reads · your cloud · every run. Lists the APIs a project has on: to turn on any the install uses that are off, and to check a project holds nothing but what Greenlight uses, for each project the project question offers and for the project a first install names.gcloud --quiet asset search-all-resources --scope=projects/<projectId> --billing-project=<projectId> --format=value(assetType,displayName)— reads · your cloud · first installs. Lists what a project holds through Cloud Asset Inventory, where its API is on, to check it holds nothing Greenlight does not create.gcloud --quiet compute instances list --project <projectId> --format=value(name)— reads · your cloud · first installs. Lists a project’s virtual machines, where Cloud Asset Inventory is off and Compute Engine is on, to check it holds nothing Greenlight does not create.gcloud --quiet container clusters list --project <projectId> --format=value(name,location)— reads · your cloud · first installs. Lists a project’s GKE clusters, where Cloud Asset Inventory is off and GKE is on, to check it holds nothing Greenlight does not create.gcloud --quiet sql instances list --project <projectId> --format=value(name)— reads · your cloud · first installs. Lists a project’s Cloud SQL instances, where Cloud Asset Inventory is off and Cloud SQL is on, to check it holds nothing Greenlight does not create.gcloud auth print-access-token— reads · this computer · interactive runs. Hands the permission checks (for a new project, and for linking billing to a project that has none) and the Cloud Quotas requests a short-lived token for the gcloud CLI’s sign-in; the token is passed on standard input and never written down.gcloud --quiet organizations list --format=value(name.basename(),displayName)— reads · your cloud · interactive runs. Lists the organizations your account can see, for the new project’s parent question.gcloud --quiet resource-manager folders list --organization <organization> --format=value(name.basename(),displayName)— reads · your cloud · interactive runs. Lists the folders directly under each of those organizations, for the same question.gcloud --quiet billing accounts list --filter=open=true --format=value(name.basename(),displayName)— reads · your cloud · interactive runs. Lists the open billing accounts your account can see, for the new project’s billing question, and to offer a link to a project that has no billing.gcloud --quiet projects describe <projectId>— reads · your cloud · every run. Checks the project exists and your account can read it.gcloud --quiet billing projects describe <projectId> --format=value(billingEnabled)— reads · your cloud · every run. Checks the project is linked to an open billing account.gcloud container clusters list --project <projectId> --filter=<filter> --format=value(<field>)— reads · your cloud · every run. Looks for an existing Greenlight cluster in the project, and reads its location and API endpoint settings.gcloud compute routes list --project <projectId> --filter=name=route-egress-via-firewall --format=value(name)— reads · your cloud · private installs. Reads whether the network already sends its egress through your firewall, on a rerun that reads no recorded settings.gcloud --quiet container node-pools list --project <projectId> --cluster <cluster> --region <region> --format=value(name,status)— reads · your cloud · only to finish a run that stopped part way. Reads whether the first install Google Cloud refused for capacity on the last run left a node pool in an error state.gcloud container clusters get-credentials <cluster> --region <region> --project <projectId> [--dns-endpoint]— changes · this computer · updates. Writes credentials for the existing cluster into this run’s own kubeconfig file, not yours, to read the settings it recorded.gcloud --quiet services list --enabled --project <projectId> --filter=config.name=compute.googleapis.com --format=value(config.name)— reads · your cloud · every run. Checks an API is on: Compute Engine, which the catalog reads below need, and Cloud Quotas before a quota request.gcloud --quiet compute regions list --project <projectId> --filter=status=UP --format=value(name)— reads · your cloud · every run. Lists the project’s regions that are up, for the region question’s list of those that can host this release, and for a stop to name them.gcloud --quiet sql tiers list --project <projectId> --format=json— reads · your cloud · every run. Reads which Cloud SQL tiers each region offers, to check the region can host Greenlight’s databases.gcloud --quiet container get-server-config --project <projectId> --region <region> --format=json— reads · your cloud · every run. Reads which Kubernetes versions each region’s REGULAR channel offers: on a first install to check the region offers the one this release runs, and on an update to move the cluster only to a version the channel offers.gcloud container clusters describe <cluster> --location <location> --project <projectId> --format=value(currentMasterVersion)— reads · your cloud · updates. Reads the Kubernetes version an existing cluster runs, so an update steps it up one minor at a time toward the one this release runs, and never down.gcloud --quiet compute zones list --project <projectId> --filter=<filter> --format=value(name)— reads · your cloud · every run. Lists the region’s zones, to check the node zone and machine types.gcloud --quiet compute machine-types list --project <projectId> --zones <zone> --format=json— reads · your cloud · every run. Reads which machine types each zone offers, for the node sizes.gcloud --quiet compute regions describe <region> --project <projectId> --format=json— reads · your cloud · every run. Reads the region’s CPU and address quotas, to check the cluster fits.gcloud --quiet compute project-info describe --project <projectId> --format=json— reads · your cloud · every run. Reads the project’s global quotas, to check the cluster fits.curl -sS --max-time 30 -X GET -H @- -w <format> https://cloudquotas.googleapis.com/v1/projects/<projectId>/locations/global/services/compute.googleapis.com/quotaInfos?pageSize=500[&pageToken=<token>]— reads · your cloud · interactive runs. Finds the Cloud Quotas id of the CPU quota to raise, only after you choose to request it at the quota question.curl -sS --max-time 30 -X GET -H @- -w <format> https://cloudquotas.googleapis.com/v1/projects/<projectId>/locations/global/quotaPreferences?pageSize=500[&pageToken=<token>]— reads · your cloud · interactive runs. Finds the preference the project already holds for the quota, since Cloud Quotas keeps one per quota and region, only after you choose to request quota at the quota question.curl -sS --max-time 30 -X PATCH -H @- --data <body> -w <format> https://cloudquotas.googleapis.com/v1/projects/<projectId>/locations/global/quotaPreferences/<preference>?updateMask=quotaConfig.preferredValue,justification,contactEmail— changes · your cloud · interactive runs. Raises the preference the project already holds for the quota, rather than filing a second, only after you choose to request quota at the quota question. Named on the review once granted.curl -sS --max-time 30 -X POST -H @- --data <body> -w <format> https://cloudquotas.googleapis.com/v1/projects/<projectId>/locations/global/quotaPreferences?quotaPreferenceId=<preference>— changes · your cloud · interactive runs. Asks Google Cloud to raise a CPU quota the install needs, through the Cloud Quotas API as your gcloud sign-in, only after you choose to request it at the quota question. Your account is the request’s contact. Named on the review once granted.curl -sS --max-time 30 -X GET -H @- -w <format> https://cloudquotas.googleapis.com/v1/projects/<projectId>/locations/global/quotaPreferences/<preference>— reads · your cloud · interactive runs · repeats until ready. Waits for Google Cloud’s answer to the quota request, up to ten minutes.gcloud container node-pools list --project <projectId> --region <region> --cluster <cluster> --format=json— reads · your cloud · updates. Reads the application pool’s bounds on an update that did not read its recorded settings, so quota is checked only for nodes the run adds.gcloud compute instances list --project <projectId> --filter=<filter> --format=value(name)— reads · your cloud · updates. Counts the system pool’s running nodes on an update that did not read its recorded settings, so quota is checked only for nodes the run adds.kubectl version --client -o json— reads · this computer · every run. Reads the kubectl version, for the dependency check.helm version --template {{.Version}}— reads · this computer · every run. Reads the Helm version, for the dependency check and the flags its major version takes.kubectl get configmap greenlight-install-inputs --namespace greenlight-updater [--ignore-not-found] -o go-template=<template>— reads · the new cluster · updates. Reads the settings this installation recorded, so an update reuses them.
Changes before the review, each named as it runs
Section titled “Changes before the review, each named as it runs”gcloud --quiet billing projects link <projectId> --billing-account=<billingAccount>— changes · your cloud · interactive runs. Links an open billing account to a project that has none, after you choose that account at the billing question. Named as it runs and on the review.gcloud --quiet services enable --project <projectId> <api> [<api>]— changes · your cloud · only when an API the install uses is off. Turns on an API the install uses that is off, before anything about the region is checked: its catalog answers only once it is on, and turning an API on creates nothing billable. Named as it runs and on the review.
After the review’s yes
Section titled “After the review’s yes”gcloud --quiet container node-pools delete <nodePool> --project <projectId> --cluster <cluster> --region <region>— changes · your cloud · only to finish a run that stopped part way. Deletes that node pool; Terraform never replaces a node pool. It is asked first and named on the review, and it never ran Greenlight.gcloud --quiet projects create <projectId> --name=Greenlight [--folder <folder>] [--organization <organization>]— changes · your cloud · interactive runs. Creates the new project when you chose to create one, in the organization or folder you chose.gcloud --quiet billing projects link <projectId> --billing-account=<billingAccount>— changes · your cloud · interactive runs. Links the billing account you chose to the new project.gcloud --quiet services enable container.googleapis.com sqladmin.googleapis.com secretmanager.googleapis.com artifactregistry.googleapis.com dns.googleapis.com storage.googleapis.com servicenetworking.googleapis.com iamcredentials.googleapis.com compute.googleapis.com iam.googleapis.com cloudresourcemanager.googleapis.com sts.googleapis.com --project <projectId>— changes · your cloud · interactive runs. Turns on the Google Cloud APIs the install uses in the new project.gcloud storage buckets describe gs://<stateBucket> --project=<projectId>— reads · your cloud · every run. Checks whether the Terraform state bucket exists.gcloud storage buckets create gs://<stateBucket> --project=<projectId> --location=<region> --uniform-bucket-level-access --public-access-prevention— changes · your cloud · first installs. Creates the private bucket that holds this installation’s Terraform state.gcloud storage buckets update gs://<stateBucket> --project=<projectId> --versioning— changes · your cloud · every run. Turns on versioning for the state bucket, so an earlier state can be recovered.terraform -chdir=<iacDir> init -input=false -backend-config=bucket=<stateBucket> -backend-config=prefix=<statePrefix>— changes · this computer · every run. Prepares Terraform in the archive’s Google Cloud directory, with its state in that bucket.terraform -chdir=<iacDir> state list— reads · your cloud · every run. Lists what this installation’s state already holds, to check the state describes this installation.terraform -chdir=<iacDir> state pull— reads · your cloud · only to finish a run that stopped part way. Reads the remote state, to compare it with a state file a stopped run left behind.terraform -chdir=<iacDir> state push <stateFile>— changes · your cloud · only to finish a run that stopped part way. Uploads the state file a stopped run left behind, so the next plan knows what it created.terraform -chdir=<iacDir> state show -no-color <address>— reads · your cloud · only to finish a run that stopped part way. Reads one resource from state, to name it in a stop or announce the DNS zone again.gcloud container operations list --project <projectId> --region <region> --filter status=RUNNING OR status=PENDING --format value(operationType,status,targetLink.scope(clusters))— reads · your cloud · updates · repeats until ready. Checks no cluster operation is already running, which would make the apply fail.terraform -chdir=<iacDir> plan -input=false -var-file=<varsFile> -out=<planFile>— reads · your cloud · every run. Plans the change; the preview you confirm is this plan, and the variables file is generated from the review’s settings.terraform -chdir=<iacDir> show -json <planFile>— reads · this computer · interactive runs. Reads the plan back, to count what it creates, changes, and destroys.kubectl get svc -n traefik traefik --ignore-not-found -o jsonpath=<field>— reads · the new cluster · private installs. Reads whether the routing layer’s load balancer is still public, before switching an installation to private.
After the preview’s yes
Section titled “After the preview’s yes”terraform -chdir=<iacDir> apply -input=false <planFile>— changes · your cloud · every run. Applies exactly the plan you confirmed. This one command creates most of the installation: the resources the overview lists, with the service accounts and role bindings below.gl-runner, for the control plane, through workload identity: roles/container.admin on the project; roles/cloudsql.admin on the project; roles/cloudsql.instanceUser on the project; roles/secretmanager.admin on the project; roles/iam.serviceAccountAdmin on the project; roles/resourcemanager.projectIamAdmin on the project; roles/storage.objectAdmin on the installation’s buckets; roles/storage.admin on the apps’ object bucket; roles/iam.serviceAccountTokenCreator on itself; roles/artifactregistry.repoAdmin on the image repository; roles/dns.admin on the installation’s DNS zonegl-updater, for the updater, through workload identity: roles/compute.networkAdmin on the project; roles/compute.securityAdmin on the project; roles/container.admin on the project; roles/cloudsql.admin on the project; roles/dns.admin on the project; roles/secretmanager.admin on the project; roles/iam.serviceAccountAdmin on the project; roles/resourcemanager.projectIamAdmin on the project; roles/serviceusage.serviceUsageAdmin on the project; roles/storage.admin on the project; roles/artifactregistry.admin on the project; roles/iam.workloadIdentityPoolAdmin on the project; roles/storage.objectAdmin on the Terraform state bucketgl-cert-manager, for cert-manager, through workload identity: roles/dns.admin on the installation’s DNS zonegl-data, for the control plane’s data access, through workload identity: roles/iam.workloadIdentityUser on itself, for the control planegke-node-greenlight, for the cluster’s nodes: roles/logging.logWriter on the project; roles/monitoring.metricWriter on the project; roles/monitoring.viewer on the project; roles/artifactregistry.reader on the image repositorygreenlight-push, for app image builds, through GitHub’s workload identity federation: roles/artifactregistry.writer on the image repository
gcloud dns managed-zones describe <zone> --project <projectId> --format=json— reads · your cloud · first installs. Reads the DNS zone’s name servers the minute the zone exists, so you can delegate it while the cluster is still being created.terraform -chdir=<iacDir> output -raw <output>— reads · your cloud · every run. Reads one value the apply produced, such as the cluster name or a database address.terraform -chdir=<iacDir> output -json <output>— reads · your cloud · every run. Reads a list the apply produced, such as the DNS zone’s name servers.gcloud container clusters get-credentials <cluster> --region <region> --project <projectId> [--dns-endpoint]— changes · this computer · every run. Writes credentials for the new cluster into this run’s own kubeconfig file, not yours.kubectl config view --minify -o jsonpath=<field>— reads · this computer · private installs. Reads which API address this run’s kubeconfig points at.kubectl config set-cluster <cluster> --server <address>— changes · this computer · private installs. Points this run’s kubeconfig at the cluster’s private API address.kubectl get --raw /readyz— reads · the new cluster · private installs. Checks the cluster answers on its private address before the public one is turned off.gcloud container clusters update <cluster> --region <region> --project <projectId> --enable-private-endpoint --enable-master-authorized-networks --no-enable-google-cloud-access --quiet— changes · your cloud · private installs. Turns off the cluster’s public API endpoint on a private install, once the private one answers.kubectl delete svc -n traefik traefik --ignore-not-found --wait=true --timeout=5m— changes · the new cluster · private installs. Removes the public load balancer when switching an installation to private; the release recreates it internal.gcloud dns record-sets delete <recordName> --type=A --zone=<zone> --project=<projectId>— changes · your cloud · private installs. Removes the public DNS records when switching an installation to private.kubectl get -n traefik serviceattachment/greenlight [-o jsonpath=<field>]— reads · the new cluster · private installs. Reads the private-link publication, to print its address or remove it before the change that replaces it.kubectl wait --for=delete -n traefik serviceattachment/greenlight --timeout=5m— reads · the new cluster · private installs. Waits until the private-link publication is removed.gcloud container clusters describe <cluster> --location <location> --project <projectId> --format=value(currentMasterVersion)— reads · your cloud · every run. Reads the Kubernetes version the cluster runs, recorded with your settings as a fact the review shows.gcloud sql instances describe <instance> --project <projectId> --format=value(databaseVersion)— reads · your cloud · every run. Reads the PostgreSQL major the database server runs, recorded with your settings as a fact the review shows.
Inside the new cluster
Section titled “Inside the new cluster”kubectl get service kubernetes --namespace default -o jsonpath=<field>— reads · the new cluster · private installs. Reads the cluster’s API address as pods reach it, for the outbound network report.kubectl config view --raw— reads · this computer · every run. Merges this run’s cluster credentials with your own kubeconfig file, which it never edits.kubectl create namespace <namespace> --dry-run=client -o yaml— reads · this computer · every run. Writes out a namespace manifest for the apply below; creates nothing.kubectl apply -f -— changes · the new cluster · every run. Applies the manifests the installer renders: the namespaces greenlight-system, greenlight-updater, traefik and cert-manager, the updater’s database Secret, and the Let’s Encrypt cluster issuers.kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/<version>/standard-install.yaml— changes · the new cluster · every run. Installs the Kubernetes Gateway API resource definitions the routing layer uses.kubectl apply -f <manifest>— changes · the new cluster · every run. Applies the archive’s prerequisite manifests: the platform priority class, and on AWS private installs the bootstrap network policies.kubectl create service clusterip greenlight-admission-probe --namespace traefik --tcp=<port> --dry-run=server -o name— reads · the new cluster · every run · repeats until ready. Rehearses creating a Service, as a server-side dry run, until the add-ons’ admission webhooks answer.kubectl get -n traefik <resource> --ignore-not-found -o name— reads · the new cluster · private installs. Checks for the private-link publication that pins the routing layer’s load balancer before changing it.kubectl get svc -n traefik traefik -o json— reads · the new cluster · updates. Reads the routing layer’s live Service, to refuse a load balancer change a private-link publication would not survive.helm template traefik traefik --repo https://traefik.github.io/charts --version <version> --namespace traefik --values <valuesFile> [--set <chartValue>=<value>] [--set-string <chartValue>=<value>] --show-only templates/service.yaml— reads · this computer · updates. Renders the routing layer’s Service as this release would install it, to compare with the live one.kubectl create --dry-run=client -o json -f -— reads · this computer · updates. Normalizes that rendered Service for the comparison; creates nothing.helm upgrade --install traefik traefik --repo https://traefik.github.io/charts --version <version> --namespace traefik --create-namespace --values <valuesFile> [--values <cloudValuesFile>] [--set <chartValue>=<value>] [--set-string <chartValue>=<value>] --wait --timeout 5m— changes · the new cluster · every run. Installs Traefik, the routing layer, and its load balancer.kubectl wait --for=condition=Accepted gatewayclass/traefik --timeout=120s— reads · the new cluster · every run. Waits until Traefik accepts the gateway class.helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager --namespace cert-manager --create-namespace --version <version> --set <chartValue>=<value> [--values <valuesFile>] [--server-side=true] [--force-conflicts] --wait --timeout 5m— changes · the new cluster · every run. Installs cert-manager, which requests and renews the HTTPS certificate.kubectl wait --for=condition=Ready clusterissuer/letsencrypt-prod --timeout=120s— reads · the new cluster · every run. Waits until the Let’s Encrypt issuer is ready.helm get values greenlight --namespace greenlight-system -o json— reads · the new cluster · updates. Reads the running release’s settings, to refuse replacing an updater-managed release with one that names no release.kubectl create secret generic <secret> --namespace <namespace> --from-file=<key>=<file> --dry-run=client -o yaml— reads · this computer · every run. Writes out the updater’s database credential and certificate authority as Secret manifests for the apply; the files are read from the installer’s private work directory.kubectl get configmap greenlight-release-validation-state -n greenlight-system --ignore-not-found -o jsonpath={.data.enabled}— reads · the new cluster · updates. Reads whether release validation is on, so an update keeps the setting.kubectl get configmap greenlight-bootstrap-route-state -n greenlight-system --ignore-not-found -o jsonpath={.data.closed}— reads · the new cluster · every run. Reads whether the setup wizard’s bootstrap address has closed, so the release does not reopen it.helm status greenlight --namespace greenlight-system— reads · the new cluster · every run. Checks whether the Greenlight release is already installed.helm upgrade --install greenlight <chart> --namespace greenlight-system --create-namespace --set <chartValue>=<value> --values <cloudValuesFile> [--set-string <chartValue>=<value>] [--values <pullSecretValuesFile>] [--reset-then-reuse-values] --dry-run=server— reads · the new cluster · every run. Renders the Greenlight release against the cluster as a dry run, so a value the chart rejects stops the run before anything changes.helm upgrade --install greenlight <chart> --namespace greenlight-system --create-namespace --set <chartValue>=<value> --values <cloudValuesFile> [--set-string <chartValue>=<value>] [--values <pullSecretValuesFile>] [--reset-then-reuse-values] [--atomic] [--rollback-on-failure] --wait --timeout 10m— changes · the new cluster · every run. Installs or upgrades the Greenlight release, and rolls it back if it does not become healthy.kubectl get deployments -n greenlight-system -l app.kubernetes.io/part-of=greenlight -o <format>— reads · the new cluster · every run · repeats until ready. Lists the control plane’s deployments and how many replicas are still starting.kubectl get deployments -n greenlight-system -o json -l app.kubernetes.io/part-of=greenlight— reads · the new cluster · every run · repeats until ready. Reads how many control-plane replicas are still starting.kubectl rollout status deployment/<deployment> -n greenlight-system --timeout=5m— reads · the new cluster · every run. Waits for each control-plane deployment to finish rolling out.kubectl get svc -n traefik traefik -o jsonpath=<field>— reads · the new cluster · every run · repeats until ready. Reads the load balancer’s address, until the cloud assigns one.kubectl exec -i --namespace greenlight-system deployment/greenlight --container server -- node --input-type=module - <url> [--connect <address>]— reads · the new cluster · every run · repeats until ready. Requests the control plane’s health address from inside the cluster, for a private install or when this computer cannot reach the load balancer.kubectl wait --for=condition=Ready certificate/greenlight-control-plane-tls -n greenlight-system --timeout=<seconds>— reads · the new cluster · updates. Waits for the HTTPS certificate to be issued.kubectl get certificate/greenlight-control-plane-tls -n greenlight-system -o jsonpath=<field>— reads · the new cluster · updates. Reads whether the HTTPS certificate is ready.kubectl get ingress -n greenlight-system -o name— reads · the new cluster · every run. Stops the run if an Ingress left from an older routing layer would answer for the control plane.kubectl version— reads · the new cluster · only to explain a step that failed. Checks the cluster answers before printing routing evidence for a failed step.kubectl get gatewayclass— reads · the new cluster · only to explain a step that failed. Prints the gateway classes, as routing evidence for a failed step.kubectl get gateway -n greenlight-system -o wide— reads · the new cluster · only to explain a step that failed. Prints the gateway, as routing evidence for a failed step.kubectl describe gateway greenlight-gateway -n greenlight-system— reads · the new cluster · only to explain a step that failed. Prints the gateway’s conditions, as routing evidence for a failed step.kubectl get httproute -n greenlight-system— reads · the new cluster · only to explain a step that failed. Prints the HTTP routes, as routing evidence for a failed step.kubectl get ingress -A— reads · the new cluster · only to explain a step that failed. Prints every Ingress, as routing evidence for a failed step.helm list -A— reads · the new cluster · only to explain a step that failed. Prints the installed Helm releases, as routing evidence for a failed step.helm version --short— reads · this computer · only to explain a step that failed. Prints the Helm version, as evidence for a failed step.kubectl create configmap greenlight-install-inputs --namespace greenlight-updater --from-literal=<setting>=<value> --dry-run=client -o yaml— reads · this computer · every run. Writes out the run’s settings as a ConfigMap manifest for the apply, so the updater and the next run reuse them; no secret is among them.kubectl get configmap greenlight-install-inputs --namespace greenlight-updater -o go-template=<template>— reads · the new cluster · every run. Reads the recorded settings back, to confirm they were stored.
Region, machine types, database tier, and quota
Section titled “Region, machine types, database tier, and quota”Before the review, the installer checks that the region can host this release and that your project can create what the install needs there:
- Region. An interactive first install lists your project’s regions that run Cloud SQL for PostgreSQL and offer machine types of the shape each node needs, grouped by continent. A region you type or pass with
--regionis checked the same way, and one that fails stops, naming the regions that pass. - Machine types and zone. The nodes go in the region’s first zone that offers both machine types. If the release’s default (
e2-standard-4) is not offered there or your CPU quota cannot cover it, the installer switches to an equal 4 vCPU / 16 GB type from the release’s list (n2-standard-4, thenn2d-standard-4) and tells you which. A machine type you name yourself is never swapped. - Database tier. The release uses
db-custom-2-8192(2 vCPUs, 8 GB) wherever the region runs Cloud SQL’s Enterprise edition. A tier you pass with--database-sizemust be a custom shape Cloud SQL accepts or an Enterprise tier the region offers; otherwise the run stops before anything is created. It is never swapped. - Kubernetes version. The release runs one Kubernetes minor on GKE (today 1.36), as the cluster’s minimum version under GKE’s Regular release channel, which applies patches. A first install checks the region’s Regular channel offers it; an existing cluster more than one version behind moves one version per run, and one already ahead stays where it is.
- CPU quota. Every node the install starts is counted against the region’s CPU quota for the machine family and against the project’s quota across all regions. Quota short of the application pool’s maximum is a warning, not a stop.
More detail: when the project is out of CPU quota
- On an interactive install, the installer checks quota as soon as you answer the region question. If no machine type the release can use fits, it names the quota (for example CPUs or N2 CPUs), how many CPUs the install needs, and how many are free, then asks: choose another region, ask Google Cloud to raise the quota, or stop. Nothing has been created at that point.
- Ask Google Cloud to raise the quota files a quota preference through the Cloud Quotas API under your sign-in, with your account as the contact, then waits up to ten minutes for Google Cloud’s answer. If the project already holds a preference for that quota, from an earlier request in the console, the installer raises that one instead, since Google Cloud keeps one per quota and region. If it is granted, the install continues and the review lists the raised quota. The installer turns on the Cloud Quotas API the first time it files a request, and names that on the review too.
- If the request is still pending, or Google Cloud grants less than asked, the installer says so with the preference’s ID, then asks again. A later run reads the quota again before asking anything.
- Requesting quota needs the optional
cloudquotas.quotas.updatepermission (roles/cloudquotas.admin). Without it, the installer stops, naming the permission and the role. - A
--non-interactiverun never asks. It stops, naming the quota and the CPUs needed. - On a rerun of an existing installation, quota is checked only for nodes the run adds. A raised system node count or app pool minimum is checked before anything changes, and a rerun that adds no nodes is never refused on quota.
- A machine type or database tier the installation already runs that the region no longer offers stops the update, naming the equal types the region does offer: changing it replaces the node pool or resizes the database, which you schedule yourself.
Install inputs
Section titled “Install inputs”The installing administrator supplies only values specific to the customer’s environment:
| Input | Required | Purpose |
|---|---|---|
--project |
Yes | Dedicated Google Cloud project ID |
--region |
Yes | Region identifier where Greenlight resources are created, such as us-central1 — not a zone such as us-central1-a |
--control-plane-host |
Yes | Delegated DNS zone and final Greenlight hostname |
--certificate-contact |
No | Contact address on the certificate issuer. Derived from your host name when omitted |
--network-range |
Only when needed | Replaces the package default when it overlaps a peered network |
--node-zone |
Only when needed | Pins GKE nodes to one zone instead of the region’s first zone. Must be a zone in --region |
--database-size |
No | Cloud SQL tier for both Greenlight databases, such as db-custom-2-8192. Chosen from the region when omitted |
--access-profile |
No | public (the default) or private. See Private network access |
--private-link-consumers |
No | Project IDs that may reach a private install through Private Service Connect, or none |
Every input above has a matching prompt: run ./install.sh with no arguments and it explains and asks for each one, so the flags are only needed for scripted runs — which must also pass --non-interactive: it requires every input up front, stops rather than prompting when one is missing, and is the only thing that skips the two confirmations. A run with no terminal that omits it stops at the first confirmation rather than proceeding on its own.
Resource names, Terraform provider versions, Kubernetes chart versions, and Greenlight image digests are derived or pinned by the release package. They are not install inputs.
Notification email
Section titled “Notification email”Greenlight emails your administrators about permission requests, requests to connect a new system, app health, and the weekly digest. Google Cloud has no built-in email service Greenlight can set up for you, so mail goes through an SMTP relay you already have: Google Workspace’s SMTP relay, SendGrid, Mailgun, or your own mail server.
| Input | Purpose |
|---|---|
--email-smtp-host |
The relay’s host name, such as smtp-relay.gmail.com |
--email-sender |
The address notifications come from. Required with a relay |
--email-smtp-port |
The relay’s port. 587 when omitted; 465 means TLS from the first byte |
--email-smtp-username |
The relay’s sign-in name, when it needs one |
The installer records these like any other input, so every update keeps them. It never asks for or records the relay’s password. Put that in the installation’s Secret Manager as notifications-smtp-password, for example with gcloud secrets create notifications-smtp-password --data-file=-. Until the password is there, nothing sends.
An install with no relay still completes. The interview and the end of the run both say plainly that Greenlight sends no email, and Settings → Notifications says the same until a relay is set.
Run the install
Section titled “Run the install”Extract the private release archive and run the script it contains:
unzip greenlight-gcp.zipcd greenlight-gcp-v<version>
./install.shRun it with no arguments. If you are not signed in to Google Cloud, a browser window opens so you can sign in — Terraform needs a second credential (Application Default Credentials), and the installer records both. It then says who is signed in and asks for the project: the projects this account can see, the active configuration’s project as the default. Its last choice, Sign in as someone else, runs gcloud auth login and asks again. Then it explains and asks for the region and host name, then asks whether Greenlight is public (the default) or private, with the private questions that follow, and then offers the standard private network 10.240.0.0/16 as a yes/no — that range cannot be changed after install and cannot be peered if it overlaps a network you will connect later; only a “no” asks for a different range. To answer up front instead, pass the flags from the table above. Scripted runs (--non-interactive) do not open a browser; they stop and name gcloud auth login and gcloud auth application-default login.
Every Google Cloud call the installer makes names the project you chose, so the project in your active gcloud configuration or CLOUDSDK_CORE_PROJECT does not change where it works. It reaches the cluster through a kubeconfig of its own, so changing your kubectl context in another terminal while it runs does not redirect it, and it also adds the new cluster to your kubeconfig as the current context, so kubectl points at it afterwards.
The package README pins the required CLI versions, and the installer checks all of them in one pass before anything else runs, including the gke-gcloud-auth-plugin that kubectl needs. Every region works without naming a zone, including the ones with no <region>-a zone such as us-east1: the install reads the region’s zones and pins nodes to the first one, then records it so later runs keep the same zone.
Signing in to Google Cloud
Section titled “Signing in to Google Cloud”gcloud and Terraform use two separate Google credentials. An interactive ./install.sh opens a browser when either is missing. --non-interactive stops and names gcloud auth login and gcloud auth application-default login. If gcloud commands work but Terraform fails with invalid_grant or invalid_rapt, only the Terraform credential is stale — run gcloud auth application-default login and rerun the installer.
If the project has no billing, an interactive run asks: link one of the billing accounts you may use (billing.resourceAssociations.create, from roles/billing.user), have someone else link one while the installer waits and checks again, or stop. A --non-interactive run stops instead; link a billing account in the Google Cloud console (Billing → Link a billing account) or run gcloud billing projects link <project-id> --billing-account=<account-id>, then rerun. If it says billing could not be checked, the signed-in account lacks billing.resourceAssociations.list on the project; the install continues, and stops at the apply if billing really is off.
-
Validate the target project. The installer signs you in if needed, checks every required tool and version in one pass, and verifies it can pull the release’s pinned public images. As soon as you name the project, before it asks anything else, it checks that the signed-in account can see that project ID and that the project has billing enabled. If either check fails, it asks for the project again; if the project came from
--project, it stops. An account that cannot read the project’s billing gets a note that the billing check was skipped, and the install continues. Before the review, it puts your nodes in the region’s first zone that offers their machine types, and checks that the project has enough CPU quota for the nodes it starts. A default size that does not fit is swapped for an equal one that does; a size you named is offered alternatives, or the run stops and says how to request more quota. On an update, it checks that the zone still offers the sizes your installation runs. These two checks need the Compute Engine API, so on a new project where the API is not on yet, the installer says they were skipped. Nothing is created before any of this. -
Confirm twice. The installer shows a review screen — target, region, zone, host name, networking, node sizes, the projects a private install publishes to through Private Service Connect, the Kubernetes and PostgreSQL versions this installation runs, and the exact image digests — and waits for you to confirm before the Terraform state bucket is created. It then writes a Terraform plan, shows it, and asks you to confirm yes or no. Applying that saved plan file is what runs, so what executes is what you reviewed.
-
Let the packaged script complete. It runs the plan, obtains GKE credentials, and applies the package-pinned Gateway API, Traefik, cert-manager, and Greenlight Helm releases. It links the install guide once when it starts, prints a phase name before each operation, and for each step that can run for minutes without output shows what it is waiting for and how long it has been waiting — so a long silence is never ambiguous. It can be rerun safely after a partial failure. On a first install the Cloud DNS zone’s name servers are printed as soon as Terraform creates the zone, so you can start step 5 while the cluster is still being created, and each part of the installation the overview listed is named as Terraform creates it (
Created: GKE cluster).Running it from a pipeline, or prefer plain output? Pass
--no-color(or setNO_COLOR) for text only, with no colour and no progress indicator. Redirected output is already plain. SetGREENLIGHT_INSTALL_VERBOSE=1to see every command’s full output instead of a progress indicator. -
Open the install wizard. The install prints
Finish setup in the browser:and a temporary URL on the reserved ingress IP that works before your final DNS name and certificate are ready, and stops working for good once the wizard moves you onto the HTTPS name. The ingress IP itself is permanent — only the temporary route on it is withdrawn. Its first screen creates your organization and asks for your work email, which fills in the initial org admin and the contact we reach about your trial; you can change either one on its own screen. The wizard then shows the Cloud DNS nameservers to delegate. This step belongs to a first install: a rerun of an installation you have already set up ends on its ownhttps://address and the version now running instead. -
Delegate the zone. Create the NS records for the name servers the installer printed (the wizard shows them too) in your parent DNS. Greenlight creates the control-plane, wildcard app, and certificate-challenge records inside the delegated zone, then checks on a repeating timer until delegation, the certificate, and HTTPS are live — nothing to click while you wait.
-
Finish the wizard. Configure OIDC, test sign-in, install the GitHub App, bootstrap the plugin marketplace (the form starts on a private repository; create a public one too if your agents clone it from GitHub), name the administrator we should contact about your trial and subscription (changed later in Settings → Organization), optionally save a payment method on a secure Stripe page if you bought Greenlight directly from Shift (saving a card charges nothing then; skip it and add one in Settings → Billing within seven days, or new building pauses until you do — running apps are unaffected), and confirm the organization and initial administrator.
-
Verify the installation. Open the final HTTPS URL and run the installation-health checks.
If Google Cloud runs out of capacity during the apply
Section titled “If Google Cloud runs out of capacity during the apply”The checks before the review read what your project may create, not whether Google Cloud has room at that moment. If a zone runs out of a machine type partway through the apply (ZONE_RESOURCE_POOL_EXHAUSTED or GCE_STOCKOUT), the installer stops, naming the machine type, the zone and the region. Run the same command again: the rerun says what stopped the last one and works around it before anything else is created.
More detail: what the rerun does
- A zone you did not name moves to the region’s next zone that offers both machine types, keeping the release’s machine type. Every zone an earlier rerun was refused in is skipped too, so reruns work through the region’s zones rather than going back and forth; once every zone has refused, the machine type moves instead. An interactive run lists those zones with the refused ones last and marked; choose one to try again. A zone only moves on a first install, before the nodes have run anywhere.
- A machine type the release chose moves to an equal type the release has vetted when the zone cannot move, and the installer says which.
- A zone or machine type you named (
--node-zone,--control-node-size,--app-node-size) is never replaced. A scripted run stops, naming the flag; an interactive run lists the types that fit, with the refused one marked. - Another region is offered only when the failed attempt created nothing in its region. Otherwise the installer says what has to be removed first. It never changes region by itself.
- A node pool Google Cloud left in an error state has to be deleted before the apply can create it again. An interactive run asks, names the deletion on the review and deletes it after you confirm. A scripted run stops with the
gcloud container node-pools deletecommand. The pool never ran Greenlight and holds no data. - CPU quota used up between the check and the apply (
QUOTA_EXCEEDED) is named as quota. Raise it, then rerun; the installer reads it again first. - The refusal is kept in
deployment/capacity-refusal/beside the installer until an apply finishes. To retry a refused value you named from a script, delete that file. - The automatic updater never changes a recorded zone or machine type. An update Google Cloud refuses for capacity is tried again on the next schedule.
Private network access
Section titled “Private network access”A private install has no public address. The control plane and every app are served by one internal load balancer at a reserved address in the Greenlight network. The default install stays public. A first install opts in at the installer’s web access question, and then asks whether all outbound traffic should go through a firewall you control: answer yes and the installer tells you to peer the networks and rerun with --egress-next-hop (Egress through your firewall); answer no and it asks which projects may connect through Private Service Connect (enter none to add them on a rerun). --access-profile private and --private-link-consumers answer the questions in a scripted run. A private install needs a network range of /20 or wider, because the internal address sits in the GKE subnet, the range’s first sixteenth. An install created with a narrower range stays public.
People and agents reach a private install from your own network in one of two ways:
- Private Service Connect (recommended). Pass
--private-link-consumerswith the IDs of your projects that should connect. Greenlight publishes its load balancer as a Private Service Connect service attachment that only those projects can use, and you create an endpoint for it in a VPC you own. Nothing routes between your network and Greenlight’s, so your address ranges may overlap, and nothing of yours runs in the Greenlight VPC. - VPC peering. Peer the Greenlight VPC to your network and reach the internal address directly.
A new install can be private from its first run. The installer reserves the internal address, prints the two names to publish in private DNS, and, with --private-link-consumers, the service attachment to aim your endpoint at. Open the wizard from your network once it can reach the install. To move an existing public install, follow the switch below.
Connect your network to a private install has the steps for your network team in one place. From the desktop installer app, a private install ends with Connect your network, which walks your network team through the same steps: the names to publish in private DNS, the private endpoint (from the address it shows) or the peering, and then a connection test from your computer on the company network or VPN. The app holds your answers for the setup wizard until the test passes, then sends them.
Do the switch in this order, inside a maintenance window. The public address stops answering before private DNS is published, so plan for that gap.
-
Reserve. On the existing public install, run
./install.sh --print-private-address. It shows a Terraform plan that adds one internal address in the GKE subnet, applies it once you confirm, and prints that address, the control-plane name, and the*.<host>wildcard. Web access does not change. -
Stage the network. For Private Service Connect, reserve an internal address for your endpoint in a subnet of your VPC, in the install’s region, and point both names at it in private DNS. For peering, peer the Greenlight VPC to your network and point both names at the printed address. Do not publish a public
Arecord for either name. Leave the cluster’s own resolver on the public view of the zone: a private zone bound to the Greenlight VPC that answers the control-plane name makes certificate renewal miss the DNS-01 challenge. -
Switch. Rerun with
--access-profile private, adding--private-link-consumers <project-id>[,<project-id>...]for Private Service Connect. A switch with no reserved address stops before it changes anything and tells you to reserve first, and so does a switch run with--phaseor with a--reconcile-modeother thanalways, since the switch needs the Google Cloud changes. After the change preview, and before changing anything else, the run records the private profile and deletes the publicArecords, and stops if it cannot. From then on, a run that stops partway is retried as private by the next run, the updater’s included. It then removes the external load balancer, creates the internal one at the reserved address, and only then releases the external address. Once it has reached the cluster through GKE’s DNS-based endpoint, it turns off the Kubernetes API’s public endpoint; if it cannot reach the cluster that way, it stops and leaves the endpoint on. It does not recreate the cluster, the VPC, the DNS zone, or the GitHub App, and it does not change the network range. A rerun that reads no recorded settings, because it passes every setting on the review screen as a flag, because it could not read them, or because there are none, must also pass--access-profile, and--private-link-consumersonce the install publishes through Private Service Connect. -
Connect. For Private Service Connect, the switch ends by printing the service attachment. In one of the listed projects, create the endpoint at the address you reserved:
Terminal window gcloud compute forwarding-rules create greenlight \--project=<your-project> --region=<install-region> \--network=<your-vpc> --address=<reserved-address-name> \--target-service-attachment=<printed-attachment>An endpoint from a listed project is accepted on its own, up to 10 endpoints per project; one from any other project stays pending. Every endpoint takes one address of the attachment’s NAT subnet, which holds 252 on the default /16 network range, so that range takes up to 25 projects; the installer refuses a list the subnet cannot hold.
-
Publish. Confirm the two names resolve on the company network to your endpoint, or to the internal address when you peer, and that they do not resolve on the public internet. Clients need TCP 443, and TCP 80 for the wizard.
-
Clear the webhook. In the GitHub App’s settings, clear the webhook Active checkbox. GitHub cannot reach a private install, and a delivery log full of failures is the symptom of leaving it on.
Reaching the cluster. A private install’s Kubernetes API has no public IP. kubectl, helm, and k9s reach it through GKE’s DNS-based endpoint from any network, with no VPN or route into the Greenlight VPC:
gcloud container clusters get-credentials <cluster> \ --region=<install-region> --project=<project> --dns-endpointGoogle checks every call against IAM before it reaches the cluster, so each person needs container.clusters.connect on the cluster, which the Kubernetes Engine Viewer and Developer roles include; Kubernetes RBAC applies as before. The endpoint accepts only Google credentials, so a static Kubernetes token or client certificate used from outside the VPC does not work. A kubeconfig that names the old public address stops working: fetch it again with --dns-endpoint. Without that flag, get-credentials writes the cluster’s internal address, which only answers inside the Greenlight VPC. Automatic updates never turn the public endpoint off, so an install made private before this release keeps it until you rerun the installer.
Changing the consumers. Rerun with a new --private-link-consumers list. Taking a project off the list refuses its new connections; an endpoint it already connected keeps working until you delete that endpoint in that project. --private-link-consumers none stops publishing: the run removes the service attachment, which closes every endpoint’s connection, and then its NAT subnet. Publishing again later creates a new attachment at a new URL; a closed endpoint does not reconnect, so each consumer recreates theirs against the URL the run prints.
What Private Service Connect changes. Greenlight sees each connection arrive from the attachment’s NAT address, not from the client. Everyone behind one endpoint shares that endpoint’s per-source rate limit, as they do behind any VPN that translates addresses. Sign-in, permissions, and audit identify people by their credentials, not by address. Greenlight never changes its load balancer while the attachment publishes it: Google Cloud does not allow it, so a release that would change it stops and says so, rather than dropping your connections.
Peering. The internal load balancer accepts connections from any region of a network peered to the Greenlight VPC, and from VPN or Interconnect tunnels that land in any region, as long as the peering carries routes for your client ranges: export them from your side and import them on the Greenlight side. GKE opens TCP 80 and 443 on the internal address to any source that can route to it, so filter at your own firewall. Firewall rules and routes you add to the Greenlight VPC stay in place across installer runs and automatic updates.
These clients work on a private install: Claude Code CLI and the Desktop Code tab, Cursor desktop, and Codex desktop. Claude.ai, connector clients, and cloud agents cannot reach it. When the control-plane name does not resolve, the CLI tells the operator to connect to the company network.
Egress through your firewall
Section titled “Egress through your firewall”By default a private install’s egress leaves through Cloud NAT, as a public one’s does. With --egress-next-hop, everything that leaves the Greenlight network goes to your firewall instead, except Google API traffic. Image pulls, logging, and Google sign-in tokens stay on Google’s network through Private Google Access, so your firewall only has to allow Greenlight’s own destinations, your identity provider, and the integrations you register.
- Put the firewall behind an internal passthrough Network Load Balancer. Google Cloud routes to a firewall in another VPC only through such a load balancer; a VM-Series deployment on Google Cloud already sits behind one. It must be in the install’s region, or have global access turned on. Note its forwarding rule’s address.
- Peer the Greenlight VPC to the firewall’s VPC. The route to your firewall runs over the peering, so an install with an egress next hop keeps its peering even when people reach it through Private Service Connect. Allow TCP 443 from the GKE subnet, the first sixteenth of the install’s network range, on your firewall: connections arrive from the nodes’ addresses.
- Rerun the private install with the hop:
./install.sh --egress-next-hop <forwarding-rule-address>. The installer accepts the hop only on a rerun of a private install, because a new network has no peering yet. The run adds the route to your firewall and removes the install’s Cloud NAT in one apply, so outbound connections pause for a moment, and for a few minutes afterwards a pod still holding a Google address it looked up before the switch reaches your firewall with that call until the address expires. It records the address, and later runs and automatic updates keep it. - Check egress. Every private run ends by checking egress from inside the control plane and naming each destination as reachable or blocked. Run
./install.sh --egress-preflightto check again after a firewall change.
Allow these destinations through your firewall:
| Destination | Why |
|---|---|
api.github.com |
The GitHub App, pipeline run discovery, and the release feed the updater reads |
github.com, release-assets.githubusercontent.com, objects.githubusercontent.com |
Release downloads and the Gateway API definitions the installer applies |
fleet.greenlight.shifthq.ai |
Fleet reporting. After seven days with no report delivered, new builds pause until one arrives |
claude.ai |
Claude Code’s sign-in. Greenlight reads Claude Code’s client metadata from there; an agent that signs in with its own metadata address needs that host too |
acme-v02.api.letsencrypt.org |
Certificate issuance and renewal |
traefik.github.io |
The routing chart an update installs |
quay.io, cdn.quay.io, cdn01.quay.io, cdn02.quay.io, cdn03.quay.io, cdn04.quay.io, cdn05.quay.io, cdn06.quay.io |
The certificate manager’s chart and images |
registry-1.docker.io, auth.docker.io, production.cloudfront.docker.com |
Greenlight’s own images and the routing layer’s images |
www.githubstatus.com, status.anthropic.com, status.openai.com, status.cloud.google.com, status.cursor.com |
Upstream status. Greenlight reads GitHub’s and the coding-agent vendors’ status pages to say when an outage there is why something looks broken; a blocked page shows as unknown and nothing else stops |
registry.terraform.io, releases.hashicorp.com |
The Terraform providers each automatic update downloads inside the cluster. Without them, updates stop at “Preparing Terraform” |
| Your identity provider | Sign-in |
Each integration you register is a destination too. Greenlight’s calls to an integration’s upstream go through your firewall like everything else, so registering one checks the upstream from inside the cluster first. If your firewall blocks it, registration is refused and names the host and port to allow from the Greenlight network. An app that calls a public service itself goes through your firewall the same way, so allow that host as well.
No Google API, registry, or GKE domain belongs on the list, and neither does the cluster’s API server: pods reach it at the kubernetes Service address inside the VPC, and nothing Greenlight runs in the cluster dials its DNS-based endpoint.
More detail: what keeps Google traffic off your firewall, and checking from the dashboard
What the installer adds. With the hop set, Terraform adds three routes to the Greenlight VPC and four private DNS zones bound to it:
route-egress-via-firewall:0.0.0.0/0to your load balancer, at priority 100, ahead of the VPC’s default internet route.route-private-googleapis:199.36.153.8/30, theprivate.googleapis.comaddresses, to the default internet gateway.route-direct-connectivity:34.126.0.0/18, the range Google uses for services that skip its front ends, to the default internet gateway.- Private zones for
googleapis.com,gcr.io,pkg.dev, andgke.googthat resolve every name in them toprivate.googleapis.com.
The two Google routes are more specific than the firewall’s, so they win for Google’s addresses, and the private zones send Google’s API, registry, and GKE names to those addresses. That is Google’s documented setup for a default route that points at an appliance. Your nodes keep resolving names through the metadata server inside each node, so the firewall needs no DNS or NTP rule.
What stays yours. The peering, your firewall, its load balancer, and any route or firewall rule you add to the Greenlight VPC are yours, and installer runs and automatic updates leave them in place. A rerun that reads no recorded settings, because it passes every setting on the review screen as a flag, because it could not read them, or because there are none, must pass --egress-next-hop again once the network routes through your firewall, or the run stops before changing anything.
Checking from the dashboard. Once the install is running, an organization administrator can check without the installer: with --egress-next-hop set, Internal Health → Cluster health has an Egress section listing every destination above, plus the install’s Artifact Registry host and Cloud Storage, your identity provider, and each registered integration’s upstream, with whether each was reachable at the last check and the stage that failed for one that was not. The Google destinations reach Google over Private Google Access, not through your firewall. Opening the section checks again when the last check is over a minute old, and Check again runs one on demand; nothing checks on a schedule, so your firewall logs carry no standing traffic from it.
Post-install verification
Section titled “Post-install verification”Installation health verifies that:
- The final URL resolves over HTTPS.
- SSO resolves to a real IdP user with the required role mapping.
- The GitHub App can reach at least one repository.
- A plugin marketplace repository has its initial generated commit.
- The policy bundle and audit log are active.
- A test app can register and pass the policy check.
The installer is idempotent: rerunning the same version reconciles the existing installation.
Upgrades
Section titled “Upgrades”Greenlight upgrades through a customer-owned updater inside the same Google Cloud project. It applies the versioned Terraform and Helm release using customer-controlled permissions. See Upgrades.
If you enabled automatic updates on an installation whose gl-updater service account predates the image-repository access listed in the identity table above, automatic updates stop at the planning step until you rerun the installer once with the current package. The updater cannot grant itself that access; the rerun grants it, and the next scheduled update proceeds on its own. New installations already include it.