Files
substrate/tools/setup-gcp
Haven Xia 9df8ca6abe [Upgrade] Version the install: node labels and a per-version atelet DaemonSet (#1357)
A rolling upgrade needs two substrate versions running in one cluster,
split by node: `atelet` and `ateom` speak a node-local protocol with no
cross-version guarantees, so everything on a node has to come from one
build. Today nothing records which build a node runs, and the `atelet`
DaemonSet is a single fixed-name object, so a second version can only
replace the first in place, instead of our porposed node by node rolling
([design](https://docs.google.com/document/d/1JduAyGZFyqdNp4UhKiv0EN-is3iW5tf5BwGTWbt2ouI/edit?usp=sharing&resourcekey=0-MXG12QCkleIxhY7cOB7g6A)).

This PR is the basis for upgrade. Nodes and the `atelet` DaemonSet get
keyed by an `ate.dev/substrate-version` label whose value is the build
version stamped into the binaries.

### One derivation for the version label

`internal/versionlabel` turns the build version into its two forms in
one place:

| form | grammar | used for |
|---|---|---|
| label value | k8s label value | node labels, DaemonSet labels,
nodeSelectors |
| name suffix | DNS-1123 | the DaemonSet name `atelet-<suffix>` |

A version that is not a valid label value is rejected, because the label
has to match what the `ldflags` stamp put into the binaries through
MAKEFILE and `ko`. It's also exposed shell, so the install script can
use it.

### A DaemonSet per version

`atelet-<suffix>` as the name, the version label on metadata, selector,
and pod template, and a pod nodeSelector on the same label. Two versions
run side by side on disjoint old/new node sets.

### The install becomes version-aware

- `install-ate.sh` reads the version from the same `make ldflags` output
it stamps the binaries with, then fills the manifest placeholders.
- It labels the nodes that exist at install time, nodes come after it
carry no version label yet (so add notes in README). Re-running the
install is idempotent, this is the basis for upgrade runbook later.
- The `README` documents the invariant, how to read the installed
version off the DaemonSet, and that a node added later hosts no workers
until it carries the label. On GKE the node pool label is the birth
default for new nodes - added in `tools/setup-gcp/README.md`.

### Follow-ups
- `ate-setup` need same modification
- minimize the affect for daily developer (pin the `VERSION` as
`{USER}-dev` instead of from git
- the manual rolling-upgrade runbook.

Fix #1270
Ref [design
doc](https://docs.google.com/document/d/1JduAyGZFyqdNp4UhKiv0EN-is3iW5tf5BwGTWbt2ouI/edit?usp=sharing&resourcekey=0-MXG12QCkleIxhY7cOB7g6A)
2026-09-01 12:40:49 -07:00
..
2026-05-22 14:43:26 -07:00

GCP Setup Tool (setup-gcp)

This tool automates the provisioning and configuration of Google Cloud Platform (GCP) resources required to run Agent Substrate. It is designed to be idempotent, meaning it can be run multiple times safely to ensure the environment is correctly configured.

Overview

The setup-gcp tool provisions and configures GCP resources for Agent Substrate. It uses a hierarchical command structure:

  • setup-gcp (root)
    • enable apis - Enable required GCP APIs.
    • create - Parent command for resource creation.
      • cluster - Create GKE cluster.
      • bucket - Create GCS bucket.
      • iam - Create IAM policy bindings and grant permissions.
      • dashboards - Create Cloud Monitoring dashboards.
    • bootstrap - Run all setup steps in order.

Prerequisites

  1. Go: Ensure Go is installed (version compatible with the project, see root go.mod).
  2. Google Cloud SDK (gcloud): Installed and authenticated.
  3. Application Default Credentials (ADC): The tool uses Google Cloud client libraries. You must set up ADC:
    gcloud auth application-default login
    
  4. Target Project: You must have a GCP project created and have sufficient permissions (typically Owner or Editor) to create resources like GKE clusters, GCS buckets, and IAM bindings.

Configuration & Defaults

All CLI flags can be configured via environment variables. If an environment variable is set, it will be used as the default value for the corresponding flag. Command-line flags always take precedence over environment variables.

A convenient way to manage these is to copy the example environment file, customize it, and source it before running the tool:

cp hack/ate-dev-env.sh.example .ate-dev-env.sh
# Edit .ate-dev-env.sh to match your project and preferences
source .ate-dev-env.sh

Global Flags

These flags can be passed to the root command and apply to all subcommands:

Flag Description Default Env Var Fallback Default
--project-id GCP Project ID. PROJECT_ID None
--project-number GCP Project Number (required for IAM). PROJECT_NUMBER None
--region GCP Region for regional resources. GCE_REGION us-west1

Subcommands

1. Enable APIs

Enables the required GCP APIs for the project.

go run ./tools/setup-gcp enable apis [flags]

Flags:

  • (Uses global --project-id)

2. Create Cluster

Creates a GKE cluster configured for Agent Substrate.

Warning

Agent Substrate requires two Kubernetes beta APIs — certificates.k8s.io/v1beta1/podcertificaterequests and certificates.k8s.io/v1beta1/clustertrustbundles — and GKE only honors enableK8sBetaApis at cluster creation. Enabling them later on an existing cluster is not recoverable in place: the tool's reconcile path issues the update, but the APIs do not become served — the cluster must be recreated with them enabled. create cluster sets them for you; if you bring your own cluster, create it with both APIs enabled from the start, e.g.:

gcloud container clusters create "${CLUSTER_NAME}" ... \
  --enable-kubernetes-unstable-apis=certificates.k8s.io/v1beta1/podcertificaterequests,certificates.k8s.io/v1beta1/clustertrustbundles

The symptom of a cluster without them: the install hangs at "Waiting for podcertificate ClusterTrustBundles to be ready" and kubectl get clustertrustbundles reports the resource type is not served.

go run ./tools/setup-gcp create cluster [flags]

Flags:

Flag Description Default Env Var Fallback Default
--name Name of the GKE cluster. CLUSTER_NAME substrate-poc
--location Zone or region for the cluster. CLUSTER_LOCATION us-west1-c
--version Kubernetes version. CLUSTER_VERSION None
--network VPC network name. NETWORK default
--subnetwork VPC subnetwork name. SUBNETWORK default
--machine-type Machine type for the gVisor node pool. GVISOR_NODE_MACHINE_TYPE c3-standard-4

Node version labels: pool labels are the birth default for every node GKE creates later (autoscaling, auto-repair, node upgrades), and setup-gcp does not set ate.dev/substrate-version on the pool, so those nodes arrive unlabeled and run no dataplane pods (see the README's note on node version labels). Stamp the pool with gcloud container node-pools update ... --node-labels=... (list the existing labels first and carry them all over; the flag replaces the full set), and create additional pools with --node-labels=ate.dev/substrate-version=<build version>.

3. Create Bucket

Creates a GCS bucket for storing snapshots.

go run ./tools/setup-gcp create bucket [flags]

Flags:

Flag Description Default Env Var Fallback Default
--name Name of the GCS bucket. BUCKET_NAME None (Required*)

*Note: Required unless the BUCKET_NAME environment variable is set.

4. Create IAM

Configures IAM permissions and Workload Identity bindings.

go run ./tools/setup-gcp create iam [flags]

Flags:

Flag Description Default Env Var Fallback Default
--bucket GCS bucket name. BUCKET_NAME None (Required for bucket bindings*)
--gke-nodes Grant GKE nodes permission to pull images. - true
--atelet Grant atelet project-level permissions. - true
--bucket-bindings Grant atelet access to the snapshot bucket. - true

*Note: Required for bucket bindings unless the BUCKET_NAME environment variable is set.

What create iam actually grants

On a fresh GCP project these bindings do not exist, and nothing else creates them — without them atelet cannot read or write snapshots (403 from GCS on the first suspend/resume) and nodes cannot pull images. bootstrap and create iam apply them for you; the table below is the reference for auditing them, or for applying them by hand in projects where you cannot run the tool with project-level IAM permissions.

atelet authenticates via GKE Workload Identity Federation: its Kubernetes ServiceAccount (ate-system/atelet, created by install-ate.sh) is addressed directly as an IAM principal — no Google service account is created or impersonated. This only works on a cluster with Workload Identity enabled (create cluster enables the PROJECT_ID.svc.id.goog pool; a pre-existing cluster must have it enabled too, or atelet's GCS calls fail with 401).

Member Role Resource Why
atelet WI principal¹ roles/storage.objectAdmin project² Read/write actor snapshots in GCS
atelet WI principal¹ roles/artifactregistry.reader project² Pull sandbox runtime assets
atelet WI principal¹ roles/storage.objectAdmin snapshot bucket Read/write snapshot objects
atelet WI principal¹ roles/storage.bucketViewer snapshot bucket List/stat the snapshot bucket
default compute SA³ roles/storage.objectViewer project² Nodes pull images from GCS-backed registries
default compute SA³ roles/artifactregistry.reader project² Nodes pull images from Artifact Registry

¹ principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/ate-system/sa/atelet — note it is keyed to the project number, not the ID. ² Project-level today; scoping these down is tracked in the TODOs in cmd/iam.go. ³ PROJECT_NUMBER-compute@developer.gserviceaccount.com (least-privileged node SA is #76).

Manual equivalent:

ATELET="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/ate-system/sa/atelet"

gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member="${ATELET}" --role=roles/storage.objectAdmin
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member="${ATELET}" --role=roles/artifactregistry.reader
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
  --member="${ATELET}" --role=roles/storage.objectAdmin
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
  --member="${ATELET}" --role=roles/storage.bucketViewer

5. Create Dashboards

Creates or updates Cloud Monitoring dashboards.

go run ./tools/setup-gcp create dashboards [flags]

Flags:

Flag Description Default Env Var Fallback Default
--dir Directory containing dashboard JSON files. DASHBOARD_DIR tools/setup-gcp/dashboards

6. Bootstrap (All Steps)

Runs all the setup steps in the correct order to fully bootstrap the environment.

go run ./tools/setup-gcp bootstrap [flags]

Flags:

Flag Description Default Env Var Fallback Default
--cluster-name Name of the GKE cluster. CLUSTER_NAME substrate-poc
--cluster-location Zone or region for the cluster. CLUSTER_LOCATION us-west1-c
--cluster-version Kubernetes version. CLUSTER_VERSION None
--network VPC network name. NETWORK default
--subnetwork VPC subnetwork name. SUBNETWORK default
--machine-type Machine type for the gVisor node pool. GVISOR_NODE_MACHINE_TYPE c3-standard-4
--bucket-name Name of the GCS bucket for snapshots. BUCKET_NAME None (Required*)
--dashboard-dir Directory containing dashboard JSON files. DASHBOARD_DIR tools/setup-gcp/dashboards

*Note: Required unless the BUCKET_NAME environment variable is set.

Examples

Run the tool from the repository root to ensure relative paths to dashboard configurations are resolved correctly.

Bootstrap everything using environment variables

If you have sourced your .ate-dev-env.sh:

go run ./tools/setup-gcp bootstrap

Bootstrap everything overriding some values

go run ./tools/setup-gcp bootstrap \
  --cluster-name="custom-cluster" \
  --machine-type="n2-standard-8"

Only create the cluster (using env vars for defaults)

go run ./tools/setup-gcp create cluster