The Project Factory Pattern
On GCP the project is the isolation unit. Billing, quota, IAM, and API enablement all key off the project, which means a project created wrong is wrong in four expensive ways at once — unlinked billing, missing APIs, the wrong IAM, the wrong folder. The act of creating a project correctly, every time, is therefore infrastructure worth automating rather than a runbook a human follows by hand.
The Project Factory pattern is a project vending machine. The community module terraform-google-modules/project-factory takes a name, a folder, a billing account, and a list of APIs, and stamps out a project with billing linked, those APIs enabled, labels applied, and the project filed under the correct folder. One module call produces a correct Hatch project; there is no place left for a human to forget a step.
Why a Factory at All
A hand-built project drifts in exactly the ways a Console-built resource does. Someone creates it in the Console, forgets to link the billing account, enables APIs ad hoc as each error surfaces, and drops it in whatever folder was selected last. The next project is built slightly differently, and now "a Hatch project" means five subtly different things. There is no diff, no review, and no record of which steps were followed.
The factory turns "a correct Hatch project" into a single reproducible module call instead of a checklist. The label schema, the mandatory APIs, the folder placement, the billing link — all of it is code, reviewed in a pull request and applied identically every time. The fiftieth project is the first project with a different name.
The project-factory Module
The module takes a name, a folder_id (one of apps, data, platform), a billing_account, an activate_apis list, and labels. It creates the project, links the billing account, enables exactly those APIs, applies the labels, and can wire a default service account and a Shared VPC attachment. The output is a project that is ready to hold resources, not an empty shell you still have to finish configuring by hand.
module "app_prod" { source = "terraform-google-modules/project-factory/google" version = "~> 17.0" name = "hatch-app-prod" folder_id = "folders/123456789012" # the apps folder billing_account = "01ABCD-2345EF-6789GH" activate_apis = [ "run.googleapis.com", "sqladmin.googleapis.com", "secretmanager.googleapis.com", ] labels = { env = "prod" team = "hatch-app" } }
That one block is the whole project. Without the factory, the same outcome is a Console session, a billing link nobody documents, and a scatter of gcloud services enable commands run until the errors stop. With it, the project is a reviewed, repeatable artifact.
Project-Per-Environment as the Isolation Unit
hatch-app-prod and hatch-app-staging are separate projects, not separate namespaces inside one project. That is the point. A separate project gives prod its own IAM policy, its own quota, and a hard billing boundary — none of which a workspace, a label, or a naming convention can provide. The isolation is structural in GCP, granted for free the moment the projects are distinct.
Collapsing two environments into one project to "manage fewer projects" throws that away. Now a quota exhaustion in staging starves prod, a bad IAM grant spans both, and billing for the two is one undifferentiated number. The project is the only hard isolation boundary GCP hands you at no cost, and the factory makes spending one per environment cheap.
Folder Placement and Inherited Policy
The factory drops each project under a folder, and the org policies and IAM bound at the folder level flow down to it automatically. Placement is policy. A project under the data folder inherits the data folder's org policies and IAM; the same project under apps inherits a different set. Getting folder_id right matters as much as the resources inside the project, because the folder decides which constraints the project is born under.
This is why a project in the wrong folder is a quiet bug. It works — until a folder-level org policy that should have constrained it does not, or one that should not bites it. The factory makes folder placement an explicit, reviewed input rather than a Console dropdown someone clicks past.
The Factory Inside a Module
Wrap the factory in a Hatch service-project module that bakes in the org's conventions — the label schema, the mandatory APIs every Hatch app needs, the log sink, the default network attachment. Then a new app project is one short call to module "service_project" with a name and an environment, not thirty lines of factory boilerplate repeated per project. The conventions live in one module; the call sites stay trivial.
This is the layout from the previous topic applied to projects: the service-project module is the reusable logic in modules/, and each envs/ directory instantiates it. Change the org's mandatory API list once in the module, and the next apply brings every project into line — there is no per-project boilerplate to chase down.
- Creating production projects by hand in the Console and only codifying the resources inside them — the project itself becomes the snowflake, with billing or APIs configured inconsistently across environments.
- Calling the factory but forgetting
activate_apis, then having every downstream resource fail with "API not enabled" because the vending machine shipped an empty project. - Putting a new project under the wrong
folder_idand silently inheriting the wrong org policies and IAM — the project works until a folder-level policy bites it. - Reusing one project across prod and staging to avoid "managing more projects" — you collapse the only hard isolation boundary GCP gives you for free, and a quota exhaustion or bad IAM grant now spans both.
- Pasting the full factory block into every environment instead of wrapping it in a
service-projectmodule — the org's conventions then drift project by project.
- Create every project through
terraform-google-modules/project-factory, wrapped in a Hatchservice-projectmodule, so billing, APIs, labels, and folder placement are uniform. - Use one project per environment per app (
hatch-app-prod,hatch-app-staging) as the isolation unit, never a shared project split by labels. - Pass the full
activate_apislist the project needs at creation so no downstream resource races an unenabled API. - Set
folder_iddeliberately and treat folder placement as a policy decision, since org policy and IAM inherit down the hierarchy. - Bake the org's label schema, mandatory APIs, and log sink into the wrapper module so a new project is a name and an environment, not boilerplate.
Project resource as the KRM equivalent
Pulumi a GCP project component
AWS Control Tower no GCP analog needed; account vending is the closest cousin
Knowledge Check
Why is creating a GCP project worth automating with a factory?
- The project is GCP's isolation unit for billing, quota, IAM, and API enablement, so a hand-built one drifts in several expensive ways at once
- A GCP project can only ever be created through Terraform running the factory module, and never through the Console UI or the gcloud command line
- The factory makes each project meaningfully cheaper to run than the same one created by hand in the Console
- GCP charges a recurring per-project fee unless that project was created by an approved factory module
What happens if you call the project factory but omit activate_apis?
- The project ships empty of enabled APIs, so downstream resources fail with "API not enabled"
- The factory quietly falls back to enabling every available GCP API by default, just to be on the safe side
- Terraform refuses to create the project at all and errors out during the plan
- The project's billing account is silently left unlinked as a side effect
Why use a separate project per environment instead of one project split by labels?
- A separate project gives each environment its own IAM, quota, and billing boundary — hard isolation a label can never provide
- Resource labels are not supported on production projects, only on dev and staging ones
- One distinct project per environment is a hard requirement baked into the factory module, which refuses to run otherwise at all
- Splitting one project's resources by label roughly doubles the monthly billing cost
Why does the folder_id the factory uses matter as much as the resources inside the project?
- Org policies and IAM bound at the folder inherit down to the project, so placement is itself a policy decision
- The folder the project sits in directly determines which region all of that project's resources end up deployed into
- A project can only be billed at all if it sits inside the designated billing folder of the org
- Folder placement directly sets the project's per-service quota limits across the board
You got correct