# Ops Webapp infrastructure bootstrap

This Pulumi project provisions the shared n=1 Airbyte Ops Webapp host at
`https://ops.internal.airbyte.ai`.

## Scope

Pulumi owns:

- Cloud Run service: `ops-webapp`
- Runtime service account and Cloud Run/load balancer IAM
- External HTTP(S) load balancer, managed TLS certificate, and HTTP to HTTPS redirect
- DNS A record for `ops.internal.airbyte.ai` in the existing `internal-airbyte-ai` Cloud DNS zone

Bootstrap owns:

- Artifact Registry repository: `us-west3-docker.pkg.dev/internal-airbyte-ai-infra/ops-webapp`
- Bootstrap image: `us-west3-docker.pkg.dev/internal-airbyte-ai-infra/ops-webapp/ops-webapp:latest`
- Secret Manager secret container, value, and runtime access grant for the Ops Webapp OAuth client secret

Pulumi does **not** own Keycloak/OAuth client configuration. The Ops Webapp Keycloak
client is managed manually.

## Stage 0: first apply blockers

The runtime infrastructure lives in `internal-airbyte-ai-infra` and the
`internal.airbyte.ai` DNS zone lives in `airbyte-intranet`.

These are the prerequisites that can block the first full `pulumi up` from
succeeding:

Set local convenience variables:

```bash
export PROJECT="internal-airbyte-ai-infra"
export REGION="us-west3"
export RUNTIME_SA="ops-webapp-sa@internal-airbyte-ai-infra.iam.gserviceaccount.com"
export REPO_ROOT="/path/to/airbyte-ops-mcp"
export OAUTH_CLIENT_SECRET_FILE="/path/to/ops-webapp-oauth-client-secret.txt"
```

1. The applying identity can administer resources in
   `internal-airbyte-ai-infra`, including enabling required Google APIs.

   ```bash
   gcloud services enable cloudresourcemanager.googleapis.com \
     --project="$PROJECT"
   ```

2. The same applying identity can add records to the `internal-airbyte-ai` DNS
   zone in `airbyte-intranet`.

   ```bash
   gcloud projects add-iam-policy-binding airbyte-intranet \
     --member=serviceAccount:github-actions-ci@internal-airbyte-ai-infra.iam.gserviceaccount.com \
     --role=roles/dns.admin
   ```

3. The `internal-airbyte-ai` DNS zone exists in `airbyte-intranet`, and there is
   no conflicting `ops.internal.airbyte.ai.` A record.
4. The Artifact Registry repository exists before pushing the bootstrap image.
   If it already exists, leave it in place and skip this command:

   ```bash
   gcloud artifacts repositories create ops-webapp \
     --project="$PROJECT" \
     --location="$REGION" \
     --repository-format=docker \
     --description="Container images for the Airbyte Ops Webapp"
   ```

5. A bootstrap image exists before Cloud Run is created:

   ```bash
   gcloud builds submit "$REPO_ROOT" \
     --project="$PROJECT" \
     --config="$REPO_ROOT/infra/ops-webapp/cloudbuild.bootstrap.yaml" \
     --substitutions=_IMAGE="${REGION}-docker.pkg.dev/${PROJECT}/ops-webapp/ops-webapp:latest"
   ```

6. The OAuth client secret container, runtime access grant, and at least one
   secret version exist before Cloud Run is created:

   ```bash
   gcloud secrets create ops-webapp-oauth-client-secret \
     --project="$PROJECT" \
     --replication-policy=automatic

   gcloud secrets add-iam-policy-binding ops-webapp-oauth-client-secret \
     --project="$PROJECT" \
     --member="serviceAccount:${RUNTIME_SA}" \
     --role="roles/secretmanager.secretAccessor"

   gcloud secrets versions add ops-webapp-oauth-client-secret \
     --project="$PROJECT" \
     --data-file="$OAUTH_CLIENT_SECRET_FILE"
   ```

The secret container, secret value, and secret IAM policy are external bootstrap
resources. Pulumi reads the secret name for Cloud Run configuration but does not
create the secret or manage its IAM policy. No other secrets are created by this
stack. Add new secrets only when Cloud Run actually consumes them.

7. Prod DB-backed pages need post-apply Prod DB grants before they can work. The
   grants are not a first-apply blocker because the runtime service account does
   not exist until Pulumi creates it. They are also intentionally not managed by
   this stack because the applying identity for `internal-airbyte-ai-infra` should
   not require IAM mutation rights in the Prod DB project.

## Stage 1: provision infrastructure

After Stage 0 is complete and this PR is merged, run the repository's Pulumi
apply workflow for the `ops-webapp` stack. Normal bootstrap does not require a
local Pulumi apply.

## Stage 2: grant Prod DB access after apply

After Pulumi creates `ops-webapp-sa`, ask a Prod DB project administrator to run
these grants:

```bash
export RUNTIME_SA="ops-webapp-sa@internal-airbyte-ai-infra.iam.gserviceaccount.com"

gcloud projects add-iam-policy-binding prod-ab-cloud-proj \
  --member="serviceAccount:${RUNTIME_SA}" \
  --role="roles/cloudsql.client"

gcloud secrets add-iam-policy-binding CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS \
  --project=587336813068 \
  --member="serviceAccount:${RUNTIME_SA}" \
  --role="roles/secretmanager.secretAccessor"
```

These commands grant the Ops Webapp runtime identity access to the Prod DB Cloud
SQL control plane and the existing connection-details secret. They do not change
the Cloud SQL instance, public/private IP settings, VPC peering, firewall rules,
or Prod DB network configuration.

The deployed app uses the Cloud SQL Python Connector against the replica's public
IP. This avoids adding a private network path from `internal-airbyte-ai-infra` to
`projects/prod-ab-cloud-proj/global/networks/prod-main-vpc`. Cloud SQL Proxy and
Tailscale are not required for Prod DB query tools. See
<https://docs.cloud.google.com/sql/docs/postgres/language-connectors>.

## Deployment workflow handoff

After Pulumi creates the service, the deploy command workflow should only build,
push, and roll out a new image for `ops-webapp`. Infrastructure creation belongs
in this Pulumi project, not the app deploy workflow.
