linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/lessons/tf-intermediate-12-multi-env-layout

lesson ── terraform-intermediate ── ~18 мин ── 6 шагов

Multi-env: workspaces vs directories

You need the same service in dev, stage, and prod. How do you lay out the code so you are not duplicating HCL? Two approaches:

  1. Workspaces, one set of HCL, you switch with terraform workspace. Simple, but risky (it is easy to apply to the wrong env).
  2. Directory per env, envs/dev/, envs/prod/. Each env has its own root with its own backend, and the shared code lives in a module.

In this lesson you build both and see the tradeoffs.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Option 1: workspaces

    bash
    cd /home/student/tf-envs/workspaces-style
    cat > main.tf <<'EOF'
    resource "random_id" "suffix" {
      byte_length = 4
    }
    locals {
      env = terraform.workspace
    }
    resource "aws_s3_bucket" "demo" {
      bucket = "linuxlab-${local.env}-${random_id.suffix.hex}"
      tags = {
        Environment = local.env
      }
    }
    output "current_workspace" {
      value = terraform.workspace
    }
    EOF
    terraform init

    Create two workspaces:

    bash
    terraform workspace new dev
    terraform workspace new prod
    terraform workspace list

    The asterisk marks the active one.

    Workspaces are separate states within one backend. With the local backend the state lives in terraform.tfstate.d/<workspace>/. With S3, under different keys.

    ✓ Two workspaces created. Each has its own state.

  2. 02

    Apply in both workspaces

    Switch to dev and apply:

    bash
    terraform workspace select dev
    terraform workspace show
    terraform apply -auto-approve
    terraform output current_workspace

    This should create a bucket named linuxlab-dev-<hash>.

    Switch to prod:

    bash
    terraform workspace select prod
    terraform workspace show
    terraform apply -auto-approve
    terraform output current_workspace

    A different bucket is created: linuxlab-prod-<hash>. These two workspaces have separate states, so each one creates its own resource.

    Check it:

    bash
    aws --endpoint-url=http://localstack:4566 s3 ls

    You see both buckets, linuxlab-dev-... and linuxlab-prod-....

    ✓ Workspaces isolated the state: two different buckets from one set of HCL.

  3. 03

    The danger of workspaces: apply to the wrong env

    The main problem with workspaces is that it is easy to lose track of which one is active.

    The scenario: you are working in dev, you switch to prod to look at something, then you forget to switch back. You run terraform apply, and you have applied your dev changes to the prod state.

    A guard is a precondition on an external data source that checks the workspace:

    hcl
    check "workspace_match" {
      assert {
        condition     = terraform.workspace != "default"
        error_message = "Apply on the 'default' workspace is not allowed: choose dev/stage/prod."
      }
    }

    This is part of a safety belt. On a large team, workspaces are not used for prod, precisely to rule out this mistake. For feature-branch experiments in a local dev, they are fine.

    Go back to dev:

    bash
    terraform workspace select dev

    ✓ You see the danger. In production, use directory style.

  4. 04

    Option 2: a shared module for dirs-style

    bash
    mkdir -p /home/student/tf-envs/dirs-style/modules/app
    cat > /home/student/tf-envs/dirs-style/modules/app/main.tf <<'EOF'
    resource "random_id" "suffix" {
      byte_length = 4
    }
    resource "aws_s3_bucket" "demo" {
      bucket = "linuxlab-${var.env}-${random_id.suffix.hex}"
      tags = {
        Environment = var.env
      }
    }
    output "bucket_arn" {
      value = aws_s3_bucket.demo.arn
    }
    EOF
    cat > /home/student/tf-envs/dirs-style/modules/app/variables.tf <<'EOF'
    variable "env" {
      type = string
      validation {
        condition     = contains(["dev", "stage", "prod"], var.env)
        error_message = "env must be dev, stage, or prod."
      }
    }
    EOF

    This is a reusable module, app. It takes env and creates a bucket with the right name.

    Now the env roots:

    ✓ The app module is described. Now the roots per environment.

  5. 05

    The dev and prod roots

    bash
    cat > /home/student/tf-envs/dirs-style/envs/dev/main.tf <<'EOF'
    module "app" {
      source = "../../modules/app"
      env    = "dev"
    }
    output "bucket_arn" {
      value = module.app.bucket_arn
    }
    EOF
    cat > /home/student/tf-envs/dirs-style/envs/prod/main.tf <<'EOF'
    module "app" {
      source = "../../modules/app"
      env    = "prod"
    }
    output "bucket_arn" {
      value = module.app.bucket_arn
    }
    EOF

    Apply dev:

    bash
    cd /home/student/tf-envs/dirs-style/envs/dev
    terraform init
    terraform apply -auto-approve
    terraform output bucket_arn

    Apply prod:

    bash
    cd /home/student/tf-envs/dirs-style/envs/prod
    terraform init
    terraform apply -auto-approve
    terraform output bucket_arn

    Each env has its own .terraform/ and its own state. You cannot accidentally apply the dev config to prod, for that you would have to physically move into the other directory.

    ✓ Two env roots, each with its own state. The isolation is solid.

    The same thing on OpenTofu

    OpenTofu keeps the CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On a first switch, though, back up the state and do a run on a feature branch, the differences cluster in the newer features (variables in backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  6. 06

    Compare the two approaches

    Move into one of the env roots:

    bash
    cd /home/student/tf-envs/dirs-style/envs/dev
    terraform state list

    You see:

    module.app.aws_s3_bucket.demo
    module.app.random_id.suffix

    A comparison:

    AspectWorkspacesDirectory-per-env
    HCL is duplicatedNoMinimal (thin root, everything in the module)
    Backend is duplicatedNo, singleYes (each env its own backend)
    Accidental apply to the wrong envEasyHard. You need a cd
    Divergence between envsImpossible, one set of HCLPossible (good or bad?)
    Complexity to startLowMedium
    Production-scaleNot recommendedStandard

    In real work: workspaces for short experiments, dirs-per-env for anything serious. Many companies use Terragrunt to manage dirs-style (see the advanced track).

    See tf-workspace for workspace details and tf-init-backends for backend per env.

    ✓ The intermediate track is done. Next: production.

    When workspaces are the only option

    Despite everything said above, workspaces are still useful:

    • Short-lived feature branches. You test a PR feature in an isolated state and tear it down after the merge. Duplicating a directory for two days is overkill.
    • One or two developers on a project. The complexity of directory-style pays off at 5+ people. Solo, a workspace is simpler.
    • Envs that stay close. If dev and prod must be bit-for-bit identical, a workspace guarantees that (one set of HCL). Directory-style in theory allows drift.

    The antipattern: workspaces for separating customers ("tenant per workspace"). At a hundred tenants the state operations (refresh, plan) on a single backend get slow, and the risk of mixing them up grows. For multi-tenant, use directory-style or multi-account.

    • → terraform workspace
    • → Init and backend configuration

Что ты узнал

Workspaces are lightweight, one set of HCL, the switch lives in state. They fit feature-branch experiments or envs that stay close to each other. A directory layout is heavier to start but safer and easier to scale. Production usually uses directory-per-env with a shared module.

команды

  • terraform workspace new devcreate a workspace
  • terraform workspace select prodswitch the active one
  • terraform workspace showwhich one is active right now: check this before apply
  • cd envs/prod && terraform applydirectory style: env = directory

концепции

  • · workspace.name is available in HCL as terraform.workspace
  • · Without an explicit workspace check it is easy to apply a prod plan thinking it is dev
  • · Directory style plus a shared module: the production default

← предыдущий

Debugging. TF_LOG, the graph, reading someone else's error

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies