Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-added-large-files
args: ['--maxkb=2000']
- repo: https://github.com/psf/black-pre-commit-mirror
Expand Down
172 changes: 16 additions & 156 deletions gcloud/README.md
Original file line number Diff line number Diff line change
@@ -1,159 +1,19 @@
# Deploy the Select AI A2A server to Google Cloud
# Google Cloud deployment modes

`gcloud/deploy.sh` builds or selects a Select AI container image, creates or
updates a private Cloud Run service, and configures its database secrets. Run
it on a machine with the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install)
installed and authenticated to the target project.
Select AI supports two distinct Google Cloud deployment modes. Choose based on
whether the database and Select AI team are known at deployment time or must
be selected dynamically by each user.

## IAM permissions

The scripts use the active `gcloud` identity. They do not elevate its access.

### Deployer (the active gcloud identity)

| Operation | Required permissions |
| --- | --- |
| Inspect and create the Artifact Registry repository | `artifactregistry.repositories.get`, `artifactregistry.repositories.create` |
| Inspect and create the default runtime service account | `iam.serviceAccounts.get`, `iam.serviceAccounts.create` |
| Deploy or update Cloud Run | `run.services.create`, `run.services.update`, `run.services.get`, `run.operations.get`; `iam.serviceAccounts.actAs` on the runtime service account; `artifactregistry.repositories.downloadArtifacts` on the image repository |
| With `--build`, upload local source, submit, and wait for a build | `storage.buckets.get`, `storage.objects.create` on the configured source-staging bucket; `cloudbuild.builds.create`, `cloudbuild.builds.get`, `serviceusage.services.use` |
| Inspect, create, and add versions to database or wallet secrets | `secretmanager.secrets.get`, `secretmanager.secrets.create`, `secretmanager.versions.add` |
| Grant the runtime account access to those secrets | `secretmanager.secrets.getIamPolicy`, `secretmanager.secrets.setIamPolicy` |
| Grant Gemini Enterprise and the active gcloud identity access to the service | `run.services.getIamPolicy`, `run.services.setIamPolicy` |
| Obtain the project number | `resourcemanager.projects.get` |

### Runtime service account

| Operation | Required permissions |
| --- | --- |
| Read database and wallet secrets while serving requests | `secretmanager.versions.access` |

### Other service identities

| Principal | Operation | Required permissions |
| | Standalone | Dynamic gateway |
| --- | --- | --- |
| Cloud Build execution service account | With `--build`, push the built image | `artifactregistry.repositories.uploadArtifacts` |
| Gemini Enterprise service agent | Invoke the private Cloud Run service | `run.routes.invoke` |
| Active gcloud identity | Fetch the Agent Card after deployment | `run.routes.invoke` |

The source-staging bucket is Cloud Build's default unless a custom bucket is
configured. Cloud Build also needs access to its build-log destination; the
default same-project build account has that access. If your organization uses
a custom build service account, source bucket, or log bucket, its administrator
must grant the equivalent Cloud Storage permissions on those resources.

Google Cloud references: [Service Usage access control](https://cloud.google.com/service-usage/docs/access-control), [Cloud Run deployment permissions](https://cloud.google.com/run/docs/reference/iam/roles), [Secret Manager access control](https://cloud.google.com/secret-manager/docs/access-control), [Artifact Registry roles](https://cloud.google.com/artifact-registry/docs/access-control), and [Cloud Build roles](https://cloud.google.com/build/docs/iam-roles-permissions).

The rows that set IAM policy are administrative mutations. They are present
because `deploy.sh` creates and rotates secrets and configures private-service
invocation. If your customer deployment identity must not change IAM, provision
the secrets and the `secretmanager.versions.access`/`run.routes.invoke`
permissions beforehand, then
remove those policy-setting commands from the deployment workflow.

## Prerequisite: enable project APIs once

An administrator must enable these APIs once for the project:

```bash
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.com \
discoveryengine.googleapis.com \
--project PROJECT_ID
```

## Deploy (and update) the A2A server

```bash
gcloud/deploy.sh --build
```

On the first deployment, the script prompts for the ADB user, password, and
connect descriptor. It stores them in Secret Manager under names based on the
Cloud Run service, and grants only the runtime service account access. The
container receives the values as `SELECT_AI_USER`, `SELECT_AI_PASSWORD`, and
`SELECT_AI_DB_CONNECT_STRING`; they are never placed in the image or source
tree.

### Optional: Autonomous Database mTLS wallet

The Select AI SDK already supports `wallet_location` and `wallet_password`.
For Cloud Run, pass the path to the downloaded Autonomous Database wallet ZIP
on the first deployment (or when replacing it):

```bash
gcloud/deploy.sh --wallet-archive /path/to/Wallet_database.zip
```

The script prompts for the wallet password, stores the ZIP and password as
service-specific Secret Manager secrets, and grants access only to the runtime
service account. Cloud Run mounts the ZIP read-only; its A2A launcher expands it
into ephemeral `/tmp` storage before starting the SDK, verifies it contains
`ewallet.pem`, and sets `SELECT_AI_WALLET_LOCATION` to that file's directory.
Do not commit the wallet ZIP or put its contents in the image.

Later deploys reuse the wallet. To replace it, pass `--wallet-archive` again.

The first deployment needs `--build` (or an explicit `--image-uri`). Later
deployments reuse the image already deployed to the service, so changing Cloud
Run configuration or secrets does not create another image. The command
deploys private Cloud Run, sets the final public URL in the Agent Card, grants your active
gcloud identity and Gemini Enterprise Discovery Engine service agent the
`run.routes.invoke` permission for this Cloud Run service.

The default Cloud Run service is `oracle-a2a-agent`. Its default Agent Team,
installed in Oracle Database, is `ORACLE_AI_DATABASE_AGENT`. Override either
with explicit options:

```bash
gcloud/deploy.sh --service sales-analyst-a2a --a2a-team SALES_ANALYST
```

Use a distinct `--service` value for each A2A team. Each service gets distinct Secret
Manager secret names by default, so credentials remain attached to that A2A
server.

`--max-instances` controls the number of Cloud Run containers. Each container
can use up to 10 Oracle connections by default; change that limit with
`--pool-max-size`, for example `gcloud/deploy.sh --pool-max-size 20`.

### Update the Select AI SDK or this repository

Update the checkout (or modify its dependency version), then explicitly build
and deploy the new image:

```bash
git pull
gcloud/deploy.sh --build
```

`--build` creates a freshly tagged image from the current source; without it,
the existing image is reused. Existing database secrets are reused without
prompting. To rotate the ADB credentials, explicitly request it:

```bash
gcloud/deploy.sh --rotate-db-credentials
```

### What `cloudbuild.yaml` does

`gcloud/deploy.sh --build` uses `gcloud/cloudbuild.yaml` to tell Cloud Build to build
`docker/Dockerfile` and push it to Artifact Registry. It is build configuration,
not a command you run. The build context is the repository root, so the image
can install the Select AI source from `pyproject.toml` and `src/`.

### Cloud Build upload contents

Before the build starts, `gcloud builds submit` archives and uploads the
repository root. The root `.gcloudignore` excludes local virtual environments,
generated documentation, test data, caches, credentials, and Git metadata.
Keep `src/`, `pyproject.toml`, `docker/`, and `gcloud/` in the upload; they are
required to build the image. If the upload is unexpectedly large, check local
directories against `.gcloudignore` before running `--build` again.

After a successful deployment, the script prints the A2A Agent Card JSON.
Paste that JSON into Gemini Enterprise to register the private service. The
required Gemini Enterprise invocation permission has already been added.
| Database and team | Fixed at deployment time | Chosen at runtime for each user session |
| Public A2A service | One service for one configured team | One gateway that presents an A2UI connection form |
| Users | All requests use the deployed database identity | Any permitted user can connect to a reachable Oracle database and Select AI team |
| Architecture | One Cloud Run service | Cloud Run gateway, plus Consul and worker replicas in GKE |
| Session isolation | Shared service database pool | One child process and async pool per active user session |
| Main benefit | Simple, predictable deployment | Dynamic, multi-database and multi-team access from one A2A endpoint |
| Operational cost | Low | Higher: GKE workers, Consul, routing, TTL, and session capacity |

Use [standalone](standalone/README.md) when a service should expose one known
database team. Use [gateway](gateway/README.md) when users must dynamically
choose their database connection and team.
159 changes: 159 additions & 0 deletions gcloud/gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Dynamic gateway deployment

Dynamic gateway mode exposes one public A2A endpoint. Each user dynamically
selects an Oracle database connection and Select AI team through the A2UI
connection form. A session remains available for 15 minutes by default. Set a
different lifetime in seconds with `--session-ttl-seconds`; for example,
`--session-ttl-seconds 1800` keeps sessions for 30 minutes.

```text
┌──────────────────────┐
│ A2A / Gemini client │
└──────────┬───────────┘
│ public A2A
v
┌──────────────────────┐
│ Cloud Run gateway │
└──────┬───────┬───────┘
│ │ private VPC: mTLS request to worker hostname
│ │
│ │ ┌─────────────────────── GKE ───────────────────────┐
│ └────>│ [Headless Service + managed VPC DNS] │
│ │ worker hostname → current worker Pod IP │
│ │ │ │
│ │ v │
│ │ [StatefulSet worker-0 / worker-1 / ...] │
│ │ session child process → Oracle Database │
│ │ │
│ │ [Consul] │
└────────────>│ selects healthy worker; returns worker hostname │
└───────────────────────────────────────────────────┘
```

The gateway is the only public A2A application. Consul and workers are a GKE
clustered service: Consul selects a worker for each new dynamic session, and
the chosen worker retains that session's process and Oracle conversation.

## Deploy the complete stack

Run this from the repository root:

```bash
gcloud/gateway/deploy.sh --project PROJECT_ID
```

The script creates the Artifact Registry repository and GKE Autopilot cluster
when they do not already exist. The cluster is created with
GKE additive VPC DNS: GKE owns the worker DNS records and keeps them current
when a worker Pod is recreated. Cloud Build then:

1. builds the existing `docker/Dockerfile` image once;
2. deploys the GKE namespace and internal Consul service;
3. deploys the requested number of GKE worker replicas using
`select-ai a2a worker`;
4. deploys the same image to Cloud Run using `select-ai a2a gateway`;
5. sets the final Cloud Run URL in `AGENT_URL` for the Agent Card.

Common options:

```bash
gcloud/gateway/deploy.sh \
--project PROJECT_ID \
--region us-central1 \
--cluster select-ai-a2a-gateway \
--gke-dns-domain select-ai-a2a-gateway.internal \
--worker-replicas 3 \
--network default \
--subnet default
```

The Cloud Run gateway uses direct VPC egress to reach the internal Consul load
balancer and GKE worker pod addresses. The default one-instance gateway limit
is intentional: gateway A2A task and context/session state is currently in
memory. Workers, rather than the gateway, provide the clustered capacity for
dynamic sessions.

`cloudbuild.yaml` is the complete build and deployment workflow. It supplies
the generated image and Consul endpoint values to the Cloud Run gateway at
deployment time.

## Optional worker mTLS test mode

Local testing does not use mTLS. The default GCloud deployment also keeps the
current private-VPC HTTP worker transport.

For a short-lived GCloud mTLS test:

```bash
gcloud/gateway/deploy.sh \
--project PROJECT_ID \
--enable-worker-mtls \
--mtls-cert-validity-days 365
```

This mode is intentionally self-contained and is not a production PKI design.
The deployment script generates an ephemeral CA and leaf certificates valid for
365 days by default, then removes the CA private key from its restricted
temporary directory. It never prints or stores that key. The leaf material is
first stored in Google Secret Manager. Cloud Build then creates the Kubernetes
Secrets used by the workers.

Set `--mtls-cert-validity-days DAYS` to choose the lifetime for both leaf
certificates: the gateway client certificate and the worker server certificate.
The CA is issued for one additional day.

The first mTLS deployment creates these certificates. Later mTLS deployments
reuse them, including when changing `--worker-replicas`. To deliberately
replace the CA and both leaf certificates, add `--rotate-worker-mtls`. Rotation
recreates the worker StatefulSet and ends active worker sessions.

Workers run as a StatefulSet. The `select-ai-worker` headless Service gives
each worker a stable name, for example
`select-ai-worker-0.select-ai-worker.select-ai-gateway.svc.select-ai-a2a-gateway.internal`.
Consul registers that name, so the gateway reaches the exact worker that owns a
session. GKE Cloud DNS updates its Pod-IP record automatically after a worker
is recreated. There is no worker load balancer, custom Cloud DNS zone, or
deployment-time Pod-IP snapshot.

`--gke-dns-domain` must be unique in the VPC and cannot end in `.local`. It is
immutable after cluster creation. Autopilot supports additive VPC DNS only when
the cluster is created, so an older cluster without it cannot be reused by this
deployment. Use a new `--cluster` name for the first migration, verify it, then
delete the old cluster when you are ready.

If existing mTLS material was issued for a different GKE DNS domain, the script
replaces it automatically before deploying the replacement cluster.

### How a worker DNS name is decided

GKE gives a StatefulSet Pod a DNS name using this form:

```text
<pod>.<headless-service>.<namespace>.svc.<gke-dns-domain>
```

For this deployment, worker 0 is:

```text
select-ai-worker-0.select-ai-worker.select-ai-gateway.svc.select-ai-a2a-gateway.internal
```

`select-ai-worker-0` is the StatefulSet Pod name, `select-ai-worker` is the
headless Service, `select-ai-gateway` is the Kubernetes namespace, and
`select-ai-a2a-gateway.internal` is the `--gke-dns-domain` value. GKE updates
the resulting record when the Pod IP changes.

### Certificate mounts

Worker certificate files are mounted from Kubernetes Secrets:

| Container file | Kubernetes Secret | Secret key | Used for |
| --- | --- | --- | --- |
| `/var/run/select-ai-mtls/tls.crt` | `select-ai-worker-server-tls` | `tls.crt` | worker HTTPS server certificate |
| `/var/run/select-ai-mtls/tls.key` | `select-ai-worker-server-tls` | `tls.key` | worker HTTPS private key |
| `/var/run/select-ai-mtls/gateway-ca.crt` | `select-ai-gateway-client-ca` | `ca.crt` | validates the gateway client certificate |

The Cloud Run gateway certificate files are mounted from Google Secret Manager.

The identity that submits Cloud Build needs permission to use GKE, Cloud Run,
and Secret Manager. GKE maintains the managed worker DNS records.
Loading