---
title: "Platform Services Environment"
slug: gcp-env-platform-services
section: GCP Infrastructure
status: stable
description: "This page describes the GCP project `alliance-platform-services` (project number 283996200641) and its Terraform folder `environments/platform-services` in `alliance-gcp-infra`."
last-updated: 2026-10-07
---

# Platform Services Environment

This page describes the GCP project `alliance-platform-services` (project number 283996200641) and its Terraform folder `environments/platform-services` in `alliance-gcp-infra`. **Everything here comes from Terraform code and its comments only. The project could not be read live, so nothing on this page has been confirmed against the running project.** The other environments are covered in `/docs/gcp-env-development`, `/docs/gcp-env-qa`, `/docs/gcp-env-production`, `/docs/gcp-env-mcp` and `/docs/gcp-env-vertex`; the overall layout is in `/docs/gcp-overview`.

## Verification status

- The live collection for this project returned no data. The gateway `gcp_*` tools only reach the development, qa, prod and agents environments, and the agents-scoped identity was denied on this project (project describe, service listing and Cloud Run listing all returned permission errors). Every live section in the snapshot is empty because it was not readable, not because the project is empty.
- Consequently every resource in the tables below shows as declared in Terraform and not matched live. That is an artefact of the missing access. Do not read it as "these resources do not exist"; the file comments say most of them were created for real.
- To verify: grant a viewer-level identity (for example a read-only role on the project) to the gateway, or add a gateway environment that resolves to `alliance-platform-services`, then re-run the live collection and the drift comparison (`/docs/gcp-drift-report`).

_Terraform inventory generated from `alliance-gcp-infra` at commit `fc9bd1d`. Live snapshots: `alliance-platform-services` read 2026-10-01 (nothing readable). Snapshots hold names and a few stable attributes only, never secret values._

## Terraform-managed resources

Every module call and resource block under `environments/platform-services` (23 in total), grouped by category.

### Identity and access (service accounts, IAM bindings, Workload Identity)

13 items.

| Terraform address | Module or resource type | Identifier | Defined in |
| --- | --- | --- | --- |
| `google_artifact_registry_repository_iam_member.itrequestportal_sa_writer` | `google_artifact_registry_repository_iam_member` | `roles/artifactregistry.writer -> serviceAccount:${google_service_account.itrequestportal_sa.email}` | `itrequestportal.tf:93` |
| `google_cloud_run_v2_service_iam_member.itrequestportal_invoker` | `google_cloud_run_v2_service_iam_member` | `google_cloud_run_v2_service.itrequestportal.name` | `itrequestportal.tf:297` |
| `google_cloud_run_v2_service_iam_member.itrequestportal_sa_run_developer` | `google_cloud_run_v2_service_iam_member` | `google_cloud_run_v2_service.itrequestportal.name` | `itrequestportal.tf:289` |
| `google_project_iam_member.itrequestportal_sa_cloudsql_client` | `google_project_iam_member` | `roles/cloudsql.client -> serviceAccount:${google_service_account.itrequestportal_sa.email}` | `itrequestportal.tf:199` |
| `google_project_iam_member.itrequestportal_sa_secret_accessor_stopgap` | `google_project_iam_member` | `roles/secretmanager.secretAccessor -> serviceAccount:${google_service_account.itrequestportal_sa.email}` | `itrequestportal.tf:243` |
| `google_secret_manager_secret_iam_member.itrequestportal_sa_accessor` | `google_secret_manager_secret_iam_member` | `each.value.secret_id` (one per `for_each` key) | `itrequestportal.tf:225` |
| `google_secret_manager_secret_iam_member.itrequestportal_sa_asana_token_version_adder` | `google_secret_manager_secret_iam_member` | `google_secret_manager_secret.itrequestportal["asana-token"].secret_id` | `itrequestportal.tf:267` |
| `google_secret_manager_secret_iam_member.itrequestportal_sa_gateway_api_key_accessor` | `google_secret_manager_secret_iam_member` | `google_secret_manager_secret.itrequestportal_gateway_api_key.secret_id` | `itrequestportal.tf:152` |
| `google_secret_manager_secret_iam_member.manus_agent_sa_nexus_mcp_api_key_version_manager` | `google_secret_manager_secret_iam_member` | `nexus-mcp-api-key` | `nexus-mcp-api-key-accessor.tf:18` |
| `google_service_account_iam_member.itrequestportal_sa_self_actas` | `google_service_account_iam_member` | `roles/iam.serviceAccountUser -> serviceAccount:${google_service_account.itrequestportal_sa.email}` | `itrequestportal.tf:283` |
| `google_service_account_iam_member.itrequestportal_sa_wif` | `google_service_account_iam_member` | `roles/iam.workloadIdentityUser -> principalSet://iam.googleapis.com/projects/283996200641/locations/global/workloadIdentityPools/github-actions/attribute.repository/Alliance-Strategies/itrequestportal` | `itrequestportal.tf:104` |
| `google_service_account.itrequestportal_sa` | `google_service_account` | `itrequestportal-sa` | `itrequestportal.tf:76` |
| `module.sys_n8n_sa` | `service-account` | `sys-n8n-sa` | `sys-n8n.tf:58` |

### Secrets (Secret Manager containers)

4 items.

| Terraform address | Module or resource type | Identifier | Defined in |
| --- | --- | --- | --- |
| `google_secret_manager_secret_version.itrequestportal_placeholder` | `google_secret_manager_secret_version` |  (one per `for_each` key) | `itrequestportal.tf:219` |
| `google_secret_manager_secret.itrequestportal` | `google_secret_manager_secret` | `itrequestportal-${each.key}` (one per `for_each` key) | `itrequestportal.tf:205` |
| `google_secret_manager_secret.itrequestportal_gateway_api_key` | `google_secret_manager_secret` | `itreq-65b3074a-ad4a-11f1-8eba-91643267b8fd` | `itrequestportal.tf:143` |
| `module.sys_n8n_secrets` | `secret-manager-secret-bulk` | `2 items: sys-n8n-db-password, sys-n8n-encryption-key` | `sys-n8n.tf:93` |

### Compute (Cloud Run, GKE, jobs, schedulers)

2 items.

| Terraform address | Module or resource type | Identifier | Defined in |
| --- | --- | --- | --- |
| `google_cloud_run_v2_service.itrequestportal` | `google_cloud_run_v2_service` | `itrequestportal` (one per `for_each` key) | `itrequestportal.tf:305` |
| `module.sys_n8n` | `cloud-run-service` | `sys-n8n` | `sys-n8n.tf:114` |

### Data stores (Cloud SQL, Redis, Firestore, Cloud Storage)

2 items.

| Terraform address | Module or resource type | Identifier | Defined in |
| --- | --- | --- | --- |
| `google_sql_database_instance.itrequestportal` | `google_sql_database_instance` | `itrequestportal-db` | `itrequestportal.tf:167` |
| `module.sys_n8n_db` | `cloud-sql-postgres` | `sys-n8n-db` | `sys-n8n.tf:70` |

### Networking and load balancing

1 item.

| Terraform address | Module or resource type | Identifier | Defined in |
| --- | --- | --- | --- |
| `module.sys_n8n_domain` | `domain-mapping` | `sys.n8n.allianceabroad.com` | `sys-n8n.tf:172` |

### Build, registry and delivery

1 item.

| Terraform address | Module or resource type | Identifier | Defined in |
| --- | --- | --- | --- |
| `google_artifact_registry_repository.itrequestportal` | `google_artifact_registry_repository` | `itrequestportal` | `itrequestportal.tf:82` |

## Live state

### Live state

No live snapshot could be read for `alliance-platform-services` through the gateway. Everything on this page for this project comes from Terraform only and is **not verified against the running project**.

| Read attempted | Result |
| --- | --- |
| gcloud projects describe alliance-platform-services | does not have permission to access projects instance (or it may not exist): The caller does not have permission |
| gcloud services list --enabled | Permission denied to list services for consumer container [projects/283996200641] (serviceusage.services.list) |
| gcloud run services list | PERMISSION_DENIED: run.services.list denied on namespaces/alliance-platform-services/services |
| all other sections | not attempted: gateway gcp_* tools only expose environments development, qa, prod, agents; none resolves to alliance-platform-services, and the agents-scoped id |

## What this project is for

`environments/platform-services/main.tf` describes the project as the home for singleton, stateful platform apps that are neither MCP tool servers (`alliance-mcp-services`, see `/docs/gcp-env-mcp`) nor automation agents (`alliance-platform-agents`, see `/docs/gcp-env-vertex`). It was created on 2026-08-18. There is no dev/qa/prod split here: one instance of each thing, default region `us-central1` (`variables.tf`). That differs from the `<system>-<component>-<env>` convention in `/docs/gcp-naming-conventions`; the `itrequestportal.tf` header says the repo's service-account and secret modules require an env of dev, qa or prod and so were not used for it.

Systems named in the code:
- `itrequestportal`: the Tech Request Portal (`/docs/tech-request-portal`), a Cloud Run v2 service with its own service account, Artifact Registry repository, Cloud SQL Postgres instance and Secret Manager secrets. Ingress is set to internal load balancer only. The code comments say it is fronted by a load balancer and IAP and served at `itrequests.allianceabroad.com`, but those pieces are not declared here. Its image is deployed by the application repo's own deploy workflow (`deploy.yml` under itrequestportal's `.github/workflows/`, via a repo-scoped Workload Identity binding), not by Terraform. Its Secret Manager containers are named `itrequestportal-<key>` for nine keys: `asana-token`, `asana-project-id`, `asana-assignee-id`, `asana-section-id`, `sendgrid-api-key`, `sendgrid-from-email`, `github-app-private-key`, `dev-to-qa-sync-agent-api-key` and `database-url`, plus a separately named gateway key secret (`itreq-65b3074a-...`) (`itrequestportal.tf` local `itrequestportal_secrets`; #246, #261). `dev-to-qa-sync-agent-api-key` mirrors the shared dev-to-qa-sync-agent key in its own itrequestportal-scoped container so the access grant stays least-privilege, and is exposed to the service as `DEV_TO_QA_SYNC_AGENT_API_KEY` for the QA Deployment Request feature (#246 comment and env block). The service also reads `DATABASE_URL` from the `database-url` secret (a Cloud SQL Auth Proxy unix-socket string) and `GATEWAY_API_KEY` from the gateway key secret, and carries the `run.googleapis.com/cloudsql-instances` annotation pointing at `itrequestportal-db`, with `roles/cloudsql.client` granted to the runtime SA (#261). The database, its `itrequestportal_app` user and the schema were created by hand and are not in Terraform; only the instance is (#261 comment).
- `itrequestportal` dev slot (`itrequestportal-sandbox.tf`): prod is the existing `itrequestportal` Cloud Run service. One dev slot, the Cloud Run v2 service `itrequestportal-dev-1`, serves feature/* branches. There is no QA environment and there are no release/* branches. The slot is served at `dev-1.itrequests.allianceabroad.com`. It has these resources of its own:
  - a Cloud SQL instance (`itrequestportal-dev-db`), rather than a database on `itrequestportal-db`
  - an Artifact Registry repository (`itrequestportal-dev`)
  - Secret Manager containers for `dev-database-url`, `dev-session-secret` and `dev-microsoft-oauth-client-secret`
  - a runtime service account (`itrequestportal-dev-sa`)

  A conditional IAM grant lets the dev slot connect to its own Cloud SQL instance only. Deployers are split per environment and are not the runtime service account: `itrequestportal-deployer-dev` and `itrequestportal-deployer-prod`. Workload Identity uses one pool per environment (`gh-itrequests-dev`, `gh-itrequests-prod`). Each provider is pinned to repository, ref and workflow: dev to feature/* branches and `trusted-deploy-dev.yml@main`, prod to `refs/heads/main` and `deploy.yml@main`. The file header says these resources were created with gcloud on 2026-10-07 and that the file documents live intent.
- `sys-n8n`: a dedicated n8n instance for system and agent-fleet workflows, split from the Globus business n8n instances. It runs a pinned `n8nio/n8n` image from Docker Hub, one minimum instance (so n8n's cron triggers fire), public ingress for webhooks, Cloud SQL Postgres, and a domain mapping for `sys.n8n.allianceabroad.com`. It was first built in `alliance-mcp-services` on 2026-09-23 and moved here the same day (per the `sys-n8n.tf` header). Because the agent workflows moved onto it call other Cloud Run services, `sys-n8n-sa` is also granted `roles/run.invoker` on `security-agent` and `vertex-sync-agent` (project `alliance-platform-agents`, us-central1; `environments/vertex/security-agent-vertex-sync-agent-sys-n8n-invoker.tf`, documented as already live via gcloud on 2026-09-23) and on `gke-workload-health-agent` (us-central1; `environments/development/gke-workload-health-agent-sys-n8n-invoker.tf`) (#298). Those grants are declared in the other environments' folders, not in this one.
- Nexus MCP API key accessor (`nexus-mcp-api-key-accessor.tf`): a single IAM grant giving the service account the development-environment gateway tools impersonate `roles/secretmanager.secretVersionManager` (manage versions) on the existing `nexus-mcp-api-key` secret. The secret container itself is not declared.
- `environment-index` snapshot store and refresh triggers (`environment-index.tf`): `GET /api/environments` reads Firestore only, and a writer inside the same service fills it. The Firestore `(default)` database is in native mode in the same region as the Cloud Run service, and the service's runtime service account holds `roles/datastore.user`. The collections are `env_index_repos` and `env_index_locks`. A Cloud Scheduler job `environment-index-refresh` sends `POST /internal/refresh` every 2 minutes with an OIDC token for the `environment-index-refresher` service account, which holds no roles. GitHub repo webhooks to `/webhooks/github` use an HMAC secret held in the `environment-index-github-webhook-secret` secret. The file comment says these were created live with gcloud and are documented as import-ready intent, and that this root module is not applied by CI.
- `nexus-hub` and `platform-hub`: named in `main.tf` as tenants whose runtime service accounts, a `github-actions` WIF pool and provider, and (for `nexus-hub`) a Cloud Run service and Secret Manager secrets exist live; `itrequestportal.tf` also refers to an existing `nexus-hub-url-map` load balancer. None of these are modelled in Terraform.

## How it is deployed

- Terraform: not applied. `backend.tf` points at the bucket `platform-services-tfstate-alliance-platform-services`, and its comment says that bucket does not exist yet and `terraform init`, `import` and `apply` have never been run. `main.tf` says the same.
- CI: no workflow covers this folder. `terraform-plan-pr.yml` and `terraform-drift-detect.yml` exclude it explicitly as unapplied draft IaC, and there is no apply workflow for it (see `/docs/gcp-terraform-workflows`). Nothing automated detects drift here.
- Manual: the project and most resources were created out of band with Owner-level local `gcloud` and gateway tooling, then written up in code. The `itrequestportal.tf` header calls the file "documentation of live intent, not the source of truth". The Tech Request Portal application deploys through its own repo workflow; `nexus-hub` deploys through `deploy-prod.yml` in the `nexus-docs` repo (per `main.tf`).

## Gotchas

- Never run an untargeted plan or apply here. The `sys-n8n.tf` header warns that it would try to create everything in `itrequestportal.tf`, which already exists live, and fail with conflicts. Use `-target` limited to the `sys-n8n` modules until a reconciliation pass imports the rest.
- The `sys-n8n` bootstrap is manual: create the `n8n` database and user, then write first versions of the `sys-n8n-db-password` and `sys-n8n-encryption-key` secrets. Cloud Run will not start with empty secret containers. Secret values, database users and schemas are outside Terraform's scope, consistent with the standing rule.
- `sys-n8n` turns CPU throttling off because n8n's task runner fails to connect with throttled CPU.
- The `itrequestportal` image tag in code is a literal commit hash, while CI deploys a new image on every push to main. The service carries a `lifecycle.ignore_changes` rule on the container image so that Terraform does not fight CI over that field.
- The code comments record that the Asana token secret is rewritten on each deploy and that this was a suspect in a past "Unknown object" ticket-creation failure; a version-adder grant exists for it. The file header later records the actual cause: the token's identity (platform-bot) had never been added as a member of the itrequests Asana project, fixed live via the Asana API on 2026-09-16 and not Terraform-managed (#261). The comment on the version-adder resource itself still calls the rotation a suspect.
- The Tech Request Portal's load balancer, IAP brand and client, certificates and the GoDaddy DNS record are live but declared nowhere in this folder.

## Security observations

These come from the Terraform code only and should be reviewed against the live project once it is readable.

- Both Cloud Run services are declared with `allUsers` as invoker. For `itrequestportal` the comment says internal-load-balancer ingress is what blocks direct public access; for `sys-n8n` ingress is `all`, deliberately public to receive webhooks.
- Both Cloud SQL instances are declared with a public IPv4 address and no authorized networks, relying on the Cloud SQL Auth Proxy.
- `itrequestportal_sa` holds `roles/secretmanager.secretAccessor` at project level as an admitted temporary stopgap, which covers every secret in the project, including those of other tenants. The comment says it is redundant once the per-secret binding is applied.
- The `itrequestportal` service account can act as itself and holds a Cloud Run developer role on its service, so that CI running as it can deploy.
- Outside this folder, `environments/mcp/itrequest-mcp.tf` (alliance-mcp-services) grants `itrequestportal-sa@alliance-platform-services` `roles/secretmanager.secretAccessor` on the shared `itrequest-gateway-api-key` secret, so the portal and the `itrequest-mcp` gateway connector read one value instead of duplicated copies (#248; the file calls this a deliberate design choice).
- Project-level IAM, the WIF pool bindings and any API keys could not be read, so a review of who holds roles on this project has not been possible.

## Known gaps

- No live verification of any resource, role, API or region, as above.
- Terraform state does not exist, so Terraform and reality can diverge silently. The code itself records at least one correction (a Cloud SQL instance added 2026-09-16 and a gateway API key secret that the running service referenced but that was found not to exist, then recreated).
- Not modelled: the project, enabled APIs, the `github-actions` WIF pool and provider, `nexus-hub` and `platform-hub` runtime service accounts, the `nexus-hub` Cloud Run service and secrets, the shared load balancer and url map, IAP, and DNS. The compared-type list also marks firewall rules as not compared.
- The `itrequestportal` header says six secret containers were created live on 2026-09-01. The Terraform `for_each` list has since grown to nine names (adding `github-app-private-key`, `dev-to-qa-sync-agent-api-key` and `database-url`) plus the separately declared gateway key secret, so the live set and the code have not been reconciled.
- `/docs/disaster-recovery-plan` and `/docs/repo-inventory` were not checked line by line against this project; confirm they do not describe it as managed by Terraform.
