Skip to main content

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

EnvironmentGCP ProjectPurpose
Developmenteigenoid-devDevelopment, fast iteration, auto-deploy on PRs
Quality Assuranceeigenoid-qaPre-production validation, approval gates
Productioneigenoid-prdProduction 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:

ResourceValue
Pool IDgithub
Provider IDeigenoid
Issuerhttps://token.actions.githubusercontent.com
Attribute conditionassertion.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:

SANamingPurpose
Terraform CIterraform-ci@eigenoid-{env}.iam.gserviceaccount.comTerraform plan/apply via WIF (IaC repos)
Platform bootstrapplatform-bootstrap@eigenoid-{env}.iam.gserviceaccount.comAutomatic state bucket creation (governance)
Deploy CIdeploy-ci@eigenoid-{env}.iam.gserviceaccount.comBuild + 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"
# ...
yaml

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 destroy in 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_condition restricts 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/owner grant 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