Terraform and OpenTofu Project Structure: Recommended Layouts

· 2 min read · Terraform & OpenTofu Tutorials

Organize the code so that it can grow #

The right structure depends on the size of the project, but the principles are the same: small state files, reusable modules, explicit environments and consistent names. Start simple and split when something hurts.

Level 1: a single configuration #

For a small project or a tutorial:

infra/
  main.tf          # resources
  variables.tf     # inputs
  outputs.tf       # outputs
  providers.tf     # provider configuration
  versions.tf      # required_version and required_providers
  terraform.tfvars # values
  README.md

Terraform reads every .tf file of the directory, so you can split main.tf by topic (network.tf, compute.tf, database.tf) when it gets long. Pin the versions:

versions.tf
terraform {
  required_version = ">= 1.6"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Commit .terraform.lock.hcl so everyone uses the same provider builds, and ignore .terraform/, *.tfstate and *.tfplan.

Level 2: modules and environments #

When you need the same infrastructure in several environments:

infra/
  modules/
    network/
      main.tf
      variables.tf
      outputs.tf
    app/
    database/
  environments/
    dev/
      main.tf          # module "network" { source = "../../modules/network" ... }
      backend.tf
      terraform.tfvars
    pro/
      main.tf
      backend.tf
      terraform.tfvars

Each environment has its own backend and state. See workspaces vs directories for the alternatives and Terraform modules to write them.

Level 3: split by layer and lifecycle #

A single state for everything makes every plan slow and every mistake dangerous. Split by how often things change and who owns them:

infra/
  live/
    pro/
      eu-west-1/
        00-account/       # IAM, organizations, KMS
        10-network/       # VPC, subnets, routing
        20-data/          # RDS, S3
        30-apps/          # ECS, EC2, load balancers

Lower layers change rarely and are read by upper layers with data sources or SSM. Terragrunt automates the dependencies between layers.

Modules: local, versioned and shared #

  • Keep modules small and focused on one thing (a network, a database), with clear inputs and outputs.
  • In a monorepo, reference them with a relative path. When many teams consume them, publish them in a Git repository or a registry and pin the version:
main.tf
module "network" {
  source = "git::https://github.com/my-org/terraform-modules.git//network?ref=v1.4.0"
}

Conventions #

  • Names with snake_case, resources named this or by role when there is only one of a kind.
  • Follow a naming and tagging convention.
  • Document every variable and output with description.
  • Run fmt, validate, lint and security scan automatically in CI.
  • Add a README per module and example configurations in examples/, which also serve as tests.

Signs that you must split the state #

  • plan takes more than a few minutes.
  • Different teams need to change different parts at the same time and wait for the lock.
  • A small change could break something critical, such as the network.
  • The blast radius of one wrong apply is bigger than you can accept.

For the complete list of recommendations read Terraform best practices.

#Terraform #OpenTofu #Best Practices