README
¶
TerraScale
Multi-tenant lifecycle manager for Terraform.
TerraScale wraps your existing Terraform project to provision, manage, and destroy isolated tenants — each with their own state file and configuration. No code duplication. No manual state management. No custom scripts.
terrascale add city-hospital --var project_name=emr-city --var subdomain=city-hospital
One command. Fully isolated tenant. Done.
The Problem
Every team that uses Terraform eventually needs to replicate their infrastructure for a second tenant, environment, or client. The options are all bad:
- Copy-paste the project folder — drifts apart within weeks
- Terraform workspaces — no registry, no visibility, one wrong
destroywrecks everything - Custom bash scripts — only the author understands them
TerraScale solves this. It treats your entire Terraform project as a "stamp" and provisions it repeatedly with different variables and isolated state.
Installation
Homebrew (macOS / Linux)
brew install 01x-in/tap/terrascale
From Source
go install github.com/01x-in/terrascale/cmd/terrascale@latest
Build Locally
git clone https://github.com/01x-in/terrascale.git
cd terrascale
make build
From Release
Download the binary for your platform from the Releases page.
Quick Start (5 Minutes)
# 1. Navigate to any existing Terraform project
cd examples/terrascale-site
# 2. Initialize TerraScale
terrascale init
# 3. Create terraform.tfvars for shared values (gitignored)
cat > terraform.tfvars <<EOF
project_name = "my-infra"
aws_region = "us-east-1"
domain_name = "example.com"
EOF
# 4. Add your first tenant
terrascale add tenant-1 --var subdomain=t1
# 5. Add a second tenant
terrascale add tenant-2 --var subdomain=t2
# 6. See what you've got
terrascale list
# 7. Inspect a tenant
terrascale inspect tenant-1
# 8. Tear one down (doesn't affect the other)
terrascale destroy tenant-1 --auto-approve
# 9. Confirm isolation
terrascale list
Commands
terrascale init
Scan a Terraform project and generate a terrascale.yaml configuration file.
terrascale init
- Discovers all variables from
.tffiles - Interactive prompts to classify variables as tenant-specific or shared
- Creates
.terrascale/directory and updates.gitignore
terrascale add <slug>
Provision a new tenant with isolated state.
terrascale add city-hospital \
--var project_name=emr-city \
--var subdomain=city-hospital \
--environment production \
--name "City General Hospital"
| Flag | Description | Default |
|---|---|---|
--var key=value |
Set a tenant variable (repeatable) | — |
--name |
Display name | slug |
--environment |
Environment type | production |
--auto-approve |
Skip confirmation prompt | false |
What happens under the hood:
- Resolves current AWS identity and asks for confirmation
- Creates
.terrascale/state/<slug>/for isolated state - Reads shared variable values from
terraform.tfvars - Generates
tenant.tfvarswith all variables merged - Generates backend override pointing to tenant's state directory
- Runs
terraform init→plan→apply - Captures outputs and saves everything to the registry
terrascale apply <slug>
Re-run terraform plan and apply for an existing tenant. Use this to retry a failed provisioning or pick up configuration changes.
terrascale apply production
terrascale apply production --auto-approve
| Flag | Description | Default |
|---|---|---|
--auto-approve |
Skip confirmation prompts | false |
terrascale list
Display all tenants in a table.
terrascale list
terrascale list --status=active
terrascale list --environment=production
terrascale list --json
| Flag | Description |
|---|---|
--status |
Filter by status: active, destroyed, failed, provisioning |
--environment |
Filter by environment |
--json |
Output as JSON |
terrascale inspect <slug>
Show full details for a tenant.
terrascale inspect city-hospital
terrascale inspect city-hospital --outputs-only
terrascale inspect city-hospital --json
terrascale inspect city-hospital --refresh
| Flag | Description |
|---|---|
--outputs-only |
Show only Terraform outputs |
--json |
Output as JSON |
--refresh |
Run terraform refresh before showing |
terrascale destroy <slug>
Destroy a tenant's infrastructure and clean up.
terrascale destroy city-hospital
terrascale destroy city-hospital --auto-approve
terrascale destroy city-hospital --keep-state
| Flag | Description | Default |
|---|---|---|
--auto-approve |
Skip confirmation (for CI/CD) | false |
--keep-state |
Preserve state directory for audit | false |
Configuration
TerraScale uses a single terrascale.yaml file as the source of truth. It tracks:
- Project settings — Terraform directory, project mode
- Tenant spec — which variables change per tenant, which are shared
- Tenant registry — every tenant's slug, status, variables, outputs, and timestamps
Example:
version: "1"
project:
name: my-infra
terraform_dir: "."
mode: root
state:
backend: local
tenant_spec:
tenant_variables:
- name: subdomain
type: string
required: false
prompt: Subdomain prefix for this tenant.
shared_variables:
- project_name
- aws_region
- domain_name
tenants: []
shared_variables lists variable names that are the same across all tenants. Their values are read at runtime from your project's terraform.tfvars — keeping sensitive or environment-specific values out of terrascale.yaml and out of version control.
# terraform.tfvars (gitignored)
project_name = "my-infra"
aws_region = "us-east-1"
domain_name = "example.com"
If a shared variable isn't in terraform.tfvars, Terraform falls back to the default defined in variables.tf.
$slug in defaults — use $slug as a default value for any tenant variable and it will be automatically replaced with the tenant's slug at provisioning time:
tenant_variables:
- name: project_name
type: string
required: true
default: "$slug"
terrascale add city-hospital # project_name defaults to "city-hospital"
terrascale add acme-corp # project_name defaults to "acme-corp"
The user can still override it with --var project_name=something-else.
How It Works
TerraScale is a wrapper around the terraform binary. It never modifies your .tf files.
Your Terraform Project (the stamp)
│
├── terrascale add hospital-a → .terrascale/state/hospital-a/
├── terrascale add hospital-b → .terrascale/state/hospital-b/
└── terrascale add uat-march → .terrascale/state/uat-march/
Same code. Different variables. Isolated state. That's it.
Requirements
- Go 1.22+ (to build from source)
- Terraform >= 1.0 installed and on PATH
Project Structure
terrascale/
├── cmd/terrascale/ # CLI entry point
├── internal/
│ ├── cli/ # Cobra commands
│ ├── config/ # YAML config structs + I/O
│ ├── terraform/ # TF binary wrapper, scanner, tfvars, state
│ ├── registry/ # Tenant CRUD
│ └── ui/ # Terminal output helpers
├── examples/
│ └── terrascale-site/ # TerraScale website deployment project
├── terrascale.yaml # Generated config (per-project)
└── .terrascale/ # Generated state directory (per-project)
Development
make build # Build the binary
make test # Run all tests
make install # Install to $GOPATH/bin
make lint # Run linter (requires golangci-lint)
make clean # Remove built binary
License
MIT