Skip to content

Workspaces

A workspace is a directory containing OpenTofu (or Terraform) files. Yaffle runs plan and apply independently for each workspace.

[[workspaces]]
path = "infra/network"
environments = ["*"]
[[workspaces]]
path = "infra/compute"
environments = ["*"]

Each workspace gets its own state file, runs independently, and can have its own variables.

Most projects have multiple workspaces:

repo/
├── infra/
│ ├── network/ # VPC, subnets, security groups
│ ├── database/ # RDS, ElastiCache
│ └── compute/ # ECS, Lambda
└── yaffle.toml
[[workspaces]]
path = "infra/network"
environments = ["*"]
[[workspaces]]
path = "infra/database"
environments = ["*"]
[[workspaces]]
path = "infra/compute"
environments = ["*"]

Workspaces often depend on each other. Your compute layer needs outputs from your network layer.

Yaffle automatically detects dependencies by scanning your OpenTofu files for module references to other workspaces. When workspace A depends on workspace B:

  1. B runs first
  2. B’s outputs become available as a module
  3. A runs and can consume B’s outputs
network database compute

Reference another workspace’s outputs using the Yaffle module registry:

infra/compute/main.tf
module "network" {
source = "yaffle.dev/acme--platform/infra--network/yaffle"
}
resource "aws_ecs_service" "app" {
# Use outputs from the network workspace
network_configuration {
subnets = module.network.private_subnet_ids
security_groups = [module.network.app_security_group_id]
}
}

The module source follows this pattern:

yaffle.dev/YAFFLE-ORG--REPO/WORKSPACE--PATH/yaffle

Where:

  • YAFFLE-ORG--REPO identifies the producer repository
  • WORKSPACE--PATH uses -- as the path separator

Examples:

  • infra/networkinfra--network
  • apps/web/infraapps--web--infra

A workspace can expose two different output surfaces:

  • internal outputs are available only to downstream workspaces in the same repo
  • public outputs are available to explicitly allowlisted workspaces in other repos in the same Yaffle org

Outputs are internal by default. You only need to configure the outputs you want to make public.

[[workspaces]]
path = "platform/eks"
environments = ["main"]
outputs.cluster_endpoint = { visibility = "public", consumers = ["applications/apps/*"] }
outputs.cluster_ca = { visibility = "public", consumers = ["applications/apps/*"] }

This means:

  • workspaces in the same repo can still consume the full module surface
  • allowlisted cross-repo consumers only see the public outputs
  • cross-org module sharing is not supported

Yaffle treats module sources differently depending on which repo they point at.

# Same repo -> included in the DAG
module "shared" {
source = "yaffle.dev/acme--applications/infra--shared/yaffle"
}
# Different repo in the same Yaffle org -> external module, not a DAG edge
module "cluster" {
source = "yaffle.dev/acme--platform/platform--eks/yaffle"
}

Rules:

  • same-repo modules participate in dependency inference, ordering, and blast-radius analysis
  • cross-repo modules resolve through the registry but do not become DAG edges in the current repo
  • cross-org module sharing is denied

Yaffle builds a dependency graph and executes workspaces in the right order:

network network-monitoring database compute app-config

Root workspaces (no dependencies) run first, in parallel. Downstream workspaces wait for their dependencies to complete.

When you open a PR or push to a branch, Yaffle executes runs through several stages:

Trigger received Runner acquired Plan complete Approved Rejected Apply complete Apply error Plan error pending planning waiting_approval applying cancelled success failed
StatusDescription
pendingRun is queued, waiting for a runner
planningOpenTofu plan is executing
waiting_approvalPlan complete, awaiting human approval
applyingApply in progress (after approval)
successRun completed successfully
failedRun failed with error
cancelledRun was cancelled or rejected

Set variables per workspace:

[[workspaces]]
path = "infra/compute"
environments = ["*"]
variables.instance_type = "t3.medium"
variables.desired_count = 2

Use template variables for environment-specific values:

[[workspaces]]
path = "infra/compute"
environments = ["*"]
variables.environment = "{{ environment }}"
variables.domain = "{{ environment }}.example.com"

Control which environments a workspace runs in:

# Runs in all environments (including previews)
[[workspaces]]
path = "infra/network"
environments = ["*"]
# Only production
[[workspaces]]
path = "infra/production-only"
environments = ["production"]
# Only named environments (not previews)
[[workspaces]]
path = "infra/staging-and-prod"
environments = ["staging", "production"]