Context
The eigenoid org needs GCP infrastructure to run services (Cloud Run, Cloud SQL), CI/CD pipelines (Terraform via WIF), and platform tools (safe-settings, Artifact Registry). The key decisions are:
- How many GCP projects? A single project is simple but offers no isolation between environments. Multiple projects isolate billing, IAM, quotas, and blast radius.
- How do pipelines authenticate? Static credentials (service account keys) are a security risk. WIF enables ephemeral tokens with no stored secrets.
- How are resources named? Without a convention, resources from different stacks and environments collide or become confusing.
Decision
We adopt a 3 independent GCP projects architecture, one per environment, with Workload Identity Federation for authentication and strict naming conventions.
Projects
| Environment | GCP Project | Purpose |
|---|---|---|
| Development | eigenoid-dev | Development, fast iteration, auto-deploy on PRs |
| Quality Assurance | eigenoid-qa | Pre-production validation, approval gates |
| Production | eigenoid-prd | Production services, safe-settings, Artifact Registry |
Each project has its own billing account assignment, IAM policies, and quotas. An incident in dev does not affect prd's billing or availability.
Workload Identity Federation
Each project has a WIF pool with an identical structure:
| Resource | Value |
|---|---|
| Pool ID | github |
| Provider ID | eigenoid |
| Issuer | https://token.actions.githubusercontent.com |
| Attribute condition | assertion.repository_owner == 'eigenoid' |
The pool allows GitHub Actions from the eigenoid org to authenticate with GCP without secrets. The attribute_condition restricts access to the org — no external workflow can use these pools.
Service Accounts
Each project has dedicated service accounts per function:
| SA | Naming | Purpose |
|---|---|---|
| Terraform CI | terraform-ci@eigenoid-{env}.iam.gserviceaccount.com | Terraform plan/apply via WIF (IaC repos) |
| Platform bootstrap | platform-bootstrap@eigenoid-{env}.iam.gserviceaccount.com | Automatic state bucket creation (governance) |
| Deploy CI | deploy-ci@eigenoid-{env}.iam.gserviceaccount.com | Build + deploy services (svc-* repos) |
terraform-ci has an org-wide binding (any eigenoid repo can authenticate). deploy-ci has per-repo bindings (only explicitly listed repos can authenticate). platform-bootstrap is exclusive to the governance workflows in platform-settings.
Naming conventions
Terraform state buckets
{prefix}-{stack_name}-tfstate-{env}
- Prefix:
eigenoid-2cea55(unique org identifier + hash) - Stack name: from the consumer's
terraflow.yaml(e.g.,foundation,platform) - Env:
dev,qa,prd
Example: eigenoid-2cea55-foundation-tfstate-dev
Artifact Registry
{location}-docker.pkg.dev/{project}/{repository_id}/{image}:{tag}
Example: europe-west1-docker.pkg.dev/eigenoid-prd/safe-settings-docker/safe-settings:2.1.20-rc.3
Region
All regional resources use europe-west1 (Belgium). The choice is based on latency for the team and service availability.
Centralized config
Each project's data (project ID, project number, WIF provider, SA email, bucket prefix) is centralized in platform-actions/config/environments.yaml. Consumers do not hardcode project IDs — the producer resolves them at runtime.
environments:
dev:
project_id: eigenoid-dev
project_number: "620520745421"
wif_provider: "projects/620520745421/locations/global/workloadIdentityPools/github/providers/eigenoid"
sa_email: "terraform-ci@eigenoid-dev.iam.gserviceaccount.com"
state_bucket_prefix: eigenoid-2cea55
region: europe-west1
qa:
project_id: eigenoid-qa
project_number: "196546244286"
# ...
prd:
project_id: eigenoid-prd
project_number: "110876616647"
# ...
Consequences
- Full per-environment isolation: IAM, billing, quotas, and resources live in separate projects. A permissions error in dev does not escalate to prd.
- Controlled blast radius: an accidental
terraform destroyin dev does not affect prd. Each project has its own state and its own SAs. - Granular billing: each project can be assigned to an independent billing account or to the same one with per-project cost labels.
- Management overhead: 3 projects mean tripling the WIF, SA, and API configuration. Mitigated by: Terraform manages all configuration, and the producer centralizes the data.
- Guaranteed consistency: the same Terraform code is applied across all 3 environments with different
tfvars. Differences are in values only, not structure. - WIF secure by design: tokens are ephemeral (~1h), no secrets in GitHub, and the
attribute_conditionrestricts access to the org. No risk of key leaks. - Strong conventions: the naming convention for buckets, SAs, and registries is predictable. An unknown bucket can be traced to its stack and environment by name alone.
Alternatives considered
- A single GCP project for all environments: simple to manage but no isolation. An IAM error (e.g., accidental
roles/ownergrant to a SA) affects all environments. Unacceptable for production. - Projects per stack instead of per environment: (e.g.,
eigenoid-foundation,eigenoid-platform). Better for stack isolation but worse for environment isolation. A broken apply on foundation-dev could affect foundation-prd if they share a project. The per-environment model is more standard. - Service account keys instead of WIF: simpler to configure but requires storing secrets in GitHub, rotating them manually, and monitoring for leaks. WIF eliminates all of this.
- One WIF pool with multiple providers: possible but unnecessary. One provider per pool is simpler and sufficient for the current org.
References
- ADR-0004 — GitHub App as default for pipeline tokens — GitHub authentication without PATs
- ADR-0008 — Shared IaC model with Terraform — producer/consumer model that uses these projects
- ADR-0012 — Auto-bootstrap state buckets — automatic bucket creation across all 3 projects
eigenoid/iac-foundation— Terraform managing WIF, SAs, and APIs in each projecteigenoid/platform-actions— centralized environment config