Free — no signup required

Strategy 3: Cell-Based (Directory per Tenant)

3 min read

The Gold Standard for Scale

For large-scale SaaS platforms, the industry best practice is to generate a separate directory — and therefore a completely separate Terraform root module with its own state file — for every tenant. This is what "cell-based architecture" means in IaC terms.

The directory structure looks like this:

infrastructure/
├── modules/
│   └── tenant_stack/        # Shared module: VPC, DB, IAM, etc.
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── tenants/
    ├── client-acme/
    │   ├── terragrunt.hcl   # Or main.tf with backend config
    │   └── tenant.auto.tfvars
    ├── client-beta/
    │   ├── terragrunt.hcl
    │   └── tenant.auto.tfvars
    └── client-gamma/
        ├── terragrunt.hcl
        └── tenant.auto.tfvars

Each tenant directory is a thin wrapper that calls the shared module with tenant-specific variables. Using Terragrunt (a thin wrapper around Terraform), a tenant's config is just a handful of lines: a pointer to the shared module, and the tenant's own inputs (name, size, region, instance class). Terragrunt automatically configures a unique remote state backend for each directory, so client-acme and client-beta have completely separate state files with zero shared configuration.

The Automated Onboarding Workflow

The real power of this pattern is that tenant directories can be generated programmatically. When a new customer signs up, your backend API can trigger a script that copies a template directory into a new tenant-named directory, writes that tenant's specific variables into it, and commits and pushes the result to Git — which is all it takes to trigger the pipeline, since the CI/CD system (GitHub Actions, GitLab CI, Spacelift, Atlantis) detects which tenant directories changed in the commit and runs terraform apply only for those directories. This gives you:

  • Targeted deployments: Updating the shared module only applies to tenants you explicitly re-run.
  • Parallel execution: CI can apply 10 tenant directories simultaneously.
  • Full audit trail: Every tenant's infrastructure change is a Git commit.

Comparing All Three Strategies

Dimension Module per Tenant Workspace per Tenant Directory per Tenant
State isolation ❌ Shared ✅ Separate ✅ Separate
Blast radius All tenants One tenant One tenant
Plan speed at scale ❌ Very slow ✅ Fast ✅ Fast
Onboarding automation ✅ Easy (add map entry) ⚠️ Script required ⚠️ Script required
Drift visibility ✅ Single plan shows all ❌ No native view ✅ Git history per tenant
Security boundary ❌ Shared credentials ❌ Shared credentials ✅ Can use separate IAM roles
Recommended scale < 20 tenants 20–200 tenants 200+ tenants
Interview Tip

A common interview question is: "What's the difference between Terraform workspaces and separate state files?" The key distinction is that workspaces are a convenience feature for state isolation within a single backend configuration — they share the same credentials, the same backend, and the same code. Separate directories with separate backend configurations can use different AWS accounts, different IAM roles, and different backend buckets entirely. For true security isolation (e.g., SOC 2 compliance requiring tenant data to be in separate AWS accounts), workspaces are insufficient; you need separate root modules with separate provider configurations.

Key Point: The directory-per-tenant pattern is the most operationally complex to set up but the most scalable and secure to operate. The upfront investment in automation (the onboarding script, the CI/CD pipeline) pays dividends at scale. Teams that start with the module-per-tenant approach almost always migrate to directory-per-tenant as they grow — doing that migration with 500 tenants in production is painful. If you anticipate significant growth, start with directory-per-tenant from day one.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free