Central infrastructure repo for a shared GCP/Firebase project. Manages all cloud resources via Terraform so that app repos (webhook, etc.) contain zero infrastructure code.
firebase-cloud/ # THIS REPO
├── terraform/
│ ├── main.tf # Providers, project setup, shared WIF pool
│ ├── modules/
│ │ ├── project-setup/ # GCP APIs, Firebase, Auth config
│ │ ├── wif-pool/ # Shared Workload Identity Pool
│ │ ├── app-identity/ # Per-app: SA + WIF provider + IAM
│ │ ├── artifact-registry/ # Shared Docker image registry
│ │ ├── firestore-databases/ # Per-app named Firestore database
│ │ ├── hosting/ # Per-app Firebase Hosting site
│ │ ├── cloud-run/ # Per-app Cloud Run service
│ │ └── bigquery/ # Per-app BigQuery dataset
│ ├── apps/
│ │ ├── hooklab.tf # Hooklab app configuration
│ │ └── _template.tf.example # Copy this to onboard a new app
│ ├── outputs.tf
│ ├── variables.tf
│ └── terraform.tfvars.example
└── .github/workflows/
└── terraform.yml # Plan on PR, apply on merge to main
Each app repo (e.g. webhook) has no terraform — it only uses GitHub secrets produced by this repo (WIF_PROVIDER, GCP_SA_EMAIL) to deploy via its own CI/CD.
| Tool | Version | Install |
|---|---|---|
| Terraform | >= 1.9 | brew install terraform |
| Google Cloud SDK | latest | brew install google-cloud-sdk |
gh CLI |
latest | brew install gh |
You need an existing GCP project with billing enabled. If you don't have one:
gcloud projects create YOUR_PROJECT_ID --name="Firebase Cloud"
gcloud billing projects link YOUR_PROJECT_ID --billing-account=BILLING_ACCOUNT_IDYour Google account needs these roles on the GCP project:
| Role | Why |
|---|---|
roles/owner or the specific roles below |
Simplest for initial setup |
roles/iam.workloadIdentityPoolAdmin |
Create/manage WIF pools and providers |
roles/iam.serviceAccountAdmin |
Create service accounts |
roles/iam.serviceAccountUser |
Bind SAs to resources |
roles/resourcemanager.projectIamAdmin |
Grant IAM roles to SAs |
roles/firebase.admin |
Enable Firebase, manage hosting sites |
roles/serviceusage.serviceUsageAdmin |
Enable GCP APIs |
roles/run.admin |
Create Cloud Run services |
roles/artifactregistry.admin |
Create AR repos |
roles/bigquery.admin |
Create datasets |
roles/storage.admin |
Create TF state bucket |
How to grant:
# Option A: Owner (simplest for solo/small team)
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="user:you@example.com" \
--role="roles/owner"
# Option B: Granular roles (recommended for teams)
for role in \
roles/iam.workloadIdentityPoolAdmin \
roles/iam.serviceAccountAdmin \
roles/iam.serviceAccountUser \
roles/resourcemanager.projectIamAdmin \
roles/firebase.admin \
roles/serviceusage.serviceUsageAdmin \
roles/run.admin \
roles/artifactregistry.admin \
roles/bigquery.admin \
roles/storage.admin; do
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="user:you@example.com" \
--role="$role"
doneThe firebase-cloud repo itself needs a bootstrap service account for Terraform to run in CI. This SA is separate from the per-app SAs.
# Create the infra SA
gcloud iam service-accounts create terraform-infra \
--project=YOUR_PROJECT_ID \
--display-name="Terraform Infrastructure CI/CD"
# Grant it the same roles as above
for role in \
roles/iam.workloadIdentityPoolAdmin \
roles/iam.serviceAccountAdmin \
roles/iam.serviceAccountUser \
roles/resourcemanager.projectIamAdmin \
roles/firebase.admin \
roles/serviceusage.serviceUsageAdmin \
roles/run.admin \
roles/artifactregistry.admin \
roles/bigquery.admin \
roles/storage.admin; do
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:terraform-infra@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--role="$role"
doneThen set up WIF for this repo (the infra repo itself):
# Create a separate WIF provider for the infra repo
# (or reuse the pool created by Terraform on first local apply)
# After first local apply, the pool exists. Then:
gcloud iam service-accounts add-iam-policy-binding \
terraform-infra@YOUR_PROJECT_ID.iam.gserviceaccount.com \
--project=YOUR_PROJECT_ID \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/YOUR_ORG/firebase-cloud"
# Create a WIF provider for this repo
gcloud iam workload-identity-pools providers create-oidc infra-github \
--project=YOUR_PROJECT_ID \
--location=global \
--workload-identity-pool=github-actions-pool \
--display-name="Infra Repo GitHub OIDC" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
--attribute-condition="assertion.repository == 'YOUR_ORG/firebase-cloud'"Then set these GitHub secrets on the firebase-cloud repo:
| Secret | Value |
|---|---|
INFRA_WIF_PROVIDER |
projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-actions-pool/providers/infra-github |
INFRA_GCP_SA_EMAIL |
terraform-infra@YOUR_PROJECT_ID.iam.gserviceaccount.com |
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_IDgsutil mb -p YOUR_PROJECT_ID -l us-central1 gs://YOUR_PROJECT_ID-tf-state
gsutil versioning set on gs://YOUR_PROJECT_ID-tf-stateThen uncomment the backend "gcs" block in terraform/main.tf.
cd terraform
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your valuesterraform init
terraform plan # Review changes
terraform apply # Apply changesAfter apply, copy the per-app outputs into each app repo's GitHub secrets:
# Get hooklab outputs
terraform output hooklab_wif_provider
terraform output hooklab_gcp_sa_email
# Set them on the webhook repo
gh secret set WIF_PROVIDER -R YOUR_ORG/webhook --body "$(terraform output -raw hooklab_wif_provider)"
gh secret set GCP_SA_EMAIL -R YOUR_ORG/webhook --body "$(terraform output -raw hooklab_gcp_sa_email)"The workflow (.github/workflows/terraform.yml) handles this automatically:
| Event | Action |
|---|---|
| PR opened/updated | terraform plan — posts plan as PR comment |
Push to main |
terraform apply — applies changes, outputs to job summary |
- Create GCP project with billing
- Run locally once to bootstrap (state bucket, WIF pool)
- Create
terraform-infraSA (see Permissions section) - Create WIF provider for this repo (see Permissions section)
- Set
INFRA_WIF_PROVIDERandINFRA_GCP_SA_EMAILas GitHub secrets - Uncomment
backend "gcs"inmain.tf - Create a
productionenvironment in GitHub repo settings (for apply approval)
- Copy
terraform/apps/_template.tf.exampletoterraform/apps/<app-name>.tf - Fill in the app name, GitHub repo, and modules needed
- Open a PR — review the Terraform plan
- Merge — CI applies, creates SA + WIF + resources
- Copy outputs to the app repo's GitHub secrets:
gh secret set WIF_PROVIDER -R YOUR_ORG/new-app-repo \
--body "$(terraform output -raw newapp_wif_provider)"
gh secret set GCP_SA_EMAIL -R YOUR_ORG/new-app-repo \
--body "$(terraform output -raw newapp_gcp_sa_email)"- The app repo's CI/CD now works with keyless auth to the shared GCP project.
If you already have resources created manually or from another Terraform state:
# Import the WIF pool
terraform import module.wif_pool.google_iam_workload_identity_pool.github \
projects/YOUR_PROJECT_ID/locations/global/workloadIdentityPools/github-actions-pool
# Import a service account
terraform import module.hooklab_identity.google_service_account.ci_cd \
projects/YOUR_PROJECT_ID/serviceAccounts/hooklab-ci-cd@YOUR_PROJECT_ID.iam.gserviceaccount.com
# Import Firestore database
terraform import module.hooklab_firestore.google_firestore_database.app \
projects/YOUR_PROJECT_ID/databases/hooklab
# Import Cloud Run service
terraform import module.hooklab_cloud_run.google_cloud_run_v2_service.default \
projects/YOUR_PROJECT_ID/locations/us-central1/services/hooklab-api
# After all imports, verify:
terraform plan # Should show no changes- WIF Pool limit: Max 100 providers per pool. Fine for <100 apps.
- WIF Pool soft-delete: Deleted pools have a 30-day grace period. Cannot reuse the same ID.
- IAM eventual consistency: New SA + WIF bindings take up to 60s to propagate. First deploys may fail.
- Terraform state chicken-and-egg: GCS bucket must exist before
terraform initwith remote backend. Bootstrap locally first. - Terraform apply ordering: Must apply here before an app repo's first CD run.
attribute_conditionis exact match: Case-sensitive. Must match exactly what GitHub sends.- Cross-repo secret rotation: Rotating SA/WIF breaks app repos until secrets are updated.