How to modularize Terraform code


by Thomas Tran



Terraform modules allow us to group resources together, define input variables to allow different configurations, and define export variables to define how other resources or modules can use. You can think of modules as self-contained packages which can be used in different places. Modules can even be shared across teams in an organization or as open-source exports!

Let’s say you have the following Datadog resource that is not modularized:

main.tf

locals {
  tags = [
    "team:SampleTeamName" 
  ]
}

resource "datadog_monitor" "aws_lambda_errors" {
   count      = var.environment == "production" ? 1 : 0
   type       = "query alert"
   name       = "AWS Lambda encountered errors"
   message    = "AWS Lambda encountered errors. Please investigate! @slack-channel"
   query      = "sum(last_1d):sum:aws.lambda.errors{service:sample_app}.as_count() > 1"
   
   monitor_thresholds {
     critical = 1
   }
  enable_logs_sample = true
  notify_no_data     = false
  notify_audit       = false
  include_tags       = true

  tags  = local.tags
}

This sets up a Datadog monitor which will alert a Slack channel if there were any Lambda errors in the past day.

Even though the above Terraform will work, it cannot be generalized and transferable to other places in the code. Let’s go ahead and modularize it!

First, in the same file as the above code, delete all the code and add the following block:

module "datadog_monitors" {
  source       = "../../modules/monitoring"
  environment  = "production"
}

This will cause Terraform to “call” the module and execute all the code located within. Next, create a new directory called “modules”. Under that directory, create another directory called “monitoring” that will contain our code. In the “monitoring” directory, create the following files:

main.tf

locals {
  tags = [
    "team:SampleTeamName",
    "environment:${var.environment}"
  ]
}

resource "datadog_monitor" "aws_lambda_errors" {
   count      = var.environment == "production" ? 1 : 0
   type       = "query alert"
   name       = "AWS Lambda encountered errors"
   message    = "AWS Lambda encountered errors. Please investigate! @slack-channel"
   query      = "sum(last_1d):sum:aws.lambda.errors{service:sample_app}.as_count() > 1"
   
   monitor_thresholds {
     critical = 1
   }
  enable_logs_sample = true
  notify_no_data     = false
  notify_audit       = false
  include_tags       = true

  tags  = local.tags
}

variables.tf

variable "environment" {}

versions.tf

terraform {
  required_providers {
    datadog = {
      source = "datadog/datadog"
    }
  }
  required_version = ">= 0.14"
}

There! Much better, right? We now have a module within ../../modules/monitoring that can be re-used in multiple places. In ../../modules/monitoring/variables.tf, we declare an input variable called environment which we will pass in when we call the module. Finally, we have the Datadog provider in versions.tf which will allow us to access the datadog_monitor resource from inside ../../modules/monitoring/main.tf.