Best practices for root modules

This document provides guidelines and recommendations to consider when using root modules.

Root configurations or root modules are the working directories from which you run the Terraform CLI. Make sure that root configurations adhere to the following standards (and to the previous Terraform guidelines where applicable). Explicit recommendations for root modules supersede the general guidelines.

This guide is not an introduction to Terraform. For an introduction to using Terraform with Google Cloud, see Get started with Terraform.

Minimize the number of resources in each root module

It is important to keep a single root configuration from growing too large, with too many resources stored in the same directory and state. All resources in a particular root configuration are refreshed every time Terraform is run. This can cause slow execution if too many resources are included in a single state. A general rule: Don't include more than 100 resources (and ideally only a few dozen) in a single state.

Use separate directories for each application

To manage applications and projects independently of each other, put resources for each application and project in their own Terraform directories. A service might represent a particular application or a common service such as shared networking. Nest all Terraform code for a particular service under one directory (including subdirectories).

Split applications into environment-specific subdirectories

When deploying services in Google Cloud, split the Terraform configuration for the service into two top-level directories: a modules directory that contains the actual configuration for the service, and an environments directory that contains the root configurations for each environment.

-- SERVICE-DIRECTORY/
   -- OWNERS
   -- modules/
      -- <service-name>/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README
      -- ...other…
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf

      -- qa/
         -- backend.tf
         -- main.tf

      -- prod/
         -- backend.tf
         -- main.tf

Use environment directories

To share code across environments, reference modules. Typically, this might be a service module that includes the base shared Terraform configuration for the service. In service modules, hard-code common inputs and only require environment-specific inputs as variables.

Each environment directory must contain the following files:

  • A backend.tf file, declaring the Terraform backend state location (typically, Cloud Storage)
  • A main.tf file that instantiates the service module

Each environment directory (dev, qa, prod) corresponds to a default Terraform workspace and deploys a version of the service to that environment. These workspaces isolate environment-specific resources into their own contexts. Use only the default workspace.

Having multiple CLI workspaces within an environment isn't recommended for the following reasons:

  • It can be difficult to inspect the configuration in each workspace.
  • Having a single shared backend for multiple workspaces isn't recommended because the shared backend becomes a single point of failure if it is used for environment separation.
  • While code reuse is possible, code becomes harder to read having to switch based on the current workspace variable (for example, terraform.workspace == "foo" ? this : that).

For more information, see the following:

Expose outputs through remote state

Make sure you're exposing useful outputs of module instances from a root module.

For example, the following code snippet passes through the project ID output from the project factory module instance as an output of the root module.

# Project root module
terraform {
  backend "gcs" {
    bucket  = "BUCKET"
  }
}

module "project" {
  source  = "terraform-google-modules/project-factory/google"
  ...
}

output "project_id" {
  value       = module.project.project_id
  description = "The ID of the created project"
}

Other Terraform environments and applications can reference root module-level outputs only.

By using remote state, you can reference root module outputs. To allow use by other dependent apps for configuration, make sure you're exporting information that's related to a service's endpoints, to remote state.

# Networks root module
data "terraform_remote_state" "network_project" {
  backend = "gcs"

  config = {
    bucket = "BUCKET"
  }
}

module "vpc" {
  source  = "terraform-google-modules/network/google"
  version = "~> 9.0"

  project_id   = data.terraform_remote_state.network_project.outputs.project_id
  network_name = "vpc-1"
  ...
}

Sometimes, such as when invoking a shared service module from environment directories, it is appropriate to re-export the entire child module, as follows:

output "service" {
  value       = module.service
  description = "The service module outputs"
}

Pin to minor provider versions

In root modules, declare each provider and pin to a minor version. This allows automatic upgrade to new patch releases while still keeping a solid target. For consistency, name the versions file versions.tf.

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0.0"
    }
  }
}

Store variables in a tfvars file

For root modules, provide variables by using a .tfvars variables file. For consistency, name variable files terraform.tfvars.

Don't specify variables by using alternative var-files or var='key=val' command-line options. Command-line options are ephemeral and easy to forget. Using a default variables file is more predictable.

Check in .terraform.lock.hcl file

For root modules, the .terraform.lock.hcl dependency lock file should be checked into source control. This allows for tracking and reviewing changes in provider selections for a given configuration.

What's next