Install on AWS
Admin guide
Greenlight installs into an AWS account you control. Plan on about 15–20 minutes for infrastructure provisioning and another 15–20 minutes for the install wizard.
This page covers AWS. See Install on Azure or Install on GCP 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-aws-<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 AWS member account with billing enabled. Greenlight’s roles act on resources across the whole account, so anything else in it would be within their reach. The installer cannot tell a fresh account from a used one, so it recommends a dedicated account and checks nothing.
- A provisioning identity with AdministratorAccess in that account.
- A non-overlapping RFC 1918 range if the Greenlight VPC will peer with an existing network. You do not need to decide the region in advance: the installer lists the regions your account has enabled that can host Greenlight, and you pick one.
- A domain where you can delegate one subdomain, such as
greenlight.acme.com, to Route 53. - 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 AWS CLI,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 AWS CLI orjqat a version the installer supports, it uses yours; it always uses its ownterraform,helm, andkubectl. Preparing that cache also usestar,unzip, andshasum, which macOS and Linux both ship. If you already sign in with your own AWS CLI, nothing changes: the installer’s copy reads the same credentials from your home directory. The installer checks what remains before it asks for anything and tells you how to install whatever is missing.
What the install provisions
Section titled “What the install provisions”Everything below is created inside the dedicated Greenlight account. The installer prints what it will create, grouped in four layers, before its first question.
| Area | Resources |
|---|---|
| Network | A VPC spanning two Availability Zones for private node and database subnets, one public ingress subnet in a single Availability Zone, an internet gateway, and NAT |
| Compute | One EKS cluster with a dedicated system node group, an autoscaling app node group, and cluster-autoscaler |
| Databases | Two private RDS for PostgreSQL instances: one for Greenlight and one for app-owned logical databases |
| Storage | Private, encrypted, versioned S3 buckets for app objects, app assets, and platform storage, plus a versioned Terraform state bucket |
| Registry | ECR repositories for Greenlight and app images |
| Secrets | Secrets Manager entries for platform, database, integration, and app secrets |
| DNS and ingress | One Route 53 hosted zone, an NLB with one reserved Elastic IP (or an internal NLB at a static private address on a private install), Traefik, and cert-manager |
| Identity | Dedicated IAM roles and IRSA bindings for the control plane, certificates, autoscaling, data access, and deployed apps |
The public entry points are the Greenlight/app ingress and the authenticated EKS API, which you and the installer use from outside and which you can limit to your own address ranges. The cluster’s own nodes and Greenlight’s services reach the EKS API at its private endpoint inside the VPC, on every install, so that traffic never leaves your network. A private install has no public ingress. RDS is private, S3 blocks public access, and workloads use IRSA instead of static AWS access keys.
If you want Greenlight to send notification email, provide an SMTP relay such as Amazon SES SMTP 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 |
|---|---|
| EKS system node group | 1 × m6a.xlarge |
| EKS app node group | m6a.xlarge, autoscale 0–3 |
| RDS control plane | PostgreSQL 16, db.t4g.large, 32 GB gp3 with automatic growth |
| RDS app databases | PostgreSQL 16, db.t4g.large, 32 GB gp3 with automatic growth |
| S3 | Regional, encrypted, versioned, all public access blocked |
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 (m6a.large), and an app group 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 group 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.
The app node group’s size is yours to set. Its instance type and the autoscaler’s smallest and largest node counts are install settings:
./install.sh --app-node-size m6a.2xlarge --app-node-min-count 1 --app-node-max-count 8The counts can be changed later; the instance 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. cluster-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 group and share the node that runs Greenlight itself when the group 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 group instead, and the autoscaler adds a node for it.
The instance type is settled at install. Changing it afterwards means replacing the node group — 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 groups or database classes directly — the next Greenlight update replays your recorded settings and undoes the edit.
Required access
Section titled “Required access”| Identity | Access | Why |
|---|---|---|
| Provisioning identity | AdministratorAccess in the dedicated Greenlight account | Creates resources, IAM roles, policies, and trust bindings |
| Greenlight runner role | Resource-scoped access to EKS, RDS, Secrets Manager, S3, ECR, and the delegated Route 53 zone, plus the registry push role’s trust policy | Operates the control plane, provisions app resources, and sets which GitHub runs may assume the push role |
Registry push role (gl-push_<token>) |
ECR image push to Greenlight app repositories, nothing else — no create or delete | Default-branch builds of your Greenlight app repositories assume it through GitHub’s OIDC token to push their images; Greenlight trusts only your GitHub organization’s gl-app-* repositories |
| cert-manager role | Route 53 record access on the delegated zone only | Completes DNS-01 certificate challenges |
| cluster-autoscaler role | Node-group autoscaling actions for the Greenlight cluster | Adjusts the app node group within its configured bounds |
| Connected-data role | No AWS permissions by default | Receives access inside a connected database only when a DBA grants it |
| Updater role | Infrastructure access within the dedicated Greenlight account, no registry writes | Applies Greenlight releases inside the maintenance window |
The installer creates the runtime roles and their IRSA trust bindings. Your team does not create or distribute long-lived AWS access keys. An AWS account holds one OIDC provider for GitHub Actions: the installer creates it when the account has none and otherwise uses the existing one unchanged, which must accept the sts.amazonaws.com audience. An organization SCP that denies iam:CreateOpenIDConnectProvider or sts:AssumeRoleWithWebIdentity in the Greenlight account blocks app image pushes. Delete protection guards the databases, buckets, hosted zone, and the cluster and its node groups, 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 region is chosen, the installer simulates the permissions below for the IAM user or role behind your session (iam:SimulatePrincipalPolicy; an SSO or assumed-role session is simulated as its role), with your region as aws:RequestedRegion so an organization policy that limits regions is read correctly. The list ships in the release archive as first-install-permissions.json.
| Permission | Granted by |
|---|---|
s3:CreateBucket |
AdministratorAccess |
iam:CreateRole |
AdministratorAccess |
iam:PutRolePolicy |
AdministratorAccess |
iam:AttachRolePolicy |
AdministratorAccess |
iam:CreatePolicy |
AdministratorAccess |
iam:CreateOpenIDConnectProvider |
AdministratorAccess |
iam:PassRole |
AdministratorAccess |
ec2:CreateVpc |
AdministratorAccess |
ec2:CreateSubnet |
AdministratorAccess |
ec2:CreateSecurityGroup |
AdministratorAccess |
ec2:CreateNatGateway |
AdministratorAccess |
ec2:CreateLaunchTemplate |
AdministratorAccess |
eks:CreateCluster |
AdministratorAccess |
eks:CreateNodegroup |
AdministratorAccess |
eks:CreateAccessEntry |
AdministratorAccess |
rds:CreateDBInstance |
AdministratorAccess |
secretsmanager:CreateSecret |
AdministratorAccess |
ecr:CreateRepository |
AdministratorAccess |
route53:CreateHostedZone |
AdministratorAccess |
servicequotas:RequestServiceQuotaIncrease |
ServiceQuotasFullAccess (optional: only for the installer to request vCPU 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.
Region, instance types, database class, and quota
Section titled “Region, instance types, database class, and quota”Before the review, the installer checks that the region can host this release and that your account can create what the install needs there:
- Region. An interactive first install lists your account’s enabled regions that offer RDS for PostgreSQL 16, EKS’s Kubernetes version for this release in standard support, and instance types of the shape each node needs, grouped by geography. A region you type or pass with
--regionis checked the same way, and one that fails stops, naming the regions that pass. - Instance types and zone. Offerings are checked in the availability zone the nodes and databases use, not anywhere in the region. If the release’s default (
m6a.xlarge) is not offered there or your quota cannot cover it, the installer switches to an equal 4 vCPU / 16 GiB type from the release’s list (m6i.xlarge, thenm5.xlarge) and tells you which. A type you name yourself is never swapped; an interactive run shows the equal x86 types that fit and lets you choose. - Database class. The release prefers
db.t4g.large. If the zone cannot host it, the installer uses the smallest class of the same family with at least 2 vCPUs and 8 GiB and tells you which. A class you pass with--database-sizeis never swapped. - vCPU quota. Every node the install starts is counted against your account’s Running On-Demand vCPU quota for the instance family in the region, less what your running on-demand instances already use. Quota short of the application group’s maximum is a warning, not a stop.
- Kubernetes version. The release runs one Kubernetes version (today 1.36); an existing cluster more than one version behind moves one version per run, and one already ahead stays where it is.
More detail: when the account is out of vCPU quota
- On an interactive install, the installer checks quota as soon as you answer the region question. If no instance type the release can use fits, it names the quota, how many vCPUs the install needs, and how many are free, then asks: choose another region, ask AWS to raise the quota, or stop. Nothing has been created at that point.
- Ask AWS to raise the quota files a Service Quotas increase under your sign-in and waits up to ten minutes for AWS’s answer. If a request for that quota is already pending for at least as much, the installer waits on it rather than filing another. If AWS approves, the install continues and the review lists the raised quota.
- If the request is still pending, or AWS denies it, the installer says so with the request’s ID, then asks again. A later run reads the quota again before asking anything.
- Requesting quota needs the optional
servicequotas:RequestServiceQuotaIncreasepermission. Without it, the installer stops, naming the permission. - If AWS does not expose the quota for your account in the region, the installer says the quota is unknown and goes on; the apply stops if it is short.
- A
--non-interactiverun never asks. It stops, naming the quota and the vCPUs needed. - On a rerun of an existing installation, quota is checked only for nodes the run adds, and a rerun that adds no nodes is never refused on quota.
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”aws --version— reads · this computer · every run. Reads the AWS CLI version, for the dependency check.terraform version -json— reads · this computer · every run. Reads the Terraform version, for the dependency check.aws configure list-profiles— reads · this computer · interactive runs. Lists the AWS CLI profiles on this computer, for the profile question.aws configure get <setting> --profile <profile>— reads · this computer · interactive runs. Reads whether a profile signs in through IAM Identity Center, to pick the sign-in command.aws sso login --profile <profile>— changes · this computer · interactive runs. Signs you in through IAM Identity Center when the chosen profile has no session; the sign-in is the AWS CLI’s own flow.aws login— changes · this computer · interactive runs. Signs you in to AWS when the chosen profile has no session; the sign-in is the AWS CLI’s own flow.aws configure export-credentials --format process— reads · this computer · every run. Hands the CLI’s session to Terraform’s AWS provider, which cannot read an aws login session itself; the credentials stay in this run’s environment.aws configure list— reads · this computer · every run. Reads where the CLI’s credentials come from, to say which identity the run uses.aws sts get-caller-identity --query <query> --output text— reads · your cloud · every run. Reads the account and identity the run signs in as.aws iam get-role --role-name <role> --query Role.Arn --output text— reads · your cloud · every run. Resolves an assumed-role session to its role, for the permission check and the cluster access entry.aws iam simulate-principal-policy --policy-source-arn <principal> --action-names <action> --context-entries <contextEntry> [--query <query>] --output json— reads · your cloud · first installs. Asks IAM whether your identity holds each permission a first install uses, without using any of them.aws eks list-clusters --region <region> --query <query> --output text— reads · your cloud · every run. Looks for an existing Greenlight cluster in the region.aws eks update-kubeconfig --name <cluster> --region <region> --kubeconfig <runKubeconfig>— 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.aws ec2 describe-regions --query Regions[].RegionName --output text— reads · your cloud · interactive runs. Lists the regions your account can use, for the region question.aws ec2 describe-instance-type-offerings --region <region> --location-type availability-zone --filters Name=instance-type,Values=<instanceType> --query InstanceTypeOfferings[].Location --output text— reads · your cloud · every run. Checks a type is offered in the zone the nodes use, when a refusal could move them.aws ec2 describe-instance-type-offerings --region <region> --location-type availability-zone --filters Name=location,Values=<zone> --query InstanceTypeOfferings[].InstanceType --output text— reads · your cloud · every run. Lists the instance types the zone the nodes and databases use offers, so sizes are checked where they land.aws ec2 describe-availability-zones --region <region> --filters Name=zone-type,Values=availability-zone Name=state,Values=available --query AvailabilityZones[].ZoneName --output text— reads · your cloud · only when a node zone is named, recorded, or moved after a capacity refusal. Reads the region’s first two availability zones, the ones Greenlight’s network uses, to check a named zone or move the nodes after a capacity refusal.aws ec2 describe-instance-types --region <region> --filters <filter> --query InstanceTypes[].InstanceType --output text— reads · your cloud · every run. Lists the instance types of a node’s shape, current-generation, on-demand, x86 and never burstable, for the sizes a region can offer.aws ec2 describe-instance-types --region <region> --filters Name=instance-type,Values=<types> --query <query> --output text— reads · your cloud · every run. Reads named instance types’ vCPUs and memory, to measure them against the vCPU quota and to size a database class.aws ec2 describe-instances --region <region> --filters Name=instance-state-name,Values=pending,running --query <query> --output text— reads · your cloud · every run. Lists the account’s running on-demand instance types in the region, the usage the vCPU quota is measured against, when the run adds nodes.aws service-quotas get-service-quota --service-code ec2 --quota-code <quotaCode> --region <region> --query Quota.Value --output text— reads · your cloud · every run. Reads the account’s on-demand vCPU quota for an instance family in the region, when the run adds nodes.aws service-quotas get-aws-default-service-quota --service-code ec2 --quota-code <quotaCode> --region <region> --query Quota.Value --output text— reads · your cloud · every run. Reads AWS’s default for that quota when the account has none applied, when the run adds nodes.aws service-quotas request-service-quota-increase --service-code ec2 --quota-code <quotaCode> --desired-value <limit> --region <region> --query RequestedQuota.Id --output text— changes · your cloud · interactive runs. Asks AWS to raise a vCPU quota the install needs, only after you choose to request it at the quota question. Named on the review once approved.aws service-quotas list-requested-service-quota-change-history-by-quota --service-code ec2 --quota-code <quotaCode> --status PENDING --region <region> --query <query> --output text— reads · your cloud · interactive runs. Finds a request for the quota already pending, which AWS takes the place of a new one.aws service-quotas get-requested-service-quota-change --request-id <request> --region <region> --query RequestedQuota.Status --output text— reads · your cloud · interactive runs · repeats until ready. Waits up to ten minutes for AWS’s answer to a quota request you chose to file at the quota question.aws eks describe-cluster-versions --region <region> --cluster-versions <version> --query <query> --output text— reads · your cloud · first installs. Checks the region’s EKS offers the Kubernetes version this release runs in standard support.aws rds describe-db-engine-versions --region <region> --engine postgres --engine-version <major> --query <query> --output text— reads · your cloud · every run. Reads the newest PostgreSQL version the region offers.aws rds describe-orderable-db-instance-options --region <region> --engine postgres --engine-version <version> --query <query> --output text— reads · your cloud · every run. Reads which database classes the region can host for PostgreSQL, and in which zones, to check the class in the zone the databases use.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.
After the review’s yes
Section titled “After the review’s yes”aws s3api head-bucket --bucket <stateBucket>— reads · your cloud · every run. Checks whether the Terraform state bucket exists.aws s3api get-bucket-location --bucket <stateBucket> --query LocationConstraint --output text— reads · your cloud · every run. Reads which region an existing state bucket lives in. One bucket serves the account from its first install’s region, so Terraform is pointed there.aws s3api create-bucket --bucket <stateBucket> --region <region> [--create-bucket-configuration LocationConstraint=<region>]— changes · your cloud · first installs. Creates the bucket that holds this installation’s Terraform state.aws s3api put-bucket-versioning --bucket <stateBucket> --versioning-configuration Status=Enabled— changes · your cloud · every run. Turns on versioning for the state bucket, so an earlier state can be recovered.aws s3api put-public-access-block --bucket <stateBucket> --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true— changes · your cloud · every run. Blocks every form of public access to the state bucket.terraform -chdir=<iacDir> init -input=false -backend-config=bucket=<stateBucket> -backend-config=key=<stateKey> -backend-config=region=<region>— changes · this computer · every run. Prepares Terraform in the archive’s AWS 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 it 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 · updates. Reads one resource from state, such as a route table to check or the DNS zone to announce again.aws iam get-open-id-connect-provider --open-id-connect-provider-arn <provider> --query ClientIDList --output text— reads · your cloud · first installs. Checks whether the account already has GitHub’s OIDC provider, which the install then uses instead of creating a second.aws eks describe-cluster --name <cluster> --region <region> --query <field> --output text— reads · your cloud · updates · repeats until ready. Reads the cluster’s Kubernetes version, whether an operation is running on it, and its API endpoint access: private access and the allowed ranges.aws eks list-nodegroups --cluster-name <cluster> --region <region> --query nodegroups --output text— reads · your cloud · updates. Lists the node groups, to wait for any operation already running on them.aws eks describe-nodegroup --cluster-name <cluster> --nodegroup-name <nodegroup> --region <region> --query <field> --output text— reads · your cloud · updates · repeats until ready. Reads a node group’s status and size, and, before the review of a rerun after a capacity refusal, its status and instance type.aws eks delete-nodegroup --cluster-name <cluster> --nodegroup-name <nodegroup> --region <region>— changes · your cloud · only to finish a run that stopped part way. Deletes a node group the first install AWS refused for capacity on the last run left failed, or created with an instance type this run changes; Terraform never replaces a node group. It is asked first and named on the review, and it never ran Greenlight.aws eks wait nodegroup-deleted --cluster-name <cluster> --nodegroup-name <nodegroup> --region <region>— reads · your cloud · only to finish a run that stopped part way. Waits until that node group is gone, so the plan creates it again.aws ec2 describe-route-tables --region <region> --route-table-ids <routeTable> --query <query> --output text— reads · your cloud · only when something Greenlight creates already exists outside this installation’s state. Checks whether a default route already exists outside Terraform’s state.terraform -chdir=<iacDir> import -input=false -var-file=<varsFile> <address> <id>— changes · your cloud · only when something Greenlight creates already exists outside this installation’s state. Brings a default route or an operator’s cluster access entry that already exists under this installation’s state, so the plan does not try to create it twice.aws eks describe-access-entry --cluster-name <cluster> --region <region> --principal-arn <principal> --query accessEntry.type --output text— reads · your cloud · only when something Greenlight creates already exists outside this installation’s state. Checks whether an operator’s cluster access entry already exists outside Terraform’s state.aws eks list-associated-access-policies --cluster-name <cluster> --region <region> --principal-arn <principal> --query associatedAccessPolicies[].policyArn --output text— reads · your cloud · only when something Greenlight creates already exists outside this installation’s state. Checks whether that entry already holds the cluster-admin access policy.kubectl get svc -n traefik traefik [--ignore-not-found] -o jsonpath=<field>— reads · the new cluster · updates. Reads the routing layer’s load balancer, to tell whether it is public or private and find it in AWS.aws elbv2 describe-load-balancers --region <region> --query <query> --output text— reads · your cloud · every run. Reads the load balancer behind the routing layer: its ARN for a private-link publication, or its addresses for the address check.aws ec2 describe-network-interfaces --region <region> --filters <filter> --query <query> --output text— reads · your cloud · private installs. Checks the private address asked for is not already held by another network interface.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 · every run. Reads the plan back, to check it targets the account you signed in to and to count what it creates, changes, and destroys.
After the preview’s yes
Section titled “After the preview’s yes”aws eks update-nodegroup-config --cluster-name <cluster> --nodegroup-name apps --region <region> --scaling-config minSize=<min>,maxSize=<max>,desiredSize=<max>— changes · your cloud · updates. Lowers the application node group to its new ceiling before the plan is made again, when you lowered the maximum below what is running.aws route53 list-resource-record-sets --hosted-zone-id <zone> --output json— reads · your cloud · private installs. Reads the public DNS records, when switching an installation to private.aws route53 change-resource-record-sets --hosted-zone-id <zone> --change-batch <changes>— changes · your cloud · private installs. Removes the public DNS records when switching an installation to private.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.aws ec2 describe-vpc-endpoint-connections --region <region> --filters Name=service-id,Values=<service> --query <query> --output text— reads · your cloud · private installs. Lists the private-link connections to the publication about to be replaced.aws ec2 reject-vpc-endpoint-connections --region <region> --service-id <service> --vpc-endpoint-ids <endpoint>— changes · your cloud · private installs. Disconnects the consumers you named before their publication is replaced, so it can be removed.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 IAM roles and policies below.gl-runner-<token>, for the control plane, through IAM roles for service accounts: gl-runner-policy on the installation’s EKS, RDS, S3, ECR, Route 53 and IAM resourcesgl-updater-<token>, for the updater, through IAM roles for service accounts: gl-updater-policy on the installation’s resources; AmazonEKSClusterAdminPolicy on the clustergl-data-<token>, for the control plane’s data access, through IAM roles for service accounts: no role of its owngl-cert-manager-<token>, for cert-manager, through IAM roles for service accounts: gl-cert-manager-policy on the installation’s Route 53 zonegl-aws-load-balancer-controller-<token>, for the load balancer controller, through IAM roles for service accounts: gl-aws-load-balancer-controller-policy on the account’s load balancersgl-cluster-autoscaler-<token>, for the cluster autoscaler, through IAM roles for service accounts: gl-cluster-autoscaler-policy on the cluster’s node groupsgl-push_<token>, for app image builds, through GitHub’s OIDC provider: gl-push-policy on the installation’s ECR repositorieseks-cluster-greenlight-<token>, for the EKS control plane: AmazonEKSClusterPolicy on the clustereks-node-greenlight-<token>, for the cluster’s nodes: AmazonEKSWorkerNodePolicy on the nodes; AmazonEKS_CNI_Policy on the nodes; AmazonEC2ContainerRegistryPullOnly on image pullseach operator principal, for you and the principals you named, through EKS access entries: AmazonEKSClusterAdminPolicy on the cluster
aws route53 get-hosted-zone --id <zone> --query DelegationSet.NameServers --output text— 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 or the operator principals.aws eks update-kubeconfig --name <cluster> --region <region> --kubeconfig <runKubeconfig>— changes · this computer · every run. Writes credentials for the new cluster into this run’s own kubeconfig file, not yours.aws rds describe-db-instances --db-instance-identifier <instance> --region <region> --query <query> --output text— 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”aws secretsmanager get-secret-value --region <region> --secret-id <secret> --query SecretString --output text— reads · your cloud · every run. Reads the database master password RDS generated, to hand it to the release’s one-time database setup; it is never printed.kubectl create secret generic greenlight-rds-master-bootstrap --namespace greenlight-system --from-file=<key>=<file> --dry-run=client -o yaml— reads · this computer · every run. Writes out that password as a Secret manifest for the apply, from a file in the installer’s private work directory.kubectl delete secret greenlight-rds-master-bootstrap --namespace greenlight-system --ignore-not-found— changes · the new cluster · every run. Deletes that Secret once the release has set up its databases.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.helm upgrade --install <prerequisite> <chart> --version <version> --namespace kube-system --create-namespace --repo <repository> [--values <valuesFile>] [--set <chartValue>=<value>] --wait --timeout 5m— changes · the new cluster · every run. Installs the cloud’s own cluster add-ons on AWS: the load balancer controller and the cluster autoscaler.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 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.
Install inputs
Section titled “Install inputs”The installing administrator supplies only values specific to the customer’s environment:
| Input | Required | Purpose |
|---|---|---|
| AWS CLI session | Yes | Selects the dedicated target account; install.sh derives the account ID from the active credentials and points Terraform at that session so it can refresh, so a browser aws login works the same as SSO |
--profile |
No | The AWS CLI profile to install with. An interactive run asks, listing your configured profiles; credentials exported in the environment take precedence over any profile, so then nothing is asked |
--region |
Yes | Region where Greenlight resources are created. Omit it in an interactive run and the installer offers the regions your account can use |
--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 |
--access-profile |
No | public (the default) or private. See Private network access |
--private-link-consumers |
No | AWS account IDs that may reach a private install through PrivateLink, or none |
--node-zone |
Only when needed | Places Greenlight’s nodes, databases and load balancer in the second of the two zones the network uses instead of the first. Settled at install |
--kubernetes-api-allowed-ranges |
No | Public IPv4 ranges the Kubernetes API accepts calls from, or none. See Kubernetes API allowed ranges |
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, instance sizes, database classes, 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. AWS 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, Amazon SES once your account has production access, 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 Secrets Manager as notifications/smtp-password, for example with aws secretsmanager create-secret --name notifications/smtp-password --secret-string …. 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”After authenticating the AWS CLI to the dedicated Greenlight account, extract the private release archive and run the script it contains:
unzip greenlight-aws.zipcd greenlight-aws-v<version>
aws sts get-caller-identity
./install.shRun it with no arguments. The installer first says who is signed in and, when you have AWS CLI profiles configured, asks which one to use, the active profile as the default. Its last choice, Sign in as someone else, runs aws sso login for an SSO profile or aws login otherwise, and asks again. It then 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. For the region it shows a numbered list, grouped by geography, containing only the enabled regions that can host Greenlight; enter a number, or type a region code if you already know which one you want. Working out that list changes nothing in your account — a region you have not opted into is simply absent, because enabling one is a change the installer will not make for you. To answer up front instead, pass the flags from the table above.
Before it creates anything, the installer checks that the region offers PostgreSQL 16 with the release’s database class and each requested EC2 instance type. If an instance type is not offered there, it names which of the two it rejected and offers the region’s types with the shape that node needs (4 vCPU / 16 GiB, or 2 vCPU / 8 GiB for the minimal profile’s system node); a scripted run names the flag to pass instead and stops before anything is created.
Verify that aws sts get-caller-identity returns the intended account before continuing: that account is where Greenlight installs; --profile chooses among your configured profiles, and no flag names an account directly. The installer prints the identity it is using — role, account, and where the credentials came from — before it does anything, holds every AWS call in the run to the --region you pass even if your shell has a different AWS_REGION exported, and stops with a clear message if Terraform and the AWS CLI resolve different accounts. 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.
-
Validate the target account. Confirm the active AWS identity and account before running the command. The installer checks every required tool and version in one pass, and verifies it can pull the release’s pinned public images, before it creates anything.
-
Confirm twice. The installer shows a review screen — account, region, host name, networking, instance sizes, the Kubernetes and PostgreSQL versions this release 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 EKS credentials, and applies the package-pinned Gateway API CRDs, AWS Load Balancer Controller, cluster-autoscaler, Traefik, cert-manager, and Greenlight Helm releases — the Greenlight step also creates the Greenlight database login inside RDS, using a temporary credential the installer discards immediately after. 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 Route 53 hosted 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: EKS cluster and add-ons).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 Elastic IP, verified to be the one address your DNS records will point at, 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 Elastic 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 Route 53 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 AWS runs out of capacity during the apply
Section titled “If AWS runs out of capacity during the apply”The checks before the review read what your account may create, not whether AWS has room at that moment. If an availability zone runs out of an instance type partway through the apply (InsufficientInstanceCapacity), the installer stops, naming the instance 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
- The zone moves to the other of the two zones Greenlight’s network uses, keeping the release’s instance type, but only while that zone has not refused this install too and neither the database nor the load balancer’s subnet exists yet: Greenlight’s nodes, databases and load balancer share one zone, so once either exists the zone cannot move. An interactive run lists the zones with the refused one marked; choose it to try again. The zone is recorded and never moves after a first install.
- An instance 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 instance 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 group AWS left failed, or one the attempt created whose instance type the rerun changes, 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
aws eks delete-nodegroupcommand. The node group never ran Greenlight and holds no data. - vCPU quota used up between the check and the apply (
VcpuLimitExceeded) is named as quota. Raise it in Service Quotas, then rerun. - 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 instance type. An update AWS refuses for capacity is tried again on the next schedule.
Kubernetes API allowed ranges
Section titled “Kubernetes API allowed ranges”The cluster’s Kubernetes API has a public, authenticated endpoint that you and the installer use from outside AWS. You can limit it to the address ranges your own people and automation connect from. The cluster’s nodes and Greenlight’s own services reach the API at its private endpoint inside the VPC, so the list holds only your ranges, never Greenlight’s.
- Set the list at first install or on any rerun:
--kubernetes-api-allowed-ranges 203.0.113.0/24,198.51.100.7/32. - Change it by rerunning with the new list, which replaces the old one.
- Clear it with
--kubernetes-api-allowed-ranges none. The endpoint then accepts calls from any address, the default.
Include the address of every machine that runs the installer, kubectl, or other tools against the cluster for you. The installer records the list, and automatic updates apply the same list: only a run you start changes it. A change takes a few minutes, touches no nodes or running apps, and applies to new connections, so a session that is already open keeps working until it reconnects.
More detail
What the installer refuses before it changes anything.
- An entry that is not a public IPv4 range in CIDR form: a private, shared, loopback, link-local, multicast, or reserved range, or an address with host bits set, such as
203.0.113.5/24for203.0.113.0/24. - More than 40 ranges, the most EKS takes for one cluster. AWS does not raise this limit, so combine neighbouring ranges.
- A list on a cluster whose private endpoint access is not on yet, because its nodes would still call the public endpoint. Rerun once without the flag, which turns private access on, then again with it.
- A rerun that read no recorded settings and names no list while the cluster has one, because it would otherwise open the endpoint. Pass the list again, or
none. - A list that leaves out the machine running the installer, unless that machine reaches the cluster inside the VPC. The run names the address to add.
If a run starts outside the list. When you run the installer from an address the cluster’s list leaves out, it cannot reach the cluster, so it stops, names this machine’s address, and prints an aws eks update-cluster-config command that adds it. That command goes through the AWS API, which the list does not limit, so you cannot lock yourself out for good. Run it, wait a few minutes for the update to finish, then rerun with your address in --kubernetes-api-allowed-ranges; otherwise the rerun puts back the recorded list without it.
Checking the list. aws eks describe-cluster --name <cluster> --query cluster.resourcesVpcConfig shows publicAccessCidrs, and endpointPrivateAccess as true.
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 Network Load Balancer at a static private address in the Greenlight VPC, and the Elastic IP is released. The default install stays public. A first install opts in at the installer’s web access question, and then asks which AWS accounts may connect through PrivateLink (enter none to add them on a rerun). Egress through your own firewall is not available on AWS yet. --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 ingress 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:
- PrivateLink (recommended). Pass
--private-link-consumerswith the IDs of your AWS accounts that should connect. Greenlight publishes its load balancer as a VPC endpoint service that only those accounts can use, and you create an interface 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 or Transit Gateway. Connect the Greenlight VPC to your network and reach the internal address directly.
A new install can be private from its first run. The installer prints the internal address, the two names to publish in private DNS, and, with --private-link-consumers, the endpoint service to aim your interface 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.
-
Record the address. On the existing public install, run
./install.sh --print-private-address. It checks that nothing else in the ingress subnet holds the internal address, records it, and prints it with the control-plane name and the*.<host>wildcard. Web access does not change. -
Stage the network. For PrivateLink, plan an interface endpoint in a VPC of a listed account, and point both names at it in a Route 53 private hosted zone associated with your VPCs. For peering or Transit Gateway, connect the Greenlight VPC to your network and point both names at the printed address. Do not publish a public
Arecord for either name, and do not associate that private hosted zone with the Greenlight VPC: the cluster’s own resolver keeps the public view of the zone, or certificate renewal misses the DNS-01 challenge. -
Switch. Rerun with
--access-profile private, adding--private-link-consumers <account-id>[,<account-id>...]for PrivateLink. A switch with no recorded address stops before it changes anything and tells you to record it first. After the change preview, and before changing anything else, the run records the private profile and deletes the public apex and wildcardArecords, 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 internet-facing load balancer, creates the internal one at the recorded address, and only then releases the Elastic IP. 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 PrivateLink. -
Connect. For PrivateLink, the run ends by printing the endpoint service name and the ID of the one Availability Zone the load balancer lives in. Zone names such as
us-east-1adiffer between accounts, so match the zone by its ID (for exampleuse1-az4). In a listed account, create the interface endpoint with a subnet in that zone:Terminal window aws ec2 create-vpc-endpoint --vpc-endpoint-type Interface \--vpc-id <your-vpc> --subnet-ids <subnet-in-that-zone> \--security-group-ids <your-endpoint-sg> \--service-name <printed-endpoint-service>An endpoint from a listed account is accepted on its own; any other account cannot connect. Its security group must allow your clients on TCP 443, and TCP 80 for the wizard.
-
Publish. Confirm the two names resolve on the company network to your endpoint’s address, or to the internal address when you peer, and that they do not resolve on the public internet.
-
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.
Changing the consumers. Rerun with a new --private-link-consumers list. Taking an account off the list refuses its new connections; an endpoint it already connected keeps working until you reject it from the Greenlight account with aws ec2 reject-vpc-endpoint-connections --service-id <vpce-svc-id> --vpc-endpoint-ids <endpoint-id>, or that account deletes it. --private-link-consumers none stops publishing: AWS cannot delete an endpoint service that still has connections, so the run rejects every connected endpoint and then removes the service.
What PrivateLink changes. Greenlight sees every PrivateLink connection arrive from the load balancer’s own address, not from the client, so all PrivateLink traffic shares one per-source rate limit, as it would behind any VPN that translates addresses. Sign-in, permissions, and audit identify people by their credentials, not by address. The load balancer’s security group does not filter PrivateLink traffic; the allowed accounts decide who connects.
Peering. The internal load balancer’s security group accepts TCP 80 and 443 from any source that can route to it, so filter at your own firewall. The load balancer answers your clients from the ingress subnet, so add the route to your network to rt-public-greenlight, and to the rt-private-greenlight-* tables when Greenlight or its apps must reach hosts in your network. Routes you add to those tables stay in place across installer runs and automatic updates.
Egress on a private AWS install leaves the way it does on a public one. Routing it through your firewall with --egress-next-hop is not in this release on AWS; the installer refuses that flag rather than ignoring it.
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.
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 AWS account. It applies the versioned Terraform and Helm release using customer-controlled permissions. See Upgrades.