How to structure and scale your Terraform project

2022-11-06

Terraform is a powerful tool for managing infrastructure as code. When working with multiple cloud accounts, how your Terraform project is structured can have a big impact on the maintainability of your code. If not done right from the start, managing the project can be a pain.

In this article, I will share some of the best practices and how you can structure your Terraform project that is scalable while keeping it sane, based on my many painful and learned experiences when managing Terraform projects.

Basic structure of a Terraform project

basic
├── main.tf
└── providers.tf

module
├── main.tf
├── variables.tf
├── outputs.tf
└── providers.tf

In each Terraform project, at the minimum you should have:

  • A main.tf file that contains the main resource definitions.
  • A provider.tf file that contains the provider configuration and versions. This helps to make it easier to locate and manage all your provider configurations.

Optional but required for Terraform modules:

  • A variables.tf file that contains the variables used in the main.tf file.
  • A outputs.tf file that contains the outputs from the main.tf file.

Contrary to other best practices, avoid the temptation to create many files for each directory, unless it is absolutely necessary. This makes it harder to manage and maintain as your Terraform project scales.

Separate directory and Terraform workspace for each cloud account

When working with multiple cloud accounts, it is best to separate the Terraform code into different directories. This way, you can easily switch between the different directories and workspaces to manage different cloud accounts.

For example, if you have two aws cloud accounts and one azure cloud account, you can structure your Terraform project like this:

.
├── aws-01
│   ├── main.tf
│   └── providers.tf
├── aws-02
│   ├── main.tf
│   └── providers.tf
└── azure
    ├── main.tf
    └── providers.tf

Define the workspace for each cloud account

You should define your Terraform workspaces for each cloud account in the providers.tf file.

terraform {
  backend "remote" {
    workspaces {
      name = "aws-01"
    }
  }
}

Shared modules directory for all workspaces

When working with multiple cloud accounts, you will most likely have some resources that can be reusable across all your workspaces.

.
├── aws-01
│   ├── main.tf
│   └── providers.tf
├── aws-02
│   ├── main.tf
│   └── providers.tf
├── azure
│   ├── main.tf
│   └── providers.tf
└── modules
    ├── rds
    │   ├── main.tf
    │   ├── outputs.tf
    │   ├── providers.tf
    │   └── variables.tf
    └── storage
        ├── main.tf
        ├── outputs.tf
        ├── providers.tf
        └── variables.tf

Keep your Terraform module small and specific

When creating a Terraform module, it is best to keep it small and specific. This way, it is easier to reuse and maintain.

For example, you might require a module that creates a VPC and RDS separately, instead of creating a module that tries to create a complete system.

Define your system classes as modules but not shared anywhere (even within your workspace)

A system class is so-called 'a set of resources' that defines a system with all its supporting resources. For example, a system class can be your complete working infrastructure which consists of a VPC, subnets, security groups, load balancers, etc.

While it can be tempting to define your system classes as shared Terraform modules, it is not recommended. Doing so, any changes to your system classes will impact all your workspaces and/or your system classes. This can lead to many awkward conditional statements which can be tricky (and risky) to manage and prone to accidental changes if you have different variations of the same system class.

It is better to define them as separate, non-sharable modules. This way, you can easily manage them separately and have more control over the deployments. Admittedly, this will result in repeated code (as opposed to keeping it DRY), but it is a small price to pay for the benefits.

.
├── aws-01
│   ├── main.tf
│   ├── providers.tf
│   ├── web-app-01       // system class
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── providers.tf
│   │   └── variables.tf
│   └── web-app-02       // system class
│       ├── main.tf
│       ├── outputs.tf
│       ├── providers.tf
│       └── variables.tf
└── modules
    ├── rds
    │   ├── main.tf
    │   ├── outputs.tf
    │   ├── providers.tf
    │   └── variables.tf
    └── s3
        ├── main.tf
        ├── outputs.tf
        ├── providers.tf
        └── variables.tf

In your aws-01/main.tf file, you can then reference the system classes like so:

module "web-app-01" {
  source = "./web-app-01"
  // ...
}

module "web-app-02" {
  source = "./web-app-02"
  // ...
}

Decouple critical resources within your system class

Critical resources such as databases, storage, etc. should be decoupled from the system class to avoid accidental changes that may render, for example, persistence data to be removed.

For example, you might want to manage your database separately from your web application.

.
└── aws-01
    ├── main.tf
    ├── providers.tf
    └── web-app-01
        ├── database
        │   ├── main.tf
        │   └── providers.tf
        ├── main.tf
        ├── outputs.tf
        ├── providers.tf
        └── variables.tf

As you can see, whenever you do a terraform apply in the aws-01 directory, it will only apply the resources in the web-app-01 directory and not the database directory. This is because the database directory is not referenced in the web-app-01/main.tf file.

Set default tags for your workspaces and system classes

When your infrastructure grows, it is best to set default tags from the start for each of your workspaces and system classes. This way, you can easily identify the resources that are provisioned by Terraform.

Setting tags can be done inside each of your providers.tf file, like so:

provider "aws" {
  // ...

  default_tags {
    tags = {
      "provisioned_by"   = "Terraform"
      "workspace"        = "aws-01"
      // and optionally
      "system_class"     = "web-app-01"
    }
  }
}

Conclusion

With the above tips, you should be able to manage your Terraform project with ease and at scale. An important takeaway from all this is to:

  • Keep your workspaces and/or multiple cloud accounts separate
  • Prefer small and specific modules over large and complex modules
  • Define your system classes as strictly non-sharable modules
  • Decouple critical resources from your system classes

What are your thoughts on the above? Any tips for an even better structure? Scroll down below and let's have a chat!