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​